diff --git a/apps/web/src/components/DataAccessEditor.vue b/apps/web/src/components/DataAccessEditor.vue index d5f2208..8cefb8e 100644 --- a/apps/web/src/components/DataAccessEditor.vue +++ b/apps/web/src/components/DataAccessEditor.vue @@ -75,10 +75,10 @@ const ownerSelfLabel = computed(() => { props.section === 'routines' || props.section === 'tickets' ) { - return 'فقط مسئول خودش'; + return 'مسئول خودش'; } - if (props.section === 'notes') return 'فقط نویسنده خودش'; - return 'فقط پرونده‌های خود'; + if (props.section === 'notes') return 'نویسنده خودش'; + return 'پرونده‌های خود'; }); const noteTypeOptions = [ @@ -106,6 +106,61 @@ const typesLegend = computed(() => props.section === 'routines' ? 'دسته روتین' : 'نوع فعالیت', ); +const boardsLegend = computed(() => + props.section === 'projects' ? 'بورد پروژه' : 'بورد فعالیت', +); + +/** چند قانون قدیمی → یک فیلتر (اتحاد گزینه‌ها) برای ویرایش ساده */ +function collapseRules(rules: DataAccessRule[]): DataAccessRule { + if (!rules.length) return {}; + if (rules.length === 1) return { ...rules[0]! }; + const owners = new Set(); + const groupIds = new Set(); + const sources = new Set(); + const moods = new Set(); + const stageIds = new Set(); + const pipelineIds = new Set(); + const boardIds = new Set(); + const typeIds = new Set(); + for (const r of rules) { + for (const o of r.owners ?? []) owners.add(o); + for (const id of r.groupIds ?? []) groupIds.add(id); + for (const s of r.sources ?? []) sources.add(s); + for (const m of r.moods ?? []) moods.add(m); + for (const id of r.stageIds ?? []) stageIds.add(id); + for (const id of r.pipelineIds ?? []) pipelineIds.add(id); + for (const id of r.boardIds ?? []) boardIds.add(id); + for (const id of r.typeIds ?? []) typeIds.add(id); + } + const out: DataAccessRule = {}; + if (owners.size) out.owners = [...owners]; + if (groupIds.size) out.groupIds = [...groupIds]; + if (sources.size) out.sources = [...sources]; + if (moods.size) out.moods = [...moods]; + if (stageIds.size) out.stageIds = [...stageIds]; + if (pipelineIds.size) out.pipelineIds = [...pipelineIds]; + if (boardIds.size) out.boardIds = [...boardIds]; + if (typeIds.size) out.typeIds = [...typeIds]; + return out; +} + +function ruleHasAnyFilter(rule: DataAccessRule): boolean { + return Boolean( + rule.owners?.length || + rule.groupIds?.length || + rule.sources?.length || + rule.moods?.length || + rule.stageIds?.length || + rule.pipelineIds?.length || + rule.boardIds?.length || + rule.typeIds?.length, + ); +} + +const hadMultipleRules = computed( + () => (props.modelValue?.rules?.length ?? 0) > 1, +); + const mode = computed({ get: () => (props.modelValue?.mode === 'rules' ? 'rules' : 'all'), set: (next: 'all' | 'rules') => { @@ -113,30 +168,39 @@ const mode = computed({ emit('update:modelValue', { mode: 'all', rules: [] }); return; } - const rules = props.modelValue?.rules?.length - ? props.modelValue.rules - : [emptyRule()]; - emit('update:modelValue', { mode: 'rules', rules }); + const collapsed = collapseRules(props.modelValue?.rules ?? []); + emit('update:modelValue', { + mode: 'rules', + rules: [ruleHasAnyFilter(collapsed) ? collapsed : {}], + }); }, }); -const rules = computed(() => props.modelValue?.rules ?? []); +const filter = computed(() => collapseRules(props.modelValue?.rules ?? [])); -function emptyRule(): DataAccessRule { - return {}; +function emitFilter(next: DataAccessRule) { + const cleaned: DataAccessRule = {}; + if (next.owners?.length) cleaned.owners = [...next.owners]; + if (next.groupIds?.length) cleaned.groupIds = [...next.groupIds]; + if (next.sources?.length) cleaned.sources = [...next.sources]; + if (next.moods?.length) cleaned.moods = [...next.moods]; + if (next.stageIds?.length) cleaned.stageIds = [...next.stageIds]; + if (next.pipelineIds?.length) cleaned.pipelineIds = [...next.pipelineIds]; + if (next.boardIds?.length) cleaned.boardIds = [...next.boardIds]; + if (next.typeIds?.length) cleaned.typeIds = [...next.typeIds]; + emit('update:modelValue', { mode: 'rules', rules: [cleaned] }); } -function patchRule(index: number, patch: Partial) { - const next = rules.value.map((r, i) => (i === index ? { ...r, ...patch } : { ...r })); - emit('update:modelValue', { mode: 'rules', rules: next }); +function patchFilter(patch: Partial) { + emitFilter({ ...filter.value, ...patch }); } -function toggleOwner(index: number, kind: DataAccessOwnerKind) { - const current = new Set(rules.value[index]?.owners ?? []); +function toggleOwner(kind: DataAccessOwnerKind) { + const current = new Set(filter.value.owners ?? []); if (current.has(kind)) current.delete(kind); else current.add(kind); const owners = [...current] as DataAccessOwnerKind[]; - patchRule(index, { owners: owners.length ? owners : undefined }); + patchFilter({ owners: owners.length ? owners : undefined }); } type ListKey = @@ -148,99 +212,12 @@ type ListKey = | 'boardIds' | 'typeIds'; -function toggleInList(index: number, key: ListKey, value: string) { - const rule = rules.value[index] ?? {}; - const current = new Set((rule[key] as string[] | undefined) ?? []); +function toggleInList(key: ListKey, value: string) { + const current = new Set((filter.value[key] as string[] | undefined) ?? []); if (current.has(value)) current.delete(value); else current.add(value); const list = [...current]; - patchRule(index, { [key]: list.length ? list : undefined }); -} - -function addRule() { - emit('update:modelValue', { - mode: 'rules', - rules: [...rules.value, emptyRule()], - }); -} - -function removeRule(index: number) { - const next = rules.value.filter((_, i) => i !== index); - emit('update:modelValue', { - mode: next.length ? 'rules' : 'all', - rules: next, - }); -} - -function nameOf(list: IdOption[], id: string) { - return list.find((x) => x.id === id)?.name ?? id; -} - -function ruleSummary(rule: DataAccessRule): string { - const parts: string[] = []; - if (rule.owners?.includes('self')) parts.push(ownerSelfLabel.value.replace('فقط ', '')); - if (rule.owners?.includes('team')) parts.push('تیم'); - if (rule.groupIds?.length) { - parts.push( - `گروه: ${rule.groupIds - .slice(0, 2) - .map((id) => nameOf(props.groups, id)) - .join('، ')}${rule.groupIds.length > 2 ? '…' : ''}`, - ); - } - if (rule.sources?.length) { - const labelMap: Record = { - ...(PERSON_IMPORT_SOURCE_LABELS as Record), - ...Object.fromEntries(noteTypeOptions.map((o) => [o.value, o.label])), - ...Object.fromEntries(ticketStatusOptions.map((o) => [o.value, o.label])), - }; - const labels = rule.sources.map((s) => labelMap[s] ?? s).slice(0, 2); - const prefix = - props.section === 'notes' - ? 'نوع' - : props.section === 'tickets' - ? 'وضعیت' - : 'منبع'; - parts.push(`${prefix}: ${labels.join('، ')}${rule.sources.length > 2 ? '…' : ''}`); - } - if (rule.moods?.length) { - parts.push( - `حال: ${rule.moods.map((m) => PERSON_MOOD_LABELS[m as PersonMood] ?? m).join('، ')}`, - ); - } - if (rule.pipelineIds?.length) { - parts.push( - `پایپ‌لاین: ${rule.pipelineIds - .slice(0, 2) - .map((id) => nameOf(props.pipelines, id)) - .join('، ')}`, - ); - } - if (rule.stageIds?.length) { - parts.push( - `مرحله: ${rule.stageIds - .slice(0, 2) - .map((id) => nameOf(props.stages, id)) - .join('، ')}`, - ); - } - if (rule.boardIds?.length) { - parts.push( - `بورد: ${rule.boardIds - .slice(0, 2) - .map((id) => nameOf(props.boards, id)) - .join('، ')}`, - ); - } - if (rule.typeIds?.length) { - parts.push( - `نوع: ${rule.typeIds - .slice(0, 2) - .map((id) => nameOf(props.activityTypes, id)) - .join('، ')}`, - ); - } - return parts.length ? parts.join(' · ') : 'قانون خالی (اثر ندارد)'; + patchFilter({ [key]: list.length ? list : undefined }); } const sourceOptions = computed(() => @@ -260,12 +237,27 @@ watch( }, ); -function addCustomSource(index: number) { +function addCustomSource() { const v = customSource.value.trim(); if (!v) return; - toggleInList(index, 'sources', v); + toggleInList('sources', v); customSource.value = ''; } + +const filterSummary = computed(() => { + const rule = filter.value; + const parts: string[] = []; + if (rule.owners?.includes('self')) parts.push(ownerSelfLabel.value); + if (rule.owners?.includes('team')) parts.push('تیم‌های عضو'); + if (rule.groupIds?.length) parts.push(`${rule.groupIds.length} گروه`); + if (rule.sources?.length) parts.push(`${rule.sources.length} مورد منبع/نوع`); + if (rule.moods?.length) parts.push(`${rule.moods.length} حال`); + if (rule.pipelineIds?.length) parts.push(`${rule.pipelineIds.length} پایپ‌لاین`); + if (rule.stageIds?.length) parts.push(`${rule.stageIds.length} مرحله`); + if (rule.boardIds?.length) parts.push(`${rule.boardIds.length} بورد`); + if (rule.typeIds?.length) parts.push(`${rule.typeIds.length} نوع/دسته`); + return parts.length ? parts.join(' · ') : 'هنوز فیلتری انتخاب نشده'; +}); @@ -508,25 +486,18 @@ function addCustomSource(index: number) { margin-bottom: 0.25rem; } .hint, -.muted { +.muted, +.summary { color: #64748b; font-size: 0.82rem; margin: 0; } -.dae-rule { - border: 1px solid #e2e8f0; - border-radius: 10px; - padding: 0.75rem; - display: grid; - gap: 0.65rem; - background: #f8fafc; +.hint.warn { + color: #b45309; } -.dae-rule header { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - align-items: center; - justify-content: space-between; +.summary { + font-weight: 600; + color: #334155; } fieldset { border: 1px dashed #e2e8f0; @@ -552,7 +523,7 @@ legend { border-radius: 8px; padding: 0.35rem 0.5rem; } -.compact .dae-rule { - padding: 0.55rem; +.compact fieldset { + padding: 0.45rem 0.55rem; } diff --git a/apps/web/src/components/DataAccessPolicyPanel.vue b/apps/web/src/components/DataAccessPolicyPanel.vue index 92fb2b1..7db9d3d 100644 --- a/apps/web/src/components/DataAccessPolicyPanel.vue +++ b/apps/web/src/components/DataAccessPolicyPanel.vue @@ -56,7 +56,7 @@ watch( { immediate: true }, ); -/** بدون parse سخت‌گیرانه تا قانون خالیِ در حال ویرایش پاک نشود */ +/** بدون parse سخت‌گیرانه تا فیلتر در حال ویرایش پاک نشود */ function sectionValue(key: DataAccessSection): SectionDataAccess { const raw = props.modelValue?.[key]; if (raw && typeof raw === 'object') { diff --git a/apps/web/src/components/TeamsSettingsPanel.vue b/apps/web/src/components/TeamsSettingsPanel.vue index c862a4c..793b8c5 100644 --- a/apps/web/src/components/TeamsSettingsPanel.vue +++ b/apps/web/src/components/TeamsSettingsPanel.vue @@ -497,10 +497,10 @@ onMounted(() => {
-

محدوده داده (مشتریان / لید / معامله / فعالیت)

+

محدوده داده

- مالک و مدیر همیشه همه را می‌بینند. پیش‌فرض از نقش می‌آید؛ برای کاربر خاص می‌توانید override - بگذارید. + مالک و مدیر همیشه همه را می‌بینند. پیش‌فرض از نقش می‌آید؛ برای کاربر خاص می‌توانید فیلتر + جدا بگذارید.

{

- زیرمجموعهٔ مجوز هر بخش: اگر تنظیم نشود همه را می‌بیند؛ قوانین با هم «یا» می‌شوند. + برای هر بخش می‌توانید همه را باز بگذارید یا با فیلتر (مالک، گروه، مرحله و…) محدود کنید.