Files
crm/apps/web/src/views/TeamSettingsView.vue
T
s.esmaeili 1ae87c97b7 feat(ui): standardize design system, modals, sidebar, and form inputs
- Implement unified design system tokens, 10px radius, and IRANSans typography
- Modernize sidebar with Gemini-style collapsed expander, square 1:1 items, and elevated profile flyout
- Standardize all modal dialogs with 16px padding, fixed header/footer, 16px medium title, and sticky bottom actions
- Refactor DealFormView and CustomFieldsSection with standard UiField, UiInput, and UiSelect components
- Add draggable bottom-left QuickCreate floating action button
2026-09-12 01:33:07 +03:30

430 lines
13 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,
OrganizationDto,
OrgUserDto,
UpdateOrganizationRequest,
UserRole,
} from '@mento/shared';
import { USER_ROLE_LABELS, canManageSettings, canManageUsers } from '@/lib/roles';
import { BUSINESS_FIELDS, BUSINESS_FIELD_LABELS } from '@/lib/business-fields';
import {
UiBadge,
UiButton,
UiCard,
UiEmptyState,
UiField,
UiInput,
UiPageHeader,
UiSelect,
} from '@/components/ui';
import UiIcon from '@/components/UiIcon.vue';
const auth = useAuthStore();
const loading = ref(false);
const saving = ref(false);
const error = ref('');
const users = ref<OrgUserDto[]>([]);
const org = ref<OrganizationDto | null>(null);
const orgName = ref('');
const businessField = ref('');
const orgSaving = ref(false);
const orgOk = ref('');
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));
const canEditOrg = computed(() => canManageSettings(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'];
}
function getRoleBadgeVariant(r: UserRole) {
if (r === 'owner') return 'primary';
if (r === 'admin') return 'warning';
if (r === 'sales') return 'info';
return 'neutral';
}
async function load() {
loading.value = true;
error.value = '';
try {
const [usersRes, orgRes] = await Promise.all([
api.get<OrgUserDto[]>('/users'),
api.get<OrganizationDto>('/organizations/current'),
]);
users.value = usersRes.data;
org.value = orgRes.data;
orgName.value = orgRes.data.name;
businessField.value = orgRes.data.businessField ?? '';
} catch (e) {
if (axios.isAxiosError(e) && !e.response) {
error.value = 'اتصال به API برقرار نشد.';
} else {
error.value = 'بارگذاری کاربران ناموفق بود.';
}
} finally {
loading.value = false;
}
}
async function saveOrg() {
if (!canEditOrg.value) return;
if (!businessField.value) {
error.value = 'زمینه فعالیت را انتخاب کنید.';
return;
}
orgSaving.value = true;
error.value = '';
orgOk.value = '';
try {
const payload: UpdateOrganizationRequest = {
name: orgName.value.trim(),
businessField: businessField.value as UpdateOrganizationRequest['businessField'],
};
const { data } = await api.patch<OrganizationDto>('/organizations/current', payload);
org.value = data;
orgName.value = data.name;
businessField.value = data.businessField ?? '';
orgOk.value = 'مشخصات سازمان ذخیره شد.';
} 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 {
orgSaving.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 changeCommission(user: OrgUserDto, raw: string) {
if (!canInvite.value) return;
const next = Number(raw);
if (!Number.isFinite(next) || next === Number(user.commissionPercent ?? 0)) return;
error.value = '';
try {
await api.patch(`/users/${user.id}/commission`, { commissionPercent: next });
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 = 'ذخیره پورسانت ناموفق بود';
}
}
}
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="crm-page team-page" dir="rtl">
<UiPageHeader
title="تیم و نقش‌ها"
description="تنظیم زمینه فعالیت سازمان، مدیریت اعضا، نقش‌های کاربری و سقف پورسانت کارشناسان فروش"
/>
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
<div v-if="orgOk" class="crm-alert crm-alert-success">{{ orgOk }}</div>
<UiCard v-if="org" title="مشخصات سازمان" class="section-card">
<form class="crm-form" @submit.prevent="saveOrg">
<div class="crm-form-grid-2">
<UiField label="نام سازمان" required>
<UiInput
v-model="orgName"
:disabled="!canEditOrg"
required
minlength="2"
placeholder="مثال: شرکت مهندسی فردا"
/>
</UiField>
<UiField label="زمینه فعالیت" required>
<UiSelect v-model="businessField" :disabled="!canEditOrg" required>
<option value="" disabled>انتخاب کنید</option>
<option v-for="id in BUSINESS_FIELDS" :key="id" :value="id">
{{ BUSINESS_FIELD_LABELS[id] }}
</option>
</UiSelect>
</UiField>
</div>
<div v-if="canEditOrg" class="form-submit-row">
<UiButton type="submit" :loading="orgSaving">
ذخیره مشخصات سازمان
</UiButton>
</div>
</form>
</UiCard>
<UiCard v-if="canInvite" title="دعوت همکار جدید" class="section-card">
<form class="crm-form" @submit.prevent="invite">
<div class="crm-form-grid-2">
<UiField label="نام کامل" required>
<UiInput v-model="fullName" required maxlength="120" placeholder="نام و نام‌خانوادگی" />
</UiField>
<UiField label="ایمیل" required>
<UiInput v-model="email" type="email" required placeholder="user@company.com" dir="ltr" />
</UiField>
<UiField label="رمز عبور موقت" required hint="حداقل ۸ کاراکتر">
<UiInput v-model="password" type="password" required minlength="8" dir="ltr" />
</UiField>
<UiField label="نقش کاربری" required>
<UiSelect v-model="role">
<option v-for="opt in inviteRoles" :key="opt.value" :value="opt.value">
{{ opt.label }}
</option>
</UiSelect>
</UiField>
</div>
<div class="form-submit-row">
<UiButton type="submit" :loading="saving">
<template #icon>
<UiIcon name="user-plus" />
</template>
افزودن کاربر
</UiButton>
</div>
</form>
</UiCard>
<UiCard title="اعضای تیم و همکاران" class="section-card">
<UiEmptyState
v-if="!loading && !users.length"
title="کاربری ثبت نشده است"
description="هنوز هیچ کاربری در این سازمان تعریف نشده است."
icon="user"
/>
<div v-else class="crm-table-container">
<table class="crm-table">
<thead>
<tr>
<th>کاربر</th>
<th>ایمیل</th>
<th>نقش سازمانی</th>
<th>درصد پورسانت پیش‌فرض</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td class="user-cell">
<div class="user-avatar-dummy">{{ user.fullName.slice(0, 1) }}</div>
<strong class="user-name">{{ user.fullName }}</strong>
</td>
<td class="email-cell font-mono">{{ user.email }}</td>
<td>
<template v-if="canInvite">
<UiSelect
class="role-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>
</UiSelect>
</template>
<UiBadge v-else :variant="getRoleBadgeVariant(user.role)">
{{ USER_ROLE_LABELS[user.role] }}
</UiBadge>
</td>
<td>
<div class="commission-wrapper">
<input
type="number"
min="0"
max="100"
step="0.1"
class="commission-input"
:value="user.commissionPercent ?? 0"
:disabled="!canInvite"
@change="changeCommission(user, ($event.target as HTMLInputElement).value)"
/>
<span class="commission-suffix">٪</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</UiCard>
</section>
</template>
<style scoped>
.team-page {
display: flex;
flex-direction: column;
gap: var(--sp-6);
width: 100%;
max-width: 100%;
}
.section-card {
display: flex;
flex-direction: column;
gap: var(--sp-4);
}
.form-submit-row {
display: flex;
justify-content: flex-end;
margin-top: var(--sp-2);
}
.user-cell {
display: flex;
align-items: center;
gap: var(--sp-3);
}
.user-avatar-dummy {
width: 34px;
height: 34px;
border-radius: var(--radius-full);
background: var(--color-brand-100, #ccfbf1);
color: var(--color-brand-700, #0f766e);
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--fs-sm);
flex-shrink: 0;
}
.user-name {
color: var(--color-text-primary);
font-size: var(--fs-sm);
}
.email-cell {
direction: ltr;
text-align: right;
font-size: var(--fs-xs);
color: var(--color-text-secondary);
}
.font-mono {
font-family: var(--font-mono, monospace);
}
.role-select {
min-width: 8.5rem;
}
.commission-wrapper {
display: inline-flex;
align-items: center;
gap: var(--sp-1);
}
.commission-input {
width: 5rem;
height: var(--control-h-sm, 32px);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: 0 var(--sp-2);
background: var(--color-surface);
font-size: var(--fs-sm);
text-align: center;
font-family: var(--font-mono, monospace);
outline: none;
transition: border-color var(--transition-fast);
}
.commission-input:focus {
border-color: var(--color-brand-500);
}
.commission-input:disabled {
background: var(--color-surface-soft);
opacity: 0.7;
}
.commission-suffix {
font-size: var(--fs-sm);
color: var(--color-text-secondary);
}
</style>