Add session class filter, distinct partial payment tag, richer notifications list, class discount/frontend toggles, and remove unused course discount column.
288 lines
9.5 KiB
Vue
288 lines
9.5 KiB
Vue
<!-- /src/views/sessions/SessionListView.vue -->
|
|
<template>
|
|
<div class="session-list-view">
|
|
<PageHeader :title="$t('sessions.title')" :subtitle="$t('sessions.subtitle')">
|
|
<div class="flex flex-wrap gap-2 align-items-center">
|
|
<Dropdown
|
|
v-model="selectedClassId"
|
|
:options="classesList"
|
|
optionLabel="name"
|
|
optionValue="_id"
|
|
placeholder="فیلتر کلاس"
|
|
showClear
|
|
filter
|
|
class="w-16rem text-sm"
|
|
@change="onClassFilter"
|
|
/>
|
|
<PermissionGate permission="sessions:create">
|
|
<Button :label="$t('sessions.addSession')" icon="pi pi-plus" @click="$router.push('/sessions/create')" />
|
|
</PermissionGate>
|
|
</div>
|
|
</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 { 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 Select from 'primevue/select';
|
|
import Dropdown from 'primevue/select';
|
|
|
|
const { formatJalali, toPersianDigits } = usePersianDate();
|
|
const { showSuccess, showError } = useToast();
|
|
|
|
const classesList = ref([]);
|
|
const selectedClassId = ref(null);
|
|
|
|
const {
|
|
items,
|
|
totalCount,
|
|
isLoading,
|
|
queryParams,
|
|
loadData,
|
|
onPageChange,
|
|
onSort,
|
|
onSearch,
|
|
setFilter
|
|
} = 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;
|
|
}
|
|
};
|
|
|
|
const onClassFilter = () => setFilter('classId', selectedClassId.value || '');
|
|
|
|
const fetchClasses = async () => {
|
|
try {
|
|
const res = await classApi.getAll({ limit: 200, sortBy: 'name', sortOrder: 'asc' });
|
|
const data = res.data || res;
|
|
classesList.value = Array.isArray(data) ? data : (data.data || []);
|
|
} catch (err) {
|
|
console.warn('Failed to load classes for filter:', err);
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
fetchClasses();
|
|
loadData();
|
|
});
|
|
</script>
|