Add admin UI to cancel payment transactions.
Show cancelled rows in history with distinct styling and let admins confirm cancellation so replacement transactions can be added later.
This commit is contained in:
@@ -9,5 +9,6 @@ export const paymentApi = {
|
||||
update: (id, data) => axiosInstance.put(`/payments/admin/update/${id}`, data),
|
||||
delete: (id) => axiosInstance.delete(`/payments/admin/delete/${id}`),
|
||||
recordTransaction: (paymentId, data) => axiosInstance.post(`/payments/admin/transactions/${paymentId}`, data),
|
||||
updateTransaction: (transactionId, data) => axiosInstance.put(`/payments/admin/transactions/${transactionId}`, data)
|
||||
updateTransaction: (transactionId, data) => axiosInstance.put(`/payments/admin/transactions/${transactionId}`, data),
|
||||
cancelTransaction: (transactionId) => axiosInstance.post(`/payments/admin/transactions/${transactionId}/cancel`)
|
||||
};
|
||||
|
||||
@@ -46,7 +46,8 @@ const PAYMENT_LABELS = {
|
||||
paid: 'تسویهشده',
|
||||
partial: 'پرداخت جزئی',
|
||||
pending: 'در انتظار پرداخت',
|
||||
overdue: 'معوق'
|
||||
overdue: 'معوق',
|
||||
cancelled: 'لغوشده'
|
||||
};
|
||||
|
||||
const PENDING_STUDENT_LABELS = {
|
||||
@@ -65,6 +66,7 @@ const severity = computed(() => {
|
||||
if (val === 'partial' || val === 'پرداخت جزئی') return 'contrast';
|
||||
if (val === 'pending' || val === 'در انتظار پرداخت') return 'info';
|
||||
if (val === 'overdue' || val === 'معوق شده' || val === 'معوق') return 'danger';
|
||||
if (val === 'cancelled' || val === 'canceled' || val === 'لغوشده') return 'secondary';
|
||||
}
|
||||
|
||||
if (props.type === 'session') {
|
||||
|
||||
@@ -67,15 +67,21 @@
|
||||
</PermissionGate>
|
||||
</div>
|
||||
|
||||
<DataTable :value="payment.transactions || []" class="p-datatable-sm text-sm">
|
||||
<DataTable
|
||||
:value="payment.transactions || []"
|
||||
class="p-datatable-sm text-sm"
|
||||
:rowClass="transactionRowClass"
|
||||
>
|
||||
<Column field="amount" header="مبلغ تراکنش">
|
||||
<template #body="{ data }">
|
||||
{{ toPersianDigits(data.amount?.toLocaleString()) }} تومان
|
||||
<span :class="{ 'line-through text-muted': isTransactionCancelled(data) }">
|
||||
{{ toPersianDigits(data.amount?.toLocaleString()) }} تومان
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="وضعیت">
|
||||
<template #body="{ data }">
|
||||
<StatusTag :status="data.status || (data.date ? 'paid' : 'pending')" type="payment" />
|
||||
<StatusTag :status="transactionStatus(data)" type="payment" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="method" header="روش پرداخت">
|
||||
@@ -108,10 +114,31 @@
|
||||
<span class="white-space-pre-wrap">{{ data.notes || '—' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="عملیات" style="width: 5rem">
|
||||
<Column header="عملیات" style="width: 7rem">
|
||||
<template #body="{ data }">
|
||||
<PermissionGate permission="payments:update">
|
||||
<Button icon="pi pi-pencil" text rounded severity="secondary" size="small" @click="openEditTransaction(data)" />
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
v-if="!isTransactionCancelled(data)"
|
||||
icon="pi pi-pencil"
|
||||
text
|
||||
rounded
|
||||
severity="secondary"
|
||||
size="small"
|
||||
v-tooltip.top="'ویرایش'"
|
||||
@click="openEditTransaction(data)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!isTransactionCancelled(data)"
|
||||
icon="pi pi-ban"
|
||||
text
|
||||
rounded
|
||||
severity="danger"
|
||||
size="small"
|
||||
v-tooltip.top="'لغو تراکنش'"
|
||||
@click="openCancelTransaction(data)"
|
||||
/>
|
||||
</div>
|
||||
</PermissionGate>
|
||||
</template>
|
||||
</Column>
|
||||
@@ -209,6 +236,20 @@
|
||||
<Button label="ذخیره تغییرات" icon="pi pi-check" severity="success" :loading="isSubmittingEdit" @click="handleUpdateTransaction" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="showCancelModal" header="لغو تراکنش" modal :style="{ width: '420px' }" :closable="!isSubmittingCancel">
|
||||
<div class="flex align-items-start gap-3 py-2">
|
||||
<i class="pi pi-exclamation-triangle text-3xl text-orange-500" />
|
||||
<p class="text-sm text-color m-0 line-height-3">
|
||||
این تراکنش لغو میشود و در محاسبات صورتحساب لحاظ نخواهد شد، اما در تاریخچه باقی میماند.
|
||||
پس از لغو میتوانید تراکنش جدید ثبت کنید.
|
||||
</p>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="انصراف" text severity="secondary" :disabled="isSubmittingCancel" @click="showCancelModal = false" />
|
||||
<Button label="لغو تراکنش" icon="pi pi-ban" severity="danger" :loading="isSubmittingCancel" @click="handleCancelTransaction" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -241,9 +282,12 @@ const { showSuccess, showError } = useToast();
|
||||
const payment = ref(null);
|
||||
const showTransactionModal = ref(false);
|
||||
const showEditModal = ref(false);
|
||||
const showCancelModal = ref(false);
|
||||
const isSubmittingTrx = ref(false);
|
||||
const isSubmittingEdit = ref(false);
|
||||
const isSubmittingCancel = ref(false);
|
||||
const editingTransactionId = ref(null);
|
||||
const cancellingTransactionId = ref(null);
|
||||
|
||||
const methodOptions = [
|
||||
{ label: 'کارت به کارت', value: 'card' },
|
||||
@@ -281,6 +325,15 @@ const editForm = reactive({
|
||||
const payableAmount = computed(() => getPayableAmount(payment.value || {}));
|
||||
const remainingAmount = computed(() => Math.max(0, payableAmount.value - (payment.value?.paidAmount || 0)));
|
||||
|
||||
const isTransactionCancelled = (trx) => String(trx?.status || '').toLowerCase() === 'cancelled';
|
||||
|
||||
const transactionStatus = (trx) => {
|
||||
if (isTransactionCancelled(trx)) return 'cancelled';
|
||||
return trx.status || (trx.date ? 'paid' : 'pending');
|
||||
};
|
||||
|
||||
const transactionRowClass = (trx) => (isTransactionCancelled(trx) ? 'transaction-row-cancelled' : '');
|
||||
|
||||
const fetchPayment = async () => {
|
||||
try {
|
||||
const res = await paymentApi.getOne(paymentId);
|
||||
@@ -328,6 +381,7 @@ const handleRecordTransaction = async () => {
|
||||
};
|
||||
|
||||
const openEditTransaction = (trx) => {
|
||||
if (isTransactionCancelled(trx)) return;
|
||||
editingTransactionId.value = trx._id || trx.id;
|
||||
editForm.amount = trx.amount || 0;
|
||||
editForm.status = trx.status || (trx.date ? 'paid' : 'pending');
|
||||
@@ -379,7 +433,36 @@ const handleUpdateTransaction = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const openCancelTransaction = (trx) => {
|
||||
if (isTransactionCancelled(trx)) return;
|
||||
cancellingTransactionId.value = trx._id || trx.id;
|
||||
showCancelModal.value = true;
|
||||
};
|
||||
|
||||
const handleCancelTransaction = async () => {
|
||||
if (!cancellingTransactionId.value) return;
|
||||
|
||||
isSubmittingCancel.value = true;
|
||||
try {
|
||||
await paymentApi.cancelTransaction(cancellingTransactionId.value);
|
||||
await fetchPayment();
|
||||
showSuccess('تراکنش با موفقیت لغو شد');
|
||||
showCancelModal.value = false;
|
||||
cancellingTransactionId.value = null;
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isSubmittingCancel.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchPayment();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.transaction-row-cancelled) {
|
||||
opacity: 0.65;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user