Edit login suoerAdmin
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import type { CreatePlatformAdminRequest, PlatformAdminDto } from '@mento/shared';
|
||||
import { platformApi } from '@/lib/platform-api';
|
||||
import { formatJalaliNumeric } from '@/lib/jalali';
|
||||
import { usePlatformAuthStore } from '@/stores/platform-auth';
|
||||
|
||||
const platform = usePlatformAuthStore();
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const success = ref('');
|
||||
const items = ref<PlatformAdminDto[]>([]);
|
||||
const busyId = ref('');
|
||||
const showCreate = ref(false);
|
||||
const creating = ref(false);
|
||||
const resetId = ref<string | null>(null);
|
||||
const resetPassword = ref('');
|
||||
const resetting = ref(false);
|
||||
|
||||
const createForm = reactive<CreatePlatformAdminRequest>({
|
||||
fullName: '',
|
||||
mobile: '',
|
||||
password: '',
|
||||
email: '',
|
||||
});
|
||||
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||
return Array.isArray(msg) ? msg.join('، ') : (msg ?? fallback);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data } = await platformApi.get<PlatformAdminDto[]>('/platform/admins');
|
||||
items.value = data;
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'بارگذاری ادمینها ناموفق بود');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetCreateForm() {
|
||||
createForm.fullName = '';
|
||||
createForm.mobile = '';
|
||||
createForm.password = '';
|
||||
createForm.email = '';
|
||||
}
|
||||
|
||||
async function createAdmin() {
|
||||
creating.value = true;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
try {
|
||||
const body: CreatePlatformAdminRequest = {
|
||||
fullName: createForm.fullName.trim(),
|
||||
mobile: createForm.mobile.trim(),
|
||||
password: createForm.password,
|
||||
email: createForm.email?.trim() || undefined,
|
||||
};
|
||||
const { data } = await platformApi.post<PlatformAdminDto>('/platform/admins', body);
|
||||
items.value = [...items.value, data];
|
||||
showCreate.value = false;
|
||||
resetCreateForm();
|
||||
success.value = `ادمین «${data.fullName}» اضافه شد`;
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'افزودن ادمین ناموفق بود');
|
||||
} finally {
|
||||
creating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setActive(admin: PlatformAdminDto, isActive: boolean) {
|
||||
busyId.value = admin.id;
|
||||
error.value = '';
|
||||
try {
|
||||
const { data } = await platformApi.patch<PlatformAdminDto>(
|
||||
`/platform/admins/${admin.id}`,
|
||||
{ isActive },
|
||||
);
|
||||
items.value = items.value.map((row) => (row.id === admin.id ? data : row));
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'تغییر وضعیت ناموفق بود');
|
||||
} finally {
|
||||
busyId.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function startReset(id: string) {
|
||||
resetId.value = resetId.value === id ? null : id;
|
||||
resetPassword.value = '';
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
}
|
||||
|
||||
async function submitReset(id: string) {
|
||||
resetting.value = true;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
try {
|
||||
await platformApi.patch(`/platform/admins/${id}/password`, {
|
||||
password: resetPassword.value,
|
||||
});
|
||||
success.value = 'رمز عبور بهروز شد';
|
||||
resetId.value = null;
|
||||
resetPassword.value = '';
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ریست رمز ناموفق بود');
|
||||
} finally {
|
||||
resetting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page">
|
||||
<header class="head">
|
||||
<div>
|
||||
<h1>سوپرادمینها</h1>
|
||||
<p>افزودن ادمین با موبایل و رمز ثابت، فعال/غیرفعال و ریست رمز.</p>
|
||||
</div>
|
||||
<button class="btn" type="button" @click="showCreate = !showCreate">
|
||||
{{ showCreate ? 'بستن فرم' : 'ادمین جدید' }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="success" class="ok">{{ success }}</p>
|
||||
|
||||
<form v-if="showCreate" class="panel" @submit.prevent="createAdmin">
|
||||
<h2>افزودن سوپرادمین</h2>
|
||||
<div class="grid">
|
||||
<label>
|
||||
<span>نام</span>
|
||||
<input v-model="createForm.fullName" required minlength="2" />
|
||||
</label>
|
||||
<label>
|
||||
<span>موبایل</span>
|
||||
<input v-model="createForm.mobile" type="tel" required dir="ltr" placeholder="09xxxxxxxxx" />
|
||||
</label>
|
||||
<label>
|
||||
<span>رمز عبور</span>
|
||||
<input v-model="createForm.password" type="password" required minlength="8" />
|
||||
</label>
|
||||
<label>
|
||||
<span>ایمیل (اختیاری)</span>
|
||||
<input v-model="createForm.email" type="email" dir="ltr" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<button class="btn" type="submit" :disabled="creating">
|
||||
{{ creating ? 'در حال ذخیره…' : 'افزودن' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p v-if="loading" class="muted">در حال بارگذاری…</p>
|
||||
<p v-else-if="!items.length" class="muted">ادمینی ثبت نشده.</p>
|
||||
|
||||
<div v-else class="list">
|
||||
<article v-for="admin in items" :key="admin.id" class="row">
|
||||
<div class="row-main">
|
||||
<div>
|
||||
<strong>
|
||||
{{ admin.fullName }}
|
||||
<em v-if="admin.id === platform.admin?.id" class="you">شما</em>
|
||||
</strong>
|
||||
<span>
|
||||
{{ admin.mobile || '—' }}
|
||||
<template v-if="admin.email"> · {{ admin.email }}</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<em :class="admin.isActive ? 'on' : 'off'">
|
||||
{{ admin.isActive ? 'فعال' : 'غیرفعال' }}
|
||||
</em>
|
||||
<small v-if="admin.lastLoginAt">
|
||||
آخرین ورود {{ formatJalaliNumeric(new Date(admin.lastLoginAt)) }}
|
||||
</small>
|
||||
<small v-else>هنوز وارد نشده</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-ghost" type="button" @click="startReset(admin.id)">
|
||||
ریست رمز
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
type="button"
|
||||
:disabled="busyId === admin.id || admin.id === platform.admin?.id"
|
||||
@click="setActive(admin, !admin.isActive)"
|
||||
>
|
||||
{{ admin.isActive ? 'غیرفعال' : 'فعال' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form
|
||||
v-if="resetId === admin.id"
|
||||
class="reset-panel"
|
||||
@submit.prevent="submitReset(admin.id)"
|
||||
>
|
||||
<label>
|
||||
<span>رمز جدید (حداقل ۸ کاراکتر)</span>
|
||||
<input
|
||||
v-model="resetPassword"
|
||||
type="password"
|
||||
required
|
||||
minlength="8"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
<div class="panel-actions">
|
||||
<button class="btn" type="submit" :disabled="resetting">ذخیره رمز</button>
|
||||
<button class="btn btn-ghost" type="button" @click="startReset(admin.id)">
|
||||
انصراف
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
max-width: 1040px;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.head h1 {
|
||||
margin: 0 0 0.3rem;
|
||||
}
|
||||
|
||||
.head p,
|
||||
.muted {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.ok {
|
||||
margin: 0;
|
||||
color: var(--success-deep);
|
||||
}
|
||||
|
||||
.panel,
|
||||
.reset-panel {
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 14px;
|
||||
padding: 1rem;
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
label span {
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.65rem 0.8rem;
|
||||
font: inherit;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
background: #fff;
|
||||
border: 1px solid #eef2f7;
|
||||
border-radius: 14px;
|
||||
padding: 0.85rem 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.7rem 1rem;
|
||||
}
|
||||
|
||||
.row-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.row-main span,
|
||||
.row-main small {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.you {
|
||||
margin-inline-start: 0.4rem;
|
||||
font-style: normal;
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent-deep);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.meta {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.on,
|
||||
.off {
|
||||
font-style: normal;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.on {
|
||||
color: var(--success-deep);
|
||||
}
|
||||
|
||||
.off {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.reset-panel {
|
||||
grid-column: 1 / -1;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -82,6 +82,13 @@ onMounted(() => {
|
||||
<span>نقش، دسترسی و ریست رمز عبور</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
<RouterLink :to="{ name: 'adm-admins' }" class="card">
|
||||
<UiIcon name="lock" :size="18" />
|
||||
<div>
|
||||
<strong>سوپرادمینها</strong>
|
||||
<span>افزودن ادمین با موبایل و رمز</span>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -156,7 +163,7 @@ onMounted(() => {
|
||||
|
||||
.shortcuts {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { brand } from '@mento/shared';
|
||||
const platform = usePlatformAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
const email = ref('');
|
||||
const mobile = ref('');
|
||||
const password = ref('');
|
||||
const showPassword = ref(false);
|
||||
const error = ref('');
|
||||
@@ -33,7 +33,7 @@ async function submit() {
|
||||
error.value = '';
|
||||
loading.value = true;
|
||||
try {
|
||||
await platform.login(email.value.trim(), password.value);
|
||||
await platform.login(mobile.value.trim(), password.value);
|
||||
await router.push({ name: 'adm-dashboard' });
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ورود ناموفق بود');
|
||||
@@ -64,19 +64,20 @@ async function submit() {
|
||||
|
||||
<form class="form" @submit.prevent="submit">
|
||||
<div class="field">
|
||||
<label for="adm-email">ایمیل</label>
|
||||
<label for="adm-mobile">شماره موبایل</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="adm-email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
id="adm-mobile"
|
||||
v-model="mobile"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="username"
|
||||
placeholder="ایمیل سوپرادمین را وارد کنید"
|
||||
placeholder="09xxxxxxxxx"
|
||||
class="ltr-value"
|
||||
dir="ltr"
|
||||
/>
|
||||
<span class="field-ico" aria-hidden="true">
|
||||
<UiIcon name="mail" :size="18" />
|
||||
<UiIcon name="phone" :size="18" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user