Improve team settings search, user sorting, and role badge layout.

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-12 14:01:08 +03:30
co-authored by Cursor
parent b583858088
commit 57e0fdd600
3 changed files with 235 additions and 80 deletions
+14 -5
View File
@@ -18,8 +18,10 @@ const props = withDefaults(
tableKey: string;
label?: string;
align?: 'start' | 'end';
/** فقط آیکون تنظیمات (بدون متن «ستون‌ها») */
iconOnly?: boolean;
}>(),
{ label: 'ستون‌ها', align: 'start' },
{ label: 'ستون‌ها', align: 'start', iconOnly: false },
);
const open = ref(false);
@@ -60,11 +62,18 @@ const placement = computed(() =>
</a-button>
</a-flex>
</template>
<a-button :type="hiddenCount > 0 ? 'primary' : 'default'" :ghost="hiddenCount > 0">
<a-button
:type="hiddenCount > 0 ? 'primary' : 'default'"
:ghost="hiddenCount > 0"
:title="iconOnly ? (hiddenCount ? `${label} · ${toFaDigits(hiddenCount)} مخفی` : label) : undefined"
:aria-label="label"
>
<template #icon><SettingOutlined /></template>
{{ label }}
<template v-if="hiddenCount">
· {{ toFaDigits(hiddenCount) }} مخفی
<template v-if="!iconOnly">
{{ label }}
<template v-if="hiddenCount">
· {{ toFaDigits(hiddenCount) }} مخفی
</template>
</template>
</a-button>
</a-popover>
+60 -4
View File
@@ -22,6 +22,7 @@ import {
import { UiButton, UiEmptyState, UiField, UiInput } from '@/components/ui';
import DataAccessPolicyPanel from '@/components/DataAccessPolicyPanel.vue';
import { formatLocalDateInput, parseLocalInput, toFaDigits } from '@/lib/jalali';
import { matchesSearch } from '@/lib/search';
const props = defineProps<{
users: OrgUserDto[];
@@ -74,6 +75,29 @@ const editableUsers = computed(() =>
props.users.filter((u) => u.isActive !== false && u.role !== 'owner' && u.role !== 'admin'),
);
const teamsQuery = ref('');
const accessQuery = ref('');
const filteredTeams = computed(() => {
const q = teamsQuery.value.trim();
if (!q) return teams.value;
return teams.value.filter((team) =>
matchesSearch(q, [
team.name,
team.description,
...team.members.map((m) => m.fullName),
]),
);
});
const filteredAccessUsers = computed(() => {
const q = accessQuery.value.trim();
if (!q) return editableUsers.value;
return editableUsers.value.filter((u) =>
matchesSearch(q, [u.fullName, u.mobile, u.email, u.orgRoleName, accessSummary(u)]),
);
});
async function setPriorArchive(userId: string, enabled: boolean) {
priorSavingId.value = userId;
priorPick.value = '';
@@ -383,14 +407,22 @@ onMounted(() => {
</section>
<section class="card">
<h3>تیم‌ها</h3>
<header class="card-head">
<h3>تیم‌ها</h3>
<UiInput
v-if="teams.length"
v-model="teamsQuery"
placeholder="جستجوی تیم یا عضو…"
/>
</header>
<UiEmptyState
v-if="!loading && !teams.length"
title="تیمی تعریف نشده"
description="تیم بسازید تا اعضای فروش پرونده‌های هم‌تیمی را ببینند."
/>
<p v-else-if="!filteredTeams.length" class="hint">تیمی با این جستجو پیدا نشد.</p>
<div v-else class="team-list">
<article v-for="t in teams" :key="t.id" class="team">
<article v-for="t in filteredTeams" :key="t.id" class="team">
<header>
<div>
<strong>{{ t.name }}</strong>
@@ -504,7 +536,14 @@ onMounted(() => {
</section>
<section class="card">
<h3>محدوده داده</h3>
<header class="card-head">
<h3>محدوده داده</h3>
<UiInput
v-if="editableUsers.length"
v-model="accessQuery"
placeholder="جستجوی کاربر…"
/>
</header>
<p class="hint">
مالک و مدیر همیشه همه را می‌بینند. پیش‌فرض از نقش می‌آید؛ برای کاربر خاص می‌توانید فیلتر
جدا بگذارید.
@@ -514,8 +553,9 @@ onMounted(() => {
title="کاربر قابل تنظیم نیست"
description="فقط نقش‌های فروش و پشتیبانی محدوده داده جدا دارند."
/>
<p v-else-if="!filteredAccessUsers.length" class="hint">کاربری با این جستجو پیدا نشد.</p>
<div v-else class="vis-table">
<div v-for="u in editableUsers" :key="u.id" class="vis-user">
<div v-for="u in filteredAccessUsers" :key="u.id" class="vis-user">
<div class="vis-row">
<span>
{{ u.fullName }}
@@ -567,6 +607,22 @@ onMounted(() => {
margin: 0 0 0.75rem;
font-size: 1rem;
}
.card-head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
margin-bottom: 0.75rem;
}
.card-head h3 {
margin: 0;
}
.card-head :deep(.field),
.card-head :deep(input) {
min-width: min(16rem, 100%);
max-width: 18rem;
}
.prior-head {
display: flex;
align-items: center;