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
This commit is contained in:
@@ -12,6 +12,17 @@ import type {
|
||||
} 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);
|
||||
@@ -45,6 +56,13 @@ function roleOptionsFor(user: OrgUserDto): UserRole[] {
|
||||
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 = '';
|
||||
@@ -170,274 +188,242 @@ onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<header class="page-head">
|
||||
<div class="head-copy">
|
||||
<h1>تیم و نقشها</h1>
|
||||
<p class="lead">
|
||||
زمینه فعالیت سازمان، نقشها و درصد پیشفرض پورسانت هر کارشناس را اینجا مدیریت کنید.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
<section class="crm-page team-page" dir="rtl">
|
||||
<UiPageHeader
|
||||
title="تیم و نقشها"
|
||||
description="تنظیم زمینه فعالیت سازمان، مدیریت اعضا، نقشهای کاربری و سقف پورسانت کارشناسان فروش"
|
||||
/>
|
||||
|
||||
<form v-if="org" class="panel" @submit.prevent="saveOrg">
|
||||
<h2>سازمان</h2>
|
||||
<div class="grid">
|
||||
<label>
|
||||
نام سازمان
|
||||
<input v-model="orgName" :disabled="!canEditOrg" required minlength="2" />
|
||||
</label>
|
||||
<label>
|
||||
زمینه فعالیت
|
||||
<select 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>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="orgOk" class="ok">{{ orgOk }}</p>
|
||||
<button v-if="canEditOrg" class="btn primary" type="submit" :disabled="orgSaving">
|
||||
{{ orgSaving ? 'در حال ذخیره…' : 'ذخیره مشخصات سازمان' }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="error" class="crm-alert crm-alert-danger">{{ error }}</div>
|
||||
<div v-if="orgOk" class="crm-alert crm-alert-success">{{ orgOk }}</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
||||
|
||||
<form v-if="canInvite" class="panel" @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 primary" 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">
|
||||
<label class="comm-field">
|
||||
<span>پورسانت %</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.1"
|
||||
:value="user.commissionPercent ?? 0"
|
||||
:disabled="!canInvite"
|
||||
@change="changeCommission(user, ($event.target as HTMLInputElement).value)"
|
||||
<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="مثال: شرکت مهندسی فردا"
|
||||
/>
|
||||
</label>
|
||||
<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] }}
|
||||
</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>
|
||||
</select>
|
||||
</template>
|
||||
<span v-else class="role-pill">{{ USER_ROLE_LABELS[user.role] }}</span>
|
||||
</UiSelect>
|
||||
</UiField>
|
||||
</div>
|
||||
</article>
|
||||
<p v-if="!loading && !users.length" class="muted">کاربری نیست.</p>
|
||||
</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>
|
||||
.page {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
max-width: 820px;
|
||||
min-width: 0;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
.team-page {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-6);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.head-copy {
|
||||
min-width: 0;
|
||||
.section-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.3rem;
|
||||
font-size: clamp(1.45rem, 2.5vw, 1.85rem);
|
||||
font-weight: 800;
|
||||
.form-submit-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: var(--sp-2);
|
||||
}
|
||||
|
||||
.lead,
|
||||
.muted {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
.user-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.ok {
|
||||
margin: 0;
|
||||
color: #0f766e;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.row {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.04);
|
||||
padding: 1.1rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
.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;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.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: #64748b;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: var(--radius);
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.85rem;
|
||||
font: inherit;
|
||||
background: #fff;
|
||||
color: #0f172a;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: var(--control-h);
|
||||
padding: 0 0.95rem;
|
||||
border-radius: var(--radius);
|
||||
font-weight: 700;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: var(--fs-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #14b8a6;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 18px rgba(20, 184, 166, 0.24);
|
||||
.user-name {
|
||||
color: var(--color-text-primary);
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
.email-cell {
|
||||
direction: ltr;
|
||||
text-align: right;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
.font-mono {
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.role-select {
|
||||
min-width: 8.5rem;
|
||||
}
|
||||
|
||||
.commission-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--sp-1);
|
||||
}
|
||||
|
||||
.row p {
|
||||
margin: 0.2rem 0 0;
|
||||
.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);
|
||||
}
|
||||
|
||||
.role-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
.commission-input:focus {
|
||||
border-color: var(--color-brand-500);
|
||||
}
|
||||
|
||||
.comm-field {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
.commission-input:disabled {
|
||||
background: var(--color-surface-soft);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.comm-field input {
|
||||
width: 4.8rem;
|
||||
padding: 0 0.45rem;
|
||||
}
|
||||
|
||||
.role-pill {
|
||||
background: #f0fdfa;
|
||||
color: #0f766e;
|
||||
border-radius: 999px;
|
||||
padding: 0.25rem 0.7rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.commission-suffix {
|
||||
font-size: var(--fs-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user