feat: add profile, class unenroll, notify toggles, and keep sidebar width
Give admins a profile password page, class removal, and SMS/email/bot checkboxes, and stop the sidebar from shrinking on edit pages.
This commit is contained in:
@@ -52,6 +52,22 @@
|
||||
<Column header="موبایل">
|
||||
<template #body="{ data }"><span dir="ltr">{{ data.phoneNumber || '—' }}</span></template>
|
||||
</Column>
|
||||
<Column v-if="canManageStudents" header="عملیات" style="width: 90px">
|
||||
<template #body="{ data }">
|
||||
<PermissionGate permission="classes:register_users">
|
||||
<Button
|
||||
icon="pi pi-user-minus"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
v-tooltip.top="'حذف از کلاس'"
|
||||
:loading="removingUserId === String(data._id || data.id)"
|
||||
@click="confirmRemoveStudent(data)"
|
||||
/>
|
||||
</PermissionGate>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
@@ -94,19 +110,30 @@
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
v-model="removeDialogVisible"
|
||||
title="حذف از کلاس"
|
||||
:message="removeDialogMessage"
|
||||
:loading="!!removingUserId"
|
||||
@confirm="handleRemoveStudent"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { classApi } from '@/api/classApi';
|
||||
import { sessionApi } from '@/api/sessionApi';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { usePermission } from '@/composables/usePermission';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import StatusTag from '@/components/common/StatusTag.vue';
|
||||
import TableSkeleton from '@/components/common/TableSkeleton.vue';
|
||||
import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
|
||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||
import Button from 'primevue/button';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
@@ -114,11 +141,23 @@ import Column from 'primevue/column';
|
||||
const route = useRoute();
|
||||
const classId = route.params.id;
|
||||
const { toPersianDigits, formatJalali } = usePersianDate();
|
||||
const { showError } = useToast();
|
||||
const { showError, showSuccess } = useToast();
|
||||
const { hasPermission } = usePermission();
|
||||
const canManageStudents = computed(() => hasPermission('classes:register_users'));
|
||||
|
||||
const classData = ref(null);
|
||||
const sessions = ref([]);
|
||||
const loadingSessions = ref(false);
|
||||
const removeDialogVisible = ref(false);
|
||||
const removingUserId = ref('');
|
||||
const studentToRemove = ref(null);
|
||||
|
||||
const removeDialogMessage = computed(() => {
|
||||
const name = studentToRemove.value?.name;
|
||||
return name
|
||||
? `آیا «${name}» از این کلاس حذف شود؟`
|
||||
: 'آیا این دانشجو از کلاس حذف شود؟';
|
||||
});
|
||||
|
||||
const fetchClass = async () => {
|
||||
try {
|
||||
@@ -159,4 +198,26 @@ onMounted(() => {
|
||||
fetchClass();
|
||||
fetchSessions();
|
||||
});
|
||||
|
||||
const confirmRemoveStudent = (student) => {
|
||||
studentToRemove.value = student;
|
||||
removeDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleRemoveStudent = async () => {
|
||||
const userId = studentToRemove.value?._id || studentToRemove.value?.id;
|
||||
if (!userId) return;
|
||||
removingUserId.value = String(userId);
|
||||
try {
|
||||
const res = await classApi.removeUser(classId, userId);
|
||||
classData.value = res.data || res;
|
||||
removeDialogVisible.value = false;
|
||||
studentToRemove.value = null;
|
||||
showSuccess('دانشجو از کلاس حذف شد');
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
removingUserId.value = '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -96,6 +96,10 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<NotifyChannelsField :notify="registerNotify" />
|
||||
</div>
|
||||
|
||||
<DataTable :value="students" class="p-datatable-sm text-sm" emptyMessage="هنوز دانشجویی ثبتنام نشده است">
|
||||
<Column header="#" style="width: 50px">
|
||||
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
||||
@@ -108,6 +112,22 @@
|
||||
<span dir="ltr">{{ data.phoneNumber || '—' }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column v-if="canManageStudents" header="عملیات" style="width: 90px">
|
||||
<template #body="{ data }">
|
||||
<PermissionGate permission="classes:register_users">
|
||||
<Button
|
||||
icon="pi pi-user-minus"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="danger"
|
||||
v-tooltip.top="'حذف از کلاس'"
|
||||
:loading="removingUserId === String(data._id || data.id)"
|
||||
@click="confirmRemoveStudent(data)"
|
||||
/>
|
||||
</PermissionGate>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +139,14 @@
|
||||
:default-session-count="defaultSessionCount"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
v-model="removeDialogVisible"
|
||||
title="حذف از کلاس"
|
||||
:message="removeDialogMessage"
|
||||
:loading="!!removingUserId"
|
||||
@confirm="handleRemoveStudent"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -132,9 +160,13 @@ import { professorApi } from '@/api/professorApi';
|
||||
import { userApi } from '@/api/userApi';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { usePermission } from '@/composables/usePermission';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import AdminNotesField from '@/components/common/AdminNotesField.vue';
|
||||
import ClassSessionsSection from '@/components/classes/ClassSessionsSection.vue';
|
||||
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
|
||||
import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
|
||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Dropdown from 'primevue/select';
|
||||
@@ -149,14 +181,20 @@ import DatePicker from 'vue3-persian-datetime-picker';
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { hasPermission } = usePermission();
|
||||
const { toPersianDigits, toLatinDigits } = usePersianDate();
|
||||
|
||||
const classId = route.params.id;
|
||||
const queryCourseId = route.query.courseId ? String(route.query.courseId) : null;
|
||||
const isEditMode = computed(() => !!classId);
|
||||
const canManageStudents = computed(() => hasPermission('classes:register_users'));
|
||||
const isSubmitting = ref(false);
|
||||
const registering = ref(false);
|
||||
const defaultSessionCount = ref(12);
|
||||
const registerNotify = reactive({ sms: true, email: true, bot: true });
|
||||
const removeDialogVisible = ref(false);
|
||||
const removingUserId = ref('');
|
||||
const studentToRemove = ref(null);
|
||||
|
||||
const courses = ref([]);
|
||||
const professors = ref([]);
|
||||
@@ -176,6 +214,13 @@ const form = reactive({
|
||||
adminNotes: []
|
||||
});
|
||||
|
||||
const removeDialogMessage = computed(() => {
|
||||
const name = studentToRemove.value?.name;
|
||||
return name
|
||||
? `آیا «${name}» از این کلاس حذف شود؟`
|
||||
: 'آیا این دانشجو از کلاس حذف شود؟';
|
||||
});
|
||||
|
||||
const availableUsers = computed(() => {
|
||||
const enrolled = new Set(students.value.map((s) => String(s._id)));
|
||||
return allUsers.value
|
||||
@@ -308,7 +353,7 @@ const registerSelected = async () => {
|
||||
if (!selectedUserIds.value.length) return;
|
||||
registering.value = true;
|
||||
try {
|
||||
const res = await classApi.registerUsers(classId, selectedUserIds.value);
|
||||
const res = await classApi.registerUsers(classId, selectedUserIds.value, { ...registerNotify });
|
||||
const data = res.data || res;
|
||||
students.value = data.students || [];
|
||||
selectedUserIds.value = [];
|
||||
@@ -320,5 +365,28 @@ const registerSelected = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmRemoveStudent = (student) => {
|
||||
studentToRemove.value = student;
|
||||
removeDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const handleRemoveStudent = async () => {
|
||||
const userId = studentToRemove.value?._id || studentToRemove.value?.id;
|
||||
if (!userId) return;
|
||||
removingUserId.value = String(userId);
|
||||
try {
|
||||
const res = await classApi.removeUser(classId, userId);
|
||||
const data = res.data || res;
|
||||
students.value = data.students || [];
|
||||
removeDialogVisible.value = false;
|
||||
studentToRemove.value = null;
|
||||
showSuccess('دانشجو از کلاس حذف شد');
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
removingUserId.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(fetchData);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user