Initial commit: admin dashboard for GameNo.
Vue 3 + Vite dashboard with PrimeVue, i18n, and API integration for institution management.
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
<!-- /src/views/payments/PaymentListView.vue -->
|
||||
<template>
|
||||
<div class="payment-list-view">
|
||||
<PageHeader :title="$t('payments.title')" :subtitle="$t('payments.subtitle')">
|
||||
<PermissionGate permission="payments:create">
|
||||
<Button :label="$t('payments.addPayment')" icon="pi pi-plus" severity="success" @click="showCreateModal = true" />
|
||||
</PermissionGate>
|
||||
</PageHeader>
|
||||
|
||||
<DataTableWrapper
|
||||
:items="items"
|
||||
:totalCount="totalCount"
|
||||
:page="queryParams.page"
|
||||
:limit="queryParams.limit"
|
||||
:sortBy="queryParams.sortBy"
|
||||
:sortOrder="queryParams.sortOrder"
|
||||
:loading="isLoading"
|
||||
@page-change="onPageChange"
|
||||
@sort-change="onSort"
|
||||
@search-change="onSearch"
|
||||
>
|
||||
<Column field="user" header="نام دانشجو / کاربر">
|
||||
<template #body="{ data }">
|
||||
<router-link :to="`/payments/view/${data._id || data.id}`" class="font-bold text-color hover:text-primary">
|
||||
{{ data.user?.name || data.userName || 'کاربر' }} {{ data.user?.surname || '' }}
|
||||
</router-link>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="classes" header="کلاسهای مربوطه">
|
||||
<template #body="{ data }">
|
||||
<div class="flex flex-wrap gap-1" v-if="data.classes && data.classes.length">
|
||||
<Tag v-for="c in data.classes" :key="c._id || c" :value="c.name || 'کلاس'" severity="info" class="text-xs" />
|
||||
</div>
|
||||
<span v-else-if="data.course" class="text-muted text-xs">{{ data.course?.title }}</span>
|
||||
<span v-else class="text-muted text-xs">-</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="amount" header="مبلغ کل (تومان)" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ toPersianDigits((data.amount || 0).toLocaleString()) }} تومان
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="paidAmount" header="مبلغ پرداختی">
|
||||
<template #body="{ data }">
|
||||
{{ toPersianDigits((data.paidAmount || 0).toLocaleString()) }} تومان
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="dueDate" header="تاریخ سررسید" sortable>
|
||||
<template #body="{ data }">
|
||||
{{ formatJalali(data.dueDate) }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="status" header="وضعیت">
|
||||
<template #body="{ data }">
|
||||
<StatusTag :status="data.status" type="payment" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="عملیات" style="width: 110px">
|
||||
<template #body="{ data }">
|
||||
<div class="flex align-items-center gap-1">
|
||||
<PermissionGate permission="payments:read">
|
||||
<Button icon="pi pi-eye" text rounded size="small" v-tooltip.top="'مشاهده'" @click="$router.push(`/payments/view/${data._id || data.id}`)" />
|
||||
</PermissionGate>
|
||||
|
||||
<PermissionGate permission="payments:delete">
|
||||
<Button icon="pi pi-trash" text rounded size="small" severity="danger" v-tooltip.top="'حذف'" @click="confirmDelete(data)" />
|
||||
</PermissionGate>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTableWrapper>
|
||||
|
||||
<!-- Create Payment Modal -->
|
||||
<Dialog v-model:visible="showCreateModal" header="ایجاد صورتحساب جدید" modal :style="{ width: '480px' }">
|
||||
<div class="flex flex-column gap-3 py-2">
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب کاربر / دانشجو *</label>
|
||||
<Dropdown v-model="createForm.user" :options="usersList" optionLabel="fullName" optionValue="_id" filter placeholder="کاربر را انتخاب کنید" class="w-full text-sm" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب یک یا چند کلاس مربوطه *</label>
|
||||
<MultiSelect
|
||||
v-model="createForm.classes"
|
||||
:options="classesList"
|
||||
optionLabel="name"
|
||||
optionValue="_id"
|
||||
display="chip"
|
||||
placeholder="کلاسها را انتخاب کنید"
|
||||
class="w-full text-sm"
|
||||
@change="onClassesSelected"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">مبلغ کل صورتحساب (تومان) *</label>
|
||||
<InputNumber v-model="createForm.amount" class="w-full text-sm" suffix=" تومان" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
||||
<DatePicker v-model="createForm.dueDate" class="w-full text-sm" placeholder="1403/01/01" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="انصراف" text severity="secondary" @click="showCreateModal = false" />
|
||||
<Button label="ایجاد صورتحساب" icon="pi pi-check" severity="success" :loading="isCreating" @click="handleCreatePayment" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
v-model="deleteDialogVisible"
|
||||
:loading="isDeleting"
|
||||
@confirm="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useDataTable } from '@/composables/useDataTable';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { paymentApi } from '@/api/paymentApi';
|
||||
import { userApi } from '@/api/userApi';
|
||||
import { classApi } from '@/api/classApi';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import DataTableWrapper from '@/components/common/DataTableWrapper.vue';
|
||||
import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
|
||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||
import StatusTag from '@/components/common/StatusTag.vue';
|
||||
import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import Tag from 'primevue/tag';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Dropdown from 'primevue/select';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import DatePicker from 'vue3-persian-datetime-picker';
|
||||
|
||||
const { toPersianDigits, formatJalali } = usePersianDate();
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
const {
|
||||
items,
|
||||
totalCount,
|
||||
isLoading,
|
||||
queryParams,
|
||||
loadData,
|
||||
onPageChange,
|
||||
onSort,
|
||||
onSearch
|
||||
} = useDataTable(paymentApi.getAll);
|
||||
|
||||
const showCreateModal = ref(false);
|
||||
const usersList = ref([]);
|
||||
const classesList = ref([]);
|
||||
const isCreating = ref(false);
|
||||
|
||||
const createForm = reactive({
|
||||
user: null,
|
||||
classes: [],
|
||||
amount: 0,
|
||||
dueDate: ''
|
||||
});
|
||||
|
||||
const deleteDialogVisible = ref(false);
|
||||
const selectedPayment = ref(null);
|
||||
const isDeleting = ref(false);
|
||||
|
||||
const onClassesSelected = () => {
|
||||
if (!createForm.classes || createForm.classes.length === 0) return;
|
||||
let totalFee = 0;
|
||||
createForm.classes.forEach(classId => {
|
||||
const c = classesList.value.find(item => (item._id || item.id) === classId);
|
||||
if (c) {
|
||||
totalFee += (c.tuitionFee || c.course?.price || 0);
|
||||
}
|
||||
});
|
||||
if (totalFee > 0) {
|
||||
createForm.amount = totalFee;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDropdownData = async () => {
|
||||
try {
|
||||
const [uRes, cRes] = await Promise.all([
|
||||
userApi.getAll({ limit: 150 }),
|
||||
classApi.getAll({ limit: 100 })
|
||||
]);
|
||||
const uData = uRes.data || uRes;
|
||||
const rawUsers = uData.items || uData.users || uData || [];
|
||||
usersList.value = rawUsers.map(u => ({ ...u, fullName: `${u.name} ${u.surname}` }));
|
||||
|
||||
const cData = cRes.data || cRes;
|
||||
classesList.value = cData.items || cData.classes || cData || [];
|
||||
} catch (e) {
|
||||
console.warn('Dropdown fetch error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePayment = async () => {
|
||||
if (!createForm.user) { showError('لطفا کاربر را انتخاب کنید'); return; }
|
||||
if (!createForm.amount) { showError('لطفا مبلغ را وارد کنید'); return; }
|
||||
if (!createForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
|
||||
|
||||
isCreating.value = true;
|
||||
try {
|
||||
await paymentApi.create(createForm);
|
||||
showSuccess('صورتحساب جدید با موفقیت ایجاد شد');
|
||||
showCreateModal.value = false;
|
||||
loadData();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isCreating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDelete = (payment) => {
|
||||
selectedPayment.value = payment;
|
||||
deleteDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedPayment.value) return;
|
||||
isDeleting.value = true;
|
||||
try {
|
||||
await paymentApi.delete(selectedPayment.value._id || selectedPayment.value.id);
|
||||
showSuccess('صورتحساب با موفقیت حذف شد');
|
||||
deleteDialogVisible.value = false;
|
||||
loadData();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isDeleting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
fetchDropdownData();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user