Files
CRM_Montzar/apps/web/src/views/TeamSettingsView.vue
T

278 lines
6.7 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import axios from 'axios';
import { api } from '@/lib/api';
import { useAuthStore } from '@/stores/auth';
import type {
InviteUserRequest,
OrgUserDto,
UserRole,
} from '@mento/shared';
import { USER_ROLE_LABELS, canManageUsers } from '@/lib/roles';
const auth = useAuthStore();
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const users = ref<OrgUserDto[]>([]);
const email = ref('');
const fullName = ref('');
const password = ref('');
const role = ref<Exclude<UserRole, 'owner'>>('sales');
const inviteRoles: Array<{ value: Exclude<UserRole, 'owner'>; label: string }> = [
{ value: 'admin', label: USER_ROLE_LABELS.admin },
{ value: 'sales', label: USER_ROLE_LABELS.sales },
{ value: 'support', label: USER_ROLE_LABELS.support },
];
const isOwner = computed(() => auth.user?.role === 'owner');
const canInvite = computed(() => canManageUsers(auth.user?.role));
function roleOptionsFor(user: OrgUserDto): UserRole[] {
if (isOwner.value) return ['owner', 'admin', 'sales', 'support'];
if (user.role === 'owner') return ['owner'];
return ['admin', 'sales', 'support'];
}
async function load() {
loading.value = true;
error.value = '';
try {
users.value = (await api.get<OrgUserDto[]>('/users')).data;
} catch (e) {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری کاربران ناموفق بود.';
}
} finally {
loading.value = false;
}
}
async function invite() {
if (!email.value.trim() || !fullName.value.trim() || password.value.length < 8) {
error.value = 'نام، ایمیل و رمز حداقل ۸ کاراکتر لازم است.';
return;
}
saving.value = true;
error.value = '';
try {
const payload: InviteUserRequest = {
email: email.value.trim(),
fullName: fullName.value.trim(),
password: password.value,
role: role.value,
};
await api.post('/users', payload);
email.value = '';
fullName.value = '';
password.value = '';
role.value = 'sales';
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'دعوت کاربر ناموفق بود');
} else {
error.value = 'دعوت کاربر ناموفق بود';
}
} finally {
saving.value = false;
}
}
async function changeRole(user: OrgUserDto, next: string) {
const nextRole = next as UserRole;
if (nextRole === user.role) return;
error.value = '';
try {
await api.patch(`/users/${user.id}/role`, { role: nextRole });
await load();
} catch (e) {
if (axios.isAxiosError(e)) {
const msg = (e.response?.data as { message?: string | string[] })?.message;
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'تغییر نقش ناموفق بود');
} else {
error.value = 'تغییر نقش ناموفق بود';
}
}
}
onMounted(load);
</script>
<template>
<section class="page">
<header>
<p class="eyebrow">تنظیمات</p>
<h1>تیم و نقش‌ها</h1>
<p class="lead">
کاربران سازمان را اضافه کنید و نقش بدهید. تنظیمات سازمانی فقط برای مالک و مدیر است.
</p>
</header>
<p v-if="error" class="error">{{ error }}</p>
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
<form v-if="canInvite" class="invite" @submit.prevent="invite">
<h2>دعوت کاربر</h2>
<div class="grid">
<label>
نام کامل
<input v-model="fullName" required maxlength="120" />
</label>
<label>
ایمیل
<input v-model="email" type="email" required />
</label>
<label>
رمز عبور موقت
<input v-model="password" type="password" required minlength="8" />
</label>
<label>
نقش
<select v-model="role">
<option v-for="opt in inviteRoles" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</select>
</label>
</div>
<button class="btn" type="submit" :disabled="saving">
{{ saving ? 'در حال ثبت…' : 'افزودن کاربر' }}
</button>
</form>
<div class="list">
<article v-for="user in users" :key="user.id" class="row">
<div>
<strong>{{ user.fullName }}</strong>
<p class="muted">{{ user.email }}</p>
</div>
<div class="role-cell">
<template v-if="canInvite">
<select
:value="user.role"
:disabled="!isOwner && user.role === 'owner'"
@change="changeRole(user, ($event.target as HTMLSelectElement).value)"
>
<option
v-for="r in roleOptionsFor(user)"
:key="r"
:value="r"
>
{{ USER_ROLE_LABELS[r] }}
</option>
</select>
</template>
<span v-else class="role-pill">{{ USER_ROLE_LABELS[user.role] }}</span>
</div>
</article>
<p v-if="!loading && !users.length" class="muted">کاربری نیست.</p>
</div>
</section>
</template>
<style scoped>
.page {
display: grid;
gap: 1.2rem;
max-width: 820px;
}
.eyebrow {
margin: 0;
color: var(--accent);
font-weight: 700;
}
h1 {
margin: 0.2rem 0;
}
.lead,
.muted {
color: var(--muted);
}
.error {
color: #b42318;
}
.invite,
.row {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 1rem;
}
.invite {
display: grid;
gap: 0.85rem;
}
.invite 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;
font-size: 0.9rem;
color: var(--muted);
}
input,
select {
border: 1px solid var(--line);
border-radius: 12px;
padding: 0.7rem 0.85rem;
font: inherit;
background: #fff;
color: var(--text);
}
.list {
display: grid;
gap: 0.65rem;
}
.row {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.row p {
margin: 0.2rem 0 0;
}
.role-pill {
background: var(--accent-soft);
color: var(--accent);
border-radius: 999px;
padding: 0.25rem 0.7rem;
font-size: 0.85rem;
font-weight: 700;
}
@media (max-width: 700px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>