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
+203
View File
@@ -0,0 +1,203 @@
<!-- /src/views/profile/ProfileView.vue -->
<template>
<div class="profile-view w-full max-w-4xl mx-auto">
<PageHeader title="پروفایل من" subtitle="مشاهده اطلاعات حساب و تغییر رمز عبور" />
<div v-if="isLoading" class="surface-card p-5 border-round border-1 border-color shadow-sm">
<p class="text-muted text-sm m-0">در حال بارگذاری پروفایل</p>
</div>
<template v-else-if="profile">
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-4">
<div class="flex align-items-center gap-3 mb-4 pb-3 border-bottom-1 border-color">
<Avatar icon="pi pi-user" size="large" shape="circle" class="bg-primary text-white" />
<div>
<h2 class="text-lg font-bold text-color m-0">{{ profile.name || profile.username }}</h2>
<p class="text-muted text-sm m-0 mt-1" dir="ltr">{{ profile.username }}</p>
</div>
</div>
<div class="grid">
<div class="col-12 sm:col-6 md:col-4 mb-3">
<span class="text-muted text-xs block mb-1">نام</span>
<span class="font-semibold text-color text-sm">{{ profile.name || '—' }}</span>
</div>
<div class="col-12 sm:col-6 md:col-4 mb-3">
<span class="text-muted text-xs block mb-1">نام کاربری</span>
<span class="font-semibold text-color text-sm" dir="ltr">{{ profile.username || '—' }}</span>
</div>
<div class="col-12 sm:col-6 md:col-4 mb-3">
<span class="text-muted text-xs block mb-1">نقش</span>
<Tag :value="roleLabel" severity="info" />
</div>
<div class="col-12 sm:col-6 md:col-4 mb-3">
<span class="text-muted text-xs block mb-1">شماره همراه</span>
<span class="font-semibold text-color text-sm" dir="ltr">{{ profile.phoneNumber || '—' }}</span>
</div>
<div class="col-12 sm:col-6 md:col-4 mb-3">
<span class="text-muted text-xs block mb-1">ایمیل</span>
<span class="font-semibold text-color text-sm" dir="ltr">{{ profile.email || '—' }}</span>
</div>
<div class="col-12 sm:col-6 md:col-4 mb-3">
<span class="text-muted text-xs block mb-1">وضعیت حساب</span>
<StatusTag :status="profile.isActive !== false" />
</div>
</div>
</div>
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
<h2 class="text-lg font-bold text-color m-0 mb-1">تغییر رمز عبور</h2>
<p class="text-muted text-sm mt-1 mb-4">
برای تغییر رمز، ابتدا رمز فعلی را وارد کنید. پس از ذخیره، نشستهای دیگر از حساب خارج میشوند.
</p>
<form @submit.prevent="handleChangePassword" class="grid">
<div class="col-12 md:col-6 flex flex-column gap-2">
<label class="font-semibold text-sm" for="current-password">رمز عبور فعلی *</label>
<Password
inputId="current-password"
v-model="passwordForm.currentPassword"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full text-sm"
autocomplete="current-password"
/>
</div>
<div class="col-12 md:col-6"></div>
<div class="col-12 md:col-6 flex flex-column gap-2">
<label class="font-semibold text-sm" for="new-password">رمز عبور جدید *</label>
<Password
inputId="new-password"
v-model="passwordForm.newPassword"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full text-sm"
autocomplete="new-password"
/>
<span class="text-xs text-muted">حداقل ۶ نویسه</span>
</div>
<div class="col-12 md:col-6 flex flex-column gap-2">
<label class="font-semibold text-sm" for="confirm-password">تکرار رمز عبور جدید *</label>
<Password
inputId="confirm-password"
v-model="passwordForm.confirmPassword"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full text-sm"
autocomplete="new-password"
/>
</div>
<div class="col-12 flex justify-content-end gap-2 mt-3 pt-3 border-top-1 border-color">
<Button type="submit" label="تغییر رمز عبور" icon="pi pi-lock" :loading="isSubmitting" />
</div>
</form>
</div>
</template>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { authApi } from '@/api/authApi';
import { useAuthStore } from '@/stores/authStore';
import { useToast } from '@/composables/useToast';
import PageHeader from '@/components/common/PageHeader.vue';
import StatusTag from '@/components/common/StatusTag.vue';
import Avatar from 'primevue/avatar';
import Tag from 'primevue/tag';
import Password from 'primevue/password';
import Button from 'primevue/button';
const MIN_PASSWORD_LENGTH = 6;
const authStore = useAuthStore();
const { showSuccess, showError } = useToast();
const isLoading = ref(true);
const isSubmitting = ref(false);
const profile = ref(null);
const passwordForm = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
const roleLabel = computed(() => {
const role = profile.value?.role;
if (!role) return '—';
return typeof role === 'object' ? (role.name || '—') : String(role);
});
const resetPasswordForm = () => {
passwordForm.currentPassword = '';
passwordForm.newPassword = '';
passwordForm.confirmPassword = '';
};
const fetchProfile = async () => {
isLoading.value = true;
try {
const res = await authApi.getSelf();
profile.value = res.data || res;
if (profile.value) {
authStore.setUser({
...(authStore.user || {}),
...profile.value
});
}
} catch (err) {
showError(err);
} finally {
isLoading.value = false;
}
};
const handleChangePassword = async () => {
if (!passwordForm.currentPassword) {
showError('رمز عبور فعلی را وارد کنید');
return;
}
if (!passwordForm.newPassword || passwordForm.newPassword.length < MIN_PASSWORD_LENGTH) {
showError('رمز عبور جدید باید حداقل ۶ نویسه باشد');
return;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
showError('رمز عبور جدید و تکرار آن یکسان نیستند');
return;
}
if (passwordForm.newPassword === passwordForm.currentPassword) {
showError('رمز عبور جدید باید با رمز فعلی متفاوت باشد');
return;
}
isSubmitting.value = true;
try {
await authApi.changePassword({
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword,
refreshToken: authStore.refreshToken
});
resetPasswordForm();
showSuccess('رمز عبور با موفقیت تغییر کرد');
} catch (err) {
showError(err);
} finally {
isSubmitting.value = false;
}
};
onMounted(fetchProfile);
</script>
<style scoped>
:deep(.p-password) {
width: 100%;
}
:deep(.p-password-input) {
width: 100%;
}
</style>