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,247 @@
|
||||
<!-- /src/views/sessions/SessionFormView.vue -->
|
||||
<template>
|
||||
<div class="session-form-view w-full max-w-5xl mx-auto">
|
||||
<PageHeader
|
||||
:title="isEditMode ? $t('sessions.editSession') : $t('sessions.addSession')"
|
||||
:subtitle="isEditMode ? 'ویرایش اطلاعات جلسه آموزشی' : 'تعریف جلسه جدید'"
|
||||
>
|
||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/sessions')" />
|
||||
</PageHeader>
|
||||
|
||||
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
|
||||
<form @submit.prevent="handleSubmit" class="grid">
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب دوره *</label>
|
||||
<Dropdown
|
||||
v-model="form.course"
|
||||
:options="courses"
|
||||
optionLabel="title"
|
||||
optionValue="_id"
|
||||
placeholder="دوره را انتخاب کنید"
|
||||
class="w-full text-sm"
|
||||
filter
|
||||
@change="onCourseChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">کلاس *</label>
|
||||
<Dropdown
|
||||
v-model="form.class"
|
||||
:options="classes"
|
||||
optionLabel="name"
|
||||
optionValue="_id"
|
||||
placeholder="کلاس را انتخاب کنید"
|
||||
class="w-full text-sm"
|
||||
:disabled="!form.course"
|
||||
filter
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">استاد مدرس *</label>
|
||||
<Dropdown v-model="form.professor" :options="professors" optionLabel="name" optionValue="_id" placeholder="استاد را انتخاب کنید" class="w-full text-sm" filter />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('sessions.date') }} *</label>
|
||||
<DatePicker v-model="form.day" class="w-full text-sm" placeholder="1403/01/01" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('courses.startTime') }} *</label>
|
||||
<InputText v-model="form.startTime" class="w-full text-sm" dir="ltr" placeholder="19:00" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('courses.endTime') }} *</label>
|
||||
<InputText v-model="form.endTime" class="w-full text-sm" dir="ltr" placeholder="20:30" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">وضعیت جلسه</label>
|
||||
<Dropdown v-model="form.status" :options="statusOptions" optionLabel="label" optionValue="value" class="w-full text-sm" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">مکان برگزاری</label>
|
||||
<InputText v-model="form.place" class="w-full text-sm" placeholder="کلاس / آنلاین / آدرس" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('sessions.topic') }}</label>
|
||||
<InputText v-model="form.topic" class="w-full text-sm" placeholder="عنوان یا موضوع این جلسه" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">یادداشت جلسه</label>
|
||||
<Textarea v-model="form.note" rows="3" class="w-full text-sm" placeholder="یادداشت داخلی درباره این جلسه…" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 flex justify-content-end gap-2 mt-4 pt-3 border-top-1 border-color">
|
||||
<Button label="انصراف" text severity="secondary" @click="$router.push('/sessions')" />
|
||||
<Button type="submit" :label="$t('app.save')" icon="pi pi-check" :loading="isSubmitting" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import moment from 'jalali-moment';
|
||||
import { sessionApi } from '@/api/sessionApi';
|
||||
import { courseApi } from '@/api/courseApi';
|
||||
import { classApi } from '@/api/classApi';
|
||||
import { professorApi } from '@/api/professorApi';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import Dropdown from 'primevue/select';
|
||||
import DatePicker from 'vue3-persian-datetime-picker';
|
||||
import Button from 'primevue/button';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { toLatinDigits } = usePersianDate();
|
||||
|
||||
const sessionId = route.params.id;
|
||||
const isEditMode = computed(() => !!sessionId);
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
const courses = ref([]);
|
||||
const classes = ref([]);
|
||||
const professors = ref([]);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: 'طبق برنامه', value: 'scheduled' },
|
||||
{ label: 'برگزارشده', value: 'held' },
|
||||
{ label: 'لغوشده', value: 'cancelled' }
|
||||
];
|
||||
|
||||
const form = reactive({
|
||||
course: null,
|
||||
class: null,
|
||||
professor: null,
|
||||
day: moment().locale('fa').format('jYYYY/jMM/jDD'),
|
||||
startTime: '19:00',
|
||||
endTime: '20:30',
|
||||
status: 'scheduled',
|
||||
topic: '',
|
||||
place: '',
|
||||
note: ''
|
||||
});
|
||||
|
||||
const toGregorianIso = (jalaliValue) => {
|
||||
if (!jalaliValue) return null;
|
||||
if (jalaliValue instanceof Date) return jalaliValue.toISOString();
|
||||
const latin = toLatinDigits(String(jalaliValue));
|
||||
const m = moment(latin, 'jYYYY/jMM/jDD');
|
||||
return m.isValid() ? m.toDate().toISOString() : null;
|
||||
};
|
||||
|
||||
const toJalaliDisplay = (value) => {
|
||||
if (!value) return '';
|
||||
return moment(value).locale('fa').format('jYYYY/jMM/jDD');
|
||||
};
|
||||
|
||||
const loadClasses = async (courseId) => {
|
||||
if (!courseId) {
|
||||
classes.value = [];
|
||||
return;
|
||||
}
|
||||
const res = await classApi.getAll({ limit: 100, courseId });
|
||||
const data = res.data || res;
|
||||
classes.value = Array.isArray(data) ? data : (data.items || data.data || []);
|
||||
};
|
||||
|
||||
const onCourseChange = async () => {
|
||||
form.class = null;
|
||||
await loadClasses(form.course);
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [cRes, pRes] = await Promise.all([
|
||||
courseApi.getAll({ limit: 100 }),
|
||||
professorApi.getAll({ limit: 100 })
|
||||
]);
|
||||
|
||||
const cData = cRes.data || cRes;
|
||||
courses.value = Array.isArray(cData) ? cData : (cData.items || cData.courses || cData.data || []);
|
||||
|
||||
const pData = pRes.data || pRes;
|
||||
professors.value = (Array.isArray(pData) ? pData : (pData.items || pData.professors || pData.data || [])).map((p) => ({
|
||||
...p,
|
||||
name: `${p.name || ''} ${p.surname || ''}`.trim()
|
||||
}));
|
||||
|
||||
if (sessionId) {
|
||||
const res = await sessionApi.getOne(sessionId);
|
||||
const data = res.data || res;
|
||||
const courseId = data.course?._id || data.course || null;
|
||||
await loadClasses(courseId);
|
||||
Object.assign(form, {
|
||||
course: courseId,
|
||||
class: data.class?._id || data.class || null,
|
||||
professor: data.professor?._id || data.professor || null,
|
||||
day: toJalaliDisplay(data.day || data.date),
|
||||
startTime: data.startTime || '19:00',
|
||||
endTime: data.endTime || '20:30',
|
||||
status: data.status || 'scheduled',
|
||||
topic: data.topic || '',
|
||||
place: data.place || '',
|
||||
note: data.note || ''
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.course) { showError('لطفا دوره را انتخاب کنید'); return; }
|
||||
if (!form.class) { showError('لطفا کلاس را انتخاب کنید'); return; }
|
||||
if (!form.professor) { showError('لطفا استاد را انتخاب کنید'); return; }
|
||||
if (!form.day) { showError('لطفا تاریخ جلسه را وارد کنید'); return; }
|
||||
if (!form.startTime || !form.endTime) { showError('ساعت شروع و پایان الزامی است'); return; }
|
||||
|
||||
const dayIso = toGregorianIso(form.day);
|
||||
if (!dayIso) { showError('تاریخ جلسه نامعتبر است'); return; }
|
||||
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
course: form.course,
|
||||
class: form.class,
|
||||
professor: form.professor,
|
||||
day: dayIso,
|
||||
startTime: form.startTime,
|
||||
endTime: form.endTime,
|
||||
status: form.status,
|
||||
topic: form.topic,
|
||||
place: form.place,
|
||||
note: form.note
|
||||
};
|
||||
|
||||
if (isEditMode.value) {
|
||||
await sessionApi.update(sessionId, payload);
|
||||
showSuccess('جلسه با موفقیت ویرایش شد');
|
||||
} else {
|
||||
await sessionApi.create(payload);
|
||||
showSuccess('جلسه جدید با موفقیت ایجاد شد');
|
||||
}
|
||||
router.push('/sessions');
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(fetchData);
|
||||
</script>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user