From b00465439e9c4f2941b831fed3b89b0b75ba21a0 Mon Sep 17 00:00:00 2001
From: Mahdi
Date: Thu, 12 Nov 2026 19:50:02 +0330
Subject: [PATCH] Drop groupIds from customer data-access filters; keep
visibility via group assignees only.
Co-authored-by: Cursor
---
apps/api/src/common/data-access.ts | 26 ++++------
apps/web/src/components/DataAccessEditor.vue | 51 +++++++++++--------
.../src/components/DataAccessPolicyPanel.vue | 4 --
packages/shared/src/index.ts | 5 +-
4 files changed, 45 insertions(+), 41 deletions(-)
diff --git a/apps/api/src/common/data-access.ts b/apps/api/src/common/data-access.ts
index 6c10b18..475a96d 100644
--- a/apps/api/src/common/data-access.ts
+++ b/apps/api/src/common/data-access.ts
@@ -115,9 +115,7 @@ export function buildContactRuleWhere(
if (!ids.length) return null;
and.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } });
}
- if (rule.groupIds?.length) {
- and.push({ groupLinks: { some: { groupId: { in: rule.groupIds } } } });
- }
+ // groupIds دیگر فیلتر دسترسی نیست؛ دید گروه فقط از مسئولگروه (personViaGroupAssignee)
if (rule.sources?.length) {
and.push({
OR: [{ importSource: { in: rule.sources } }, { sourceRef: { in: rule.sources } }],
@@ -188,20 +186,13 @@ export function customersCompanyWhere(
if (!ids.length) continue;
and.push(ids.length === 1 ? { ownerId: ids[0]! } : { ownerId: { in: ids } });
}
- if (rule.groupIds?.length) {
- and.push({ groupLinks: { some: { groupId: { in: rule.groupIds } } } });
- }
+ // groupIds دیگر فیلتر دسترسی نیست؛ دید گروه فقط از مسئولگروه (companyViaGroupAssignee)
if (rule.sources?.length) {
and.push({
OR: [{ importSource: { in: rule.sources } }, { sourceRef: { in: rule.sources } }],
});
}
- if (
- rule.moods?.length &&
- !rule.owners?.length &&
- !rule.groupIds?.length &&
- !rule.sources?.length
- ) {
+ if (rule.moods?.length && !rule.owners?.length && !rule.sources?.length) {
continue;
}
if (!and.length) continue;
@@ -469,9 +460,14 @@ export function sectionOwnerIdUnion(
if (rule.owners?.length) {
sawOwnerRule = true;
for (const id of ownerIdList(rule, ctx)) ids.add(id);
- } else if (rule.groupIds?.length || rule.sources?.length || rule.moods?.length ||
- rule.stageIds?.length || rule.pipelineIds?.length || rule.boardIds?.length ||
- rule.typeIds?.length) {
+ } else if (
+ rule.sources?.length ||
+ rule.moods?.length ||
+ rule.stageIds?.length ||
+ rule.pipelineIds?.length ||
+ rule.boardIds?.length ||
+ rule.typeIds?.length
+ ) {
sawNonOwnerRule = true;
}
}
diff --git a/apps/web/src/components/DataAccessEditor.vue b/apps/web/src/components/DataAccessEditor.vue
index 73571f4..05c7198 100644
--- a/apps/web/src/components/DataAccessEditor.vue
+++ b/apps/web/src/components/DataAccessEditor.vue
@@ -22,7 +22,6 @@ const props = withDefaults(
defineProps<{
modelValue: SectionDataAccess;
section?: DataAccessSection;
- groups?: IdOption[];
stages?: IdOption[];
pipelines?: IdOption[];
boards?: IdOption[];
@@ -36,7 +35,6 @@ const props = withDefaults(
}>(),
{
section: 'customers',
- groups: () => [],
stages: () => [],
pipelines: () => [],
boards: () => [],
@@ -53,9 +51,9 @@ const emit = defineEmits<{
const sectionLabel = computed(() => DATA_ACCESS_SECTION_LABELS[props.section]);
-const showGroups = computed(() => props.section === 'customers');
const showMoods = computed(() => props.section === 'customers');
const showImportSources = computed(() => props.section === 'customers');
+const isCustomers = computed(() => props.section === 'customers');
const showLeadSources = computed(() => props.section === 'leads');
const showNoteTypes = computed(() => props.section === 'notes');
const showTicketStatuses = computed(() => props.section === 'tickets');
@@ -119,9 +117,8 @@ const boardsLegend = computed(() =>
/** چند قانون قدیمی → یک فیلتر (اتحاد گزینهها) برای ویرایش ساده */
function collapseRules(rules: DataAccessRule[]): DataAccessRule {
if (!rules.length) return {};
- if (rules.length === 1) return { ...rules[0]! };
+ if (rules.length === 1) return stripCustomerGroupIds({ ...rules[0]! });
const owners = new Set();
- const groupIds = new Set();
const sources = new Set();
const moods = new Set();
const stageIds = new Set();
@@ -130,7 +127,6 @@ function collapseRules(rules: DataAccessRule[]): DataAccessRule {
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);
@@ -140,7 +136,6 @@ function collapseRules(rules: DataAccessRule[]): DataAccessRule {
}
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];
@@ -150,10 +145,17 @@ function collapseRules(rules: DataAccessRule[]): DataAccessRule {
return out;
}
+/** دسترسی گروه فقط از مسئولگروه؛ groupIds دیگر در فیلتر مشتریان ذخیره/نمایش نمیشود */
+function stripCustomerGroupIds(rule: DataAccessRule): DataAccessRule {
+ if (!isCustomers.value || !rule.groupIds?.length) return rule;
+ const { groupIds: _drop, ...rest } = rule;
+ return rest;
+}
+
function ruleHasAnyFilter(rule: DataAccessRule): boolean {
return Boolean(
rule.owners?.length ||
- rule.groupIds?.length ||
+ (!isCustomers.value && rule.groupIds?.length) ||
rule.sources?.length ||
rule.moods?.length ||
rule.stageIds?.length ||
@@ -184,10 +186,26 @@ const mode = computed({
const filter = computed(() => collapseRules(props.modelValue?.rules ?? []));
+/** پاکسازی groupIds قدیمی مشتریان هنگام باز شدن ویرایشگر */
+watch(
+ () => [props.section, props.modelValue] as const,
+ () => {
+ if (!isCustomers.value || props.modelValue?.mode !== 'rules') return;
+ const rules = props.modelValue.rules ?? [];
+ if (!rules.some((r) => r.groupIds?.length)) return;
+ const collapsed = collapseRules(rules);
+ emit('update:modelValue', {
+ mode: 'rules',
+ rules: [ruleHasAnyFilter(collapsed) ? collapsed : {}],
+ });
+ },
+ { immediate: true },
+);
+
function emitFilter(next: DataAccessRule) {
const cleaned: DataAccessRule = {};
if (next.owners?.length) cleaned.owners = [...next.owners];
- if (next.groupIds?.length) cleaned.groupIds = [...next.groupIds];
+ if (!isCustomers.value && 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];
@@ -210,7 +228,6 @@ function toggleOwner(kind: DataAccessOwnerKind) {
}
type ListKey =
- | 'groupIds'
| 'sources'
| 'moods'
| 'stageIds'
@@ -293,16 +310,6 @@ const moodOptions: Choice[] = PERSON_MOODS.map((mood) => ({
const pickers = computed((): Picker[] => {
const rule = filter.value;
const items: Picker[] = [];
- if (showGroups.value) {
- items.push({
- key: 'groupIds',
- legend: 'گروهها',
- mode: 'multiple',
- placeholder: props.groups.length ? 'جستجو و انتخاب گروه…' : 'گروهی تعریف نشده',
- notFound: props.groups.length ? 'موردی پیدا نشد' : 'گروهی تعریف نشده',
- options: withSelected(namedOptions(props.groups), rule.groupIds),
- });
- }
if (showImportSources.value) {
const ready = props.importSourcesReady;
const failed = props.importSourcesFailed;
@@ -417,7 +424,6 @@ const filterSummary = computed(() => {
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} پایپلاین`);
@@ -446,6 +452,9 @@ const filterSummary = computed(() => {
فقط رکوردهایی دیده میشوند که با
همهٔ فیلترهای پرشده
جور باشند. در هر فهرست، چند انتخاب یعنی هر کدام کافی است. خالی یعنی بدون محدودیت.
+
+ اعضای گروههایی که این کاربر مسئولشان است جداگانه به همین محدوده اضافه میشوند (بدون تکرار).
+
تنظیم قبلی چندتکه بود؛ اینجا در یک فیلتر ادغام شده. با ذخیره به همین شکل ساده ثبت میشود.
diff --git a/apps/web/src/components/DataAccessPolicyPanel.vue b/apps/web/src/components/DataAccessPolicyPanel.vue
index bd1c0ac..831e8e8 100644
--- a/apps/web/src/components/DataAccessPolicyPanel.vue
+++ b/apps/web/src/components/DataAccessPolicyPanel.vue
@@ -16,7 +16,6 @@ import {
emptySectionDataAccess,
} from '@mento/shared';
import { api } from '@/lib/api';
-import { useCustomerGroups } from '@/lib/customer-groups';
import DataAccessEditor, { type IdOption } from '@/components/DataAccessEditor.vue';
const props = defineProps<{
@@ -85,7 +84,6 @@ function setSection(key: DataAccessSection, next: SectionDataAccess) {
});
}
-const { groups: customerGroups, load: loadCustomerGroups } = useCustomerGroups();
const importSources = ref>([]);
const importSourcesReady = ref(false);
const importSourcesFailed = ref(false);
@@ -142,7 +140,6 @@ async function loadTreasuryAccounts() {
}
async function loadOptions() {
- await loadCustomerGroups().catch(() => undefined);
try {
const { data } = await api.get('/people/source-stats');
importSources.value = data.map((row) => ({ value: row.source, label: row.label }));
@@ -362,7 +359,6 @@ const typesForActive = computed(() => {
:key="active"
:section="editorSection"
:model-value="sectionValue(editorSection)"
- :groups="customerGroups"
:stages="stagesForActive"
:pipelines="pipelines"
:boards="boardsForActive"
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index ff415c6..8250c94 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -653,7 +653,10 @@ export const DATA_ACCESS_LEGACY_SCOPE: Record