Files
gameno-dashboard/src/views/payments/PaymentDetailView.vue
T

490 lines
20 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="openTransactionModal" />
</PermissionGate>
</div>
<DataTable
:value="payment.transactions || []"
class="p-datatable-sm text-sm"
:rowClass="transactionRowClass"
>
<Column field="amount" header="مبلغ تراکنش">
<template #body="{ data }">
<span :class="{ 'line-through text-muted': isTransactionCancelled(data) }">
{{ toPersianDigits(data.amount?.toLocaleString()) }} تومان
</span>
</template>
</Column>
<Column field="status" header="وضعیت">
<template #body="{ data }">
<StatusTag :status="transactionStatus(data)" 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: 7rem">
<template #body="{ data }">
<PermissionGate permission="payments:update">
<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>
</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>
<InputGroup>
<InputNumber v-model="trxForm.amount" class="w-full text-sm" :min="1" />
<InputGroupAddon>تومان</InputGroupAddon>
</InputGroup>
</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="getTodayJalali()" />
</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="getTodayJalali()" />
</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>
<NotifyChannelsField :notify="trxNotify" />
</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>
<InputGroup>
<InputNumber v-model="editForm.amount" class="w-full text-sm" :min="0" />
<InputGroupAddon>تومان</InputGroupAddon>
</InputGroup>
</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="getTodayJalali()" />
</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="getTodayJalali()" />
</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>
<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>
<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 NotifyChannelsField from '@/components/common/NotifyChannelsField.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, getTodayJalali } = usePersianDate();
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 trxNotify = reactive({ sms: true, email: true, bot: true });
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: getTodayJalali(),
date: getTodayJalali(),
notes: ''
});
const trxForm = reactive(defaultTrxForm());
const openTransactionModal = () => {
Object.assign(trxForm, defaultTrxForm());
if (remainingAmount.value > 0) {
trxForm.amount = remainingAmount.value;
}
trxForm.dueDate = getTodayJalali();
trxForm.date = getTodayJalali();
showTransactionModal.value = true;
};
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 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);
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; }
isSubmittingTrx.value = true;
try {
const payload = {
amount: trxForm.amount,
status: trxForm.status,
dueDate,
notes: trxForm.notes || undefined,
notify: { ...trxNotify }
};
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());
trxNotify.sms = true;
trxNotify.email = true;
trxNotify.bot = true;
} catch (err) {
showError(err);
} finally {
isSubmittingTrx.value = false;
}
};
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');
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;
}
};
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>