add password in admin

This commit is contained in:
2026-05-25 20:51:51 +03:30
parent 43f0524ae2
commit 7bc963b9e1
4 changed files with 79 additions and 5 deletions
+5
View File
@@ -109,6 +109,11 @@ export type UpdateUserPayload = {
isActive?: boolean; isActive?: boolean;
}; };
export type AdminSetUserPasswordPayload = {
password: string;
confirmPassword: string;
};
export type AssignOwnerPayload = { export type AssignOwnerPayload = {
mobile: string; mobile: string;
fullName?: string | null; fullName?: string | null;
+8 -1
View File
@@ -1,5 +1,5 @@
import { api } from './client'; import { api } from './client';
import type { UpdateUserPayload, User } from './types'; import type { AdminSetUserPasswordPayload, UpdateUserPayload, User } from './types';
export function fetchUsers() { export function fetchUsers() {
return api<User[]>('/users'); return api<User[]>('/users');
@@ -15,3 +15,10 @@ export function updateUser(id: string, payload: UpdateUserPayload) {
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
} }
export function setUserPassword(id: string, payload: AdminSetUserPasswordPayload) {
return api<User>(`/users/${id}/password`, {
method: 'PATCH',
body: JSON.stringify(payload),
});
}
@@ -130,9 +130,18 @@ async function load() {
} }
} }
function parsePrice(value: string | number): number | null {
if (value === '' || value == null) return null;
if (typeof value === 'number') {
return Number.isFinite(value) && value >= 0 ? value : null;
}
const n = Number.parseFloat(String(value).replace(/,/g, '').trim());
return Number.isFinite(n) && n >= 0 ? n : null;
}
async function onAdd() { async function onAdd() {
const price = Number.parseFloat(form.value.price.replace(/,/g, '')); const price = parsePrice(form.value.price);
if (!form.value.serviceTemplateId || !Number.isFinite(price) || price < 0) { if (!form.value.serviceTemplateId || price === null) {
error.value = 'نوع خدمت و قیمت معتبر را وارد کنید.'; error.value = 'نوع خدمت و قیمت معتبر را وارد کنید.';
return; return;
} }
+55 -2
View File
@@ -37,6 +37,9 @@
<AppBadge :tone="user.isActive ? 'success' : 'danger'"> <AppBadge :tone="user.isActive ? 'success' : 'danger'">
{{ activeLabel(user.isActive) }} {{ activeLabel(user.isActive) }}
</AppBadge> </AppBadge>
<AppBadge :tone="user.hasPassword ? 'default' : 'warning'">
{{ user.hasPassword ? 'رمز دارد' : 'بدون رمز' }}
</AppBadge>
</div> </div>
</div> </div>
<AppButton variant="ghost" class="!w-auto shrink-0 px-3" @click="openEdit(user)"> <AppButton variant="ghost" class="!w-auto shrink-0 px-3" @click="openEdit(user)">
@@ -64,6 +67,31 @@
حساب فعال است حساب فعال است
</label> </label>
<section class="space-y-3 rounded-xl border border-border bg-surface-3 p-3">
<h3 class="text-sm font-semibold text-text">
{{ editing.hasPassword ? 'تغییر رمز عبور' : 'تنظیم رمز عبور' }}
</h3>
<p class="text-xs text-muted">
اختیاری — در صورت پر کردن، رمز کاربر بدون نیاز به رمز قبلی تنظیم می‌شود.
</p>
<AppInput
id="userNewPassword"
v-model="form.password"
label="رمز جدید"
type="password"
autocomplete="new-password"
ltr
/>
<AppInput
id="userConfirmPassword"
v-model="form.confirmPassword"
label="تکرار رمز جدید"
type="password"
autocomplete="new-password"
ltr
/>
</section>
<AppButton type="submit" :loading="saving">ذخیره</AppButton> <AppButton type="submit" :loading="saving">ذخیره</AppButton>
</form> </form>
</AppModal> </AppModal>
@@ -72,7 +100,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { fetchUsers, updateUser } from '@/api/users'; import { fetchUsers, setUserPassword, updateUser } from '@/api/users';
import type { User, UserRole } from '@/api/types'; import type { User, UserRole } from '@/api/types';
import { ApiError } from '@/api/client'; import { ApiError } from '@/api/client';
import AppBadge from '@/components/ui/AppBadge.vue'; import AppBadge from '@/components/ui/AppBadge.vue';
@@ -106,6 +134,8 @@ const form = ref({
fullName: '', fullName: '',
role: 'customer' as UserRole, role: 'customer' as UserRole,
isActive: true, isActive: true,
password: '',
confirmPassword: '',
}); });
const roleOptions = [ const roleOptions = [
@@ -132,6 +162,8 @@ function openEdit(user: User) {
fullName: user.fullName ?? '', fullName: user.fullName ?? '',
role: user.role, role: user.role,
isActive: user.isActive, isActive: user.isActive,
password: '',
confirmPassword: '',
}; };
} }
@@ -141,14 +173,35 @@ function closeEdit() {
async function onSave() { async function onSave() {
if (!editing.value) return; if (!editing.value) return;
const wantsPassword = Boolean(
form.value.password.trim() || form.value.confirmPassword.trim(),
);
if (wantsPassword) {
if (form.value.password.length < 8) {
error.value = 'رمز باید حداقل ۸ کاراکتر باشد.';
return;
}
if (form.value.password !== form.value.confirmPassword) {
error.value = 'تکرار رمز با رمز جدید یکسان نیست.';
return;
}
}
saving.value = true; saving.value = true;
error.value = ''; error.value = '';
try { try {
const updated = await updateUser(editing.value.id, { let updated = await updateUser(editing.value.id, {
fullName: form.value.fullName.trim() || null, fullName: form.value.fullName.trim() || null,
role: form.value.role, role: form.value.role,
isActive: form.value.isActive, isActive: form.value.isActive,
}); });
if (wantsPassword) {
updated = await setUserPassword(editing.value.id, {
password: form.value.password,
confirmPassword: form.value.confirmPassword,
});
}
const idx = users.value.findIndex((u) => u.id === updated.id); const idx = users.value.findIndex((u) => u.id === updated.id);
if (idx >= 0) users.value[idx] = updated; if (idx >= 0) users.value[idx] = updated;
closeEdit(); closeEdit();