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
+255
View File
@@ -0,0 +1,255 @@
<!-- /src/views/sessions/SessionListView.vue -->
<template>
<div class="session-list-view">
<PageHeader :title="$t('sessions.title')" :subtitle="$t('sessions.subtitle')">
<PermissionGate permission="sessions:create">
<Button :label="$t('sessions.addSession')" icon="pi pi-plus" @click="$router.push('/sessions/create')" />
</PermissionGate>
</PageHeader>
<DataTableWrapper
:items="items"
:totalCount="totalCount"
:page="queryParams.page"
:limit="queryParams.limit"
:sortBy="queryParams.sortBy"
:sortOrder="queryParams.sortOrder"
:loading="isLoading"
selectable
v-model:selection="selectedSessions"
@page-change="onPageChange"
@sort-change="onSort"
@search-change="onSearch"
>
<template #toolbar>
<div v-if="selectedSessions.length" class="flex align-items-center gap-2 flex-wrap justify-content-end">
<Tag :value="`${toPersianDigits(selectedSessions.length)} انتخاب‌شده`" severity="info" />
<PermissionGate permission="sessions:update">
<Select
v-model="bulkStatus"
:options="statusOptions"
optionLabel="label"
optionValue="value"
placeholder="تغییر وضعیت"
class="text-sm"
style="min-width: 10rem"
/>
<Button
label="اعمال وضعیت"
icon="pi pi-check"
size="small"
:loading="isBulkUpdating"
:disabled="!bulkStatus"
@click="applyBulkStatus"
/>
</PermissionGate>
<PermissionGate permission="sessions:delete">
<Button
label="حذف انتخاب‌شده‌ها"
icon="pi pi-trash"
size="small"
severity="danger"
outlined
@click="confirmBulkDelete"
/>
</PermissionGate>
<Button label="لغو انتخاب" text size="small" severity="secondary" @click="selectedSessions = []" />
</div>
</template>
<Column field="topic" header="موضوع جلسه">
<template #body="{ data }">
<span class="font-semibold text-color">{{ data.topic || '—' }}</span>
</template>
</Column>
<Column field="course" header="عنوان دوره">
<template #body="{ data }">
<span class="font-bold text-color">{{ data.course?.title || data.courseTitle || '-' }}</span>
</template>
</Column>
<Column field="class" header="کلاس">
<template #body="{ data }">
{{ data.class?.name || '—' }}
</template>
</Column>
<Column field="professor" header="استاد مدرس">
<template #body="{ data }">
{{
data.professor
? `${data.professor.name || ''} ${data.professor.surname || ''}`.trim() || '—'
: (data.professorName || '—')
}}
</template>
</Column>
<Column field="date" header="تاریخ برگزاری" sortable sortField="day">
<template #body="{ data }">
{{ formatJalali(data.day || data.date) }}
</template>
</Column>
<Column field="timeRange" header="زمان برگزاری">
<template #body="{ data }">
{{ data.startTime || '—' }} - {{ data.endTime || '—' }}
</template>
</Column>
<Column field="status" header="وضعیت">
<template #body="{ data }">
<StatusTag :status="data.status || 'scheduled'" type="session" />
</template>
</Column>
<Column header="عملیات" style="width: 170px">
<template #body="{ data }">
<div class="flex align-items-center gap-1">
<PermissionGate permission="sessions:attendance">
<Button
icon="pi pi-check-square"
text
rounded
size="small"
severity="success"
v-tooltip.top="$t('sessions.enterAttendance')"
@click="$router.push(`/sessions/attendance/${data._id || data.id}`)"
/>
</PermissionGate>
<PermissionGate permission="sessions:update">
<Button icon="pi pi-pencil" text rounded size="small" severity="warning" v-tooltip.top="'ویرایش'" @click="$router.push(`/sessions/edit/${data._id || data.id}`)" />
</PermissionGate>
<PermissionGate permission="sessions:delete">
<Button icon="pi pi-trash" text rounded size="small" severity="danger" v-tooltip.top="'حذف'" @click="confirmDelete(data)" />
</PermissionGate>
</div>
</template>
</Column>
</DataTableWrapper>
<ConfirmDeleteDialog
v-model="deleteDialogVisible"
:loading="isDeleting"
:title="bulkDeleteMode ? 'حذف گروهی جلسات' : ''"
:message="bulkDeleteMessage"
@confirm="handleDeleteConfirm"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useDataTable } from '@/composables/useDataTable';
import { usePersianDate } from '@/composables/usePersianDate';
import { useToast } from '@/composables/useToast';
import { sessionApi } from '@/api/sessionApi';
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 Select from 'primevue/select';
const { formatJalali, toPersianDigits } = usePersianDate();
const { showSuccess, showError } = useToast();
const {
items,
totalCount,
isLoading,
queryParams,
loadData,
onPageChange,
onSort,
onSearch
} = useDataTable(sessionApi.getAll, {
sortBy: 'day',
sortOrder: 'asc'
});
const selectedSessions = ref([]);
const bulkStatus = ref(null);
const isBulkUpdating = ref(false);
const deleteDialogVisible = ref(false);
const selectedSession = ref(null);
const bulkDeleteMode = ref(false);
const isDeleting = ref(false);
const statusOptions = [
{ label: 'طبق برنامه', value: 'scheduled' },
{ label: 'برگزار شده', value: 'held' },
{ label: 'لغو شده', value: 'cancelled' }
];
const selectedIds = computed(() =>
selectedSessions.value.map((s) => s._id || s.id).filter(Boolean)
);
const bulkDeleteMessage = computed(() => {
if (bulkDeleteMode.value) {
return `آیا از حذف ${toPersianDigits(selectedSessions.value.length)} جلسه انتخاب‌شده اطمینان دارید؟ این عملیات قابل بازگشت نیست.`;
}
return 'آیا از حذف این جلسه اطمینان دارید؟ این عملیات قابل بازگشت نیست.';
});
const confirmDelete = (session) => {
bulkDeleteMode.value = false;
selectedSession.value = session;
deleteDialogVisible.value = true;
};
const confirmBulkDelete = () => {
if (!selectedSessions.value.length) return;
bulkDeleteMode.value = true;
selectedSession.value = null;
deleteDialogVisible.value = true;
};
const handleDeleteConfirm = async () => {
isDeleting.value = true;
try {
if (bulkDeleteMode.value) {
const res = await sessionApi.bulkDelete(selectedIds.value);
const deletedCount = res?.data?.deletedCount ?? selectedIds.value.length;
showSuccess(`${toPersianDigits(deletedCount)} جلسه حذف شد`);
selectedSessions.value = [];
} else if (selectedSession.value) {
await sessionApi.delete(selectedSession.value._id || selectedSession.value.id);
showSuccess('جلسه با موفقیت حذف شد');
}
deleteDialogVisible.value = false;
await loadData();
} catch (err) {
showError(err);
} finally {
isDeleting.value = false;
}
};
const applyBulkStatus = async () => {
if (!selectedIds.value.length || !bulkStatus.value) return;
isBulkUpdating.value = true;
try {
const res = await sessionApi.bulkUpdateStatus(selectedIds.value, bulkStatus.value);
const updatedCount = res?.data?.updatedCount ?? selectedIds.value.length;
showSuccess(`وضعیت ${toPersianDigits(updatedCount)} جلسه به‌روز شد`);
bulkStatus.value = null;
selectedSessions.value = [];
await loadData();
} catch (err) {
showError(err);
} finally {
isBulkUpdating.value = false;
}
};
onMounted(() => {
loadData();
});
</script>