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:
2026-08-15 23:48:18 +03:30
parent 7b083d5dbc
commit 3f220dda52
13 changed files with 427 additions and 17 deletions
+63 -2
View File
@@ -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>
+69 -1
View File
@@ -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>