From 9812e4e5ba5be9fecfd881b27de648075bb4c08b Mon Sep 17 00:00:00 2001 From: Mahdi Date: Tue, 27 Oct 2026 18:07:42 +0330 Subject: [PATCH] fix(crm): honor open data scopes and role assignees Unset sections on a saved role policy were still filtered as own-only, so full lead access showed nothing. Activities can now be assigned by org role, and the Tahesab sales report reads as a result instead of a tag wall. Co-authored-by: Cursor --- apps/api/src/common/data-access.ts | 5 + apps/api/test/record-scope.test.ts | 55 ++++ .../web/src/components/TeamsSettingsPanel.vue | 2 +- .../components/tahesab/SalesReportBoard.vue | 255 +++++++++++++----- apps/web/src/lib/tahesab.ts | 6 +- apps/web/src/views/ActivityFormView.vue | 225 ++++++++++++++-- apps/web/src/views/TahesabSalesReportView.vue | 102 ++++++- apps/web/src/views/TeamSettingsView.vue | 13 +- 8 files changed, 549 insertions(+), 114 deletions(-) diff --git a/apps/api/src/common/data-access.ts b/apps/api/src/common/data-access.ts index c848a93..d65b590 100644 --- a/apps/api/src/common/data-access.ts +++ b/apps/api/src/common/data-access.ts @@ -62,6 +62,11 @@ export function resolveSectionAccess( if (rolePolicy[sectionKey]) { return { section: rolePolicy[sectionKey]!, source: 'role' }; } + // فرم محدودهٔ نقش، بخشِ ننوشته را «بدون محدودیت» نشان می‌دهد. + // اگر نقش حتی یک بخش ذخیره کرده باشد، بقیه نباید به دید قدیمی own/team برگردند. + if (Object.keys(rolePolicy).length > 0) { + return { section: { mode: 'all', rules: [] }, source: 'role' }; + } if (input.legacyVisibility != null || input.legacyScopes) { return { section: migrateVisibilityToSectionAccess( diff --git a/apps/api/test/record-scope.test.ts b/apps/api/test/record-scope.test.ts index 1a5d535..822105c 100644 --- a/apps/api/test/record-scope.test.ts +++ b/apps/api/test/record-scope.test.ts @@ -1,5 +1,6 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; +import { leadsDeepWhere, resolveSectionAccess } from '../src/common/data-access'; import { noteAccessWhere, normalizeDataVisibility, @@ -67,3 +68,57 @@ describe('سطح دسترسی به پرونده‌ها', () => { assert.equal(normalizeDataVisibility('weird'), 'own'); }); }); + +describe('عمق داده وقتی نقش فقط بعضی بخش‌ها را ذخیره کرده', () => { + const legacyOwn = { + role: 'sales', + legacyVisibility: 'own' as const, + legacyScopes: {}, + }; + + it('بخش ننوشتهٔ نقشِ تنظیم‌شده بدون محدودیت است، نه فقط مسئول خودش', () => { + const leads = resolveSectionAccess('leads', { + ...legacyOwn, + rolePolicy: { customers: { mode: 'rules', rules: [] } }, + override: {}, + }); + assert.equal(leads.source, 'role'); + assert.deepEqual(leads.section, { mode: 'all', rules: [] }); + assert.deepEqual( + leadsDeepWhere({ userId: 'u1', role: 'sales', leadsAccess: leads.section, teamPeerIds: null }), + {}, + ); + }); + + it('نقش بدون سیاست جدید همچنان دید قدیمی را نگه می‌دارد', () => { + const leads = resolveSectionAccess('leads', { + ...legacyOwn, + rolePolicy: {}, + override: {}, + }); + assert.deepEqual(leads.section, { mode: 'rules', rules: [{ owners: ['self'] }] }); + assert.deepEqual( + leadsDeepWhere({ userId: 'u1', role: 'sales', leadsAccess: leads.section, teamPeerIds: null }), + { assigneeId: 'u1' }, + ); + }); + + it('override کاربر بر بخش باز نقش اولویت دارد', () => { + const leads = resolveSectionAccess('leads', { + ...legacyOwn, + rolePolicy: { customers: { mode: 'all', rules: [] } }, + override: { leads: { mode: 'rules', rules: [{ owners: ['self'] }] } }, + }); + assert.equal(leads.source, 'override'); + assert.deepEqual(leads.section, { mode: 'rules', rules: [{ owners: ['self'] }] }); + }); + + it('سفارشی کردن فقط مشتریان، لید را از دید قدیمی جدا نمی‌کند', () => { + const leads = resolveSectionAccess('leads', { + ...legacyOwn, + rolePolicy: {}, + override: { customers: { mode: 'all', rules: [] } }, + }); + assert.deepEqual(leads.section, { mode: 'rules', rules: [{ owners: ['self'] }] }); + }); +}); diff --git a/apps/web/src/components/TeamsSettingsPanel.vue b/apps/web/src/components/TeamsSettingsPanel.vue index 793b8c5..c6f35de 100644 --- a/apps/web/src/components/TeamsSettingsPanel.vue +++ b/apps/web/src/components/TeamsSettingsPanel.vue @@ -321,7 +321,7 @@ function openAccessEditor(u: OrgUserDto) { const hasOverride = DATA_ACCESS_SECTIONS.some((s) => Boolean(override[s])); accessMode.value = hasOverride ? 'custom' : 'inherit'; draftAccess.value = hasOverride - ? clonePolicy(override) + ? { ...clonePolicy(u.effectiveDataAccess), ...clonePolicy(override) } : clonePolicy(u.effectiveDataAccess); expandedAccess.value = u.id; } diff --git a/apps/web/src/components/tahesab/SalesReportBoard.vue b/apps/web/src/components/tahesab/SalesReportBoard.vue index 7caaff6..98a4f13 100644 --- a/apps/web/src/components/tahesab/SalesReportBoard.vue +++ b/apps/web/src/components/tahesab/SalesReportBoard.vue @@ -1,8 +1,23 @@