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:
@@ -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>
|
||||
Reference in New Issue
Block a user