Initial commit: admin dashboard for GameNo.

Vue 3 + Vite dashboard with PrimeVue, i18n, and API integration for institution management.
This commit is contained in:
2026-08-09 04:18:09 +02:00
commit 1a34c3b133
94 changed files with 12016 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
<!-- /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 || 'کاربر' }} {{ payment.user?.surname || '' }}</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?.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((payment.amount - (payment.paidAmount || 0)).toLocaleString()) }} تومان</span>
</div>
<div>
<span class="text-muted text-xs block mb-1">وضعیت پرداخت</span>
<StatusTag :status="payment.status" type="payment" />
</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="method" header="روش پرداخت">
<template #body="{ data }">
<Tag :value="data.method === 'online' ? 'درگاه آنلاین' : (data.method === 'card' ? 'کارت به کارت' : 'نقدی')" severity="info" />
</template>
</Column>
<Column field="receiptNumber" header="شماره فیش / پیگیری">
<template #body="{ data }">
<span dir="ltr">{{ toPersianDigits(data.receiptNumber || '-') }}</span>
</template>
</Column>
<Column field="createdAt" header="تاریخ ثبت">
<template #body="{ data }">
{{ formatJalali(data.createdAt || data.date) }}
</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>
<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, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { paymentApi } from '@/api/paymentApi';
import { usePersianDate } from '@/composables/usePersianDate';
import { useToast } from '@/composables/useToast';
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';
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: ''
});
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;
fetchPayment();
} catch (err) {
showError(err);
} finally {
isSubmittingTrx.value = false;
}
};
onMounted(() => {
fetchPayment();
});
</script>
+250
View File
@@ -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>