Unified search algorithm across all lists; per-section record visibility (contacts, deals, activities, leads, finance, tickets, notes, projects)

This commit is contained in:
2026-10-06 15:04:46 +03:30
parent 43717392b2
commit e08f1452ef
38 changed files with 699 additions and 420 deletions
+59 -25
View File
@@ -1,48 +1,82 @@
import type { DataVisibility, UserRole } from '@mento/shared';
import { canViewAllRecords } from '@mento/shared';
import type { DataScope, DataVisibility, DataVisibilityScopes, UserRole } from '@mento/shared';
import { DATA_SCOPES, canViewAllRecords } from '@mento/shared';
import type { Prisma } from '@prisma/client';
export type ScopeVisibility = {
dataVisibility: DataVisibility;
visibleUserIds: string[] | null;
};
export type Actor = {
userId: string;
role: string;
organizationId: string;
/** own | team | org — از Membership */
/** own | team | org — سطح پیش‌فرض از Membership */
dataVisibility?: DataVisibility;
/**
* شناسه کاربرانی که پرونده‌شان برای این Actor قابل‌مشاهده است.
* اگر undefined باشد فقط خود کاربر؛ اگر null یعنی همه (elevated/org).
*/
visibleUserIds?: string[] | null;
/** سطح دید تفکیکی هر بخش (از Membership.dataVisibilityScopes) */
scopes?: Partial<Record<DataScope, ScopeVisibility>>;
};
export type ScopedActor = Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds' | 'scopes'>;
/** Actor با سطح دید مخصوص یک بخش (اگر تنظیم شده باشد). */
export function forScope<T extends ScopedActor>(actor: T, scope: DataScope): T {
const s = actor.scopes?.[scope];
if (!s) return actor;
return { ...actor, dataVisibility: s.dataVisibility, visibleUserIds: s.visibleUserIds };
}
export function parseVisibilityScopes(raw: unknown): DataVisibilityScopes {
const out: DataVisibilityScopes = {};
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return out;
for (const scope of DATA_SCOPES) {
const v = (raw as Record<string, unknown>)[scope];
if (v === 'own' || v === 'team' || v === 'org') out[scope] = v;
}
return out;
}
export function elevated(role: string | null | undefined): boolean {
return canViewAllRecords(role);
}
export function seesAllRecords(
actor: Pick<Actor, 'role' | 'dataVisibility' | 'visibleUserIds'>,
actor: Pick<Actor, 'role' | 'dataVisibility' | 'visibleUserIds' | 'scopes'>,
scope?: DataScope,
): boolean {
if (elevated(actor.role)) return true;
if (actor.dataVisibility === 'org') return true;
if (actor.visibleUserIds === null) return true;
const s = scope ? actor.scopes?.[scope] : undefined;
const visibility = s ? s.dataVisibility : actor.dataVisibility;
const userIds = s ? s.visibleUserIds : actor.visibleUserIds;
if (visibility === 'org') return true;
if (userIds === null) return true;
return false;
}
function scopedOwnerIds(
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
/** شناسه کاربرانی که پرونده‌شان در این بخش دیده می‌شود؛ null یعنی همه. */
export function scopedOwnerIds(
actor: ScopedActor,
scope?: DataScope,
): string[] | null {
if (seesAllRecords(actor)) return null;
const ids = actor.visibleUserIds?.length
? actor.visibleUserIds
: [actor.userId];
const a = scope ? forScope(actor, scope) : actor;
if (seesAllRecords(a)) return null;
const ids = a.visibleUserIds?.length
? a.visibleUserIds
: [a.userId];
return [...new Set(ids.filter(Boolean))];
}
/** فیلتر مالکیت برای Deal / Quote / Invoice / Person / Company */
/** فیلتر مالکیت؛ پیش‌فرض بخش «مشتریان» (Person / Company). برای فاکتور/پیش‌فاکتور scope=finance. */
export function ownerWhere(
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
actor: ScopedActor,
scope: DataScope = 'contacts',
): { ownerId: string } | { ownerId: { in: string[] } } | Record<string, never> {
const ids = scopedOwnerIds(actor);
const ids = scopedOwnerIds(actor, scope);
if (!ids) return {};
if (ids.length === 1) return { ownerId: ids[0]! };
return { ownerId: { in: ids } };
@@ -50,9 +84,9 @@ export function ownerWhere(
/** معامله: مالک اصلی یا یکی از مسئولان مشترک در محدوده دید */
export function dealAccessWhere(
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
actor: ScopedActor,
): Prisma.DealWhereInput {
const ids = scopedOwnerIds(actor);
const ids = scopedOwnerIds(actor, 'deals');
if (!ids) return {};
return {
OR: [
@@ -64,9 +98,9 @@ export function dealAccessWhere(
/** کارت پروژه: مالک یا یکی از مسئولان در محدوده دید */
export function projectCardAccessWhere(
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
actor: ScopedActor,
): Prisma.ProjectCardWhereInput {
const ids = scopedOwnerIds(actor);
const ids = scopedOwnerIds(actor, 'projects');
if (!ids) return {};
return {
OR: [
@@ -78,9 +112,9 @@ export function projectCardAccessWhere(
/** فعالیت‌ها بر اساس مسئول اصلی یا مسئولان مشترک */
export function assigneeWhere(
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
actor: ScopedActor,
): Prisma.ActivityWhereInput {
const ids = scopedOwnerIds(actor);
const ids = scopedOwnerIds(actor, 'activities');
if (!ids) return {};
return {
OR: [
@@ -92,9 +126,9 @@ export function assigneeWhere(
/** یادداشت: نویسنده یا پرونده‌های تحت مالکیت در محدوده دید + محرمانگی شخصی */
export function noteAccessWhere(
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
actor: ScopedActor,
): Prisma.NoteWhereInput {
const ids = scopedOwnerIds(actor);
const ids = scopedOwnerIds(actor, 'notes');
const personalGate: Prisma.NoteWhereInput = {
OR: [
{ visibility: { not: 'personal' } },
@@ -137,9 +171,9 @@ export function resolveOwnerId(
/** تیکت: مسئول یا ثبت‌کننده در محدوده دید */
export function ticketAccessWhere(
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
actor: ScopedActor,
): Prisma.TicketWhereInput {
const ids = scopedOwnerIds(actor);
const ids = scopedOwnerIds(actor, 'tickets');
if (!ids) return {};
return {
OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }],