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 */ dataVisibility?: DataVisibility; /** * شناسه کاربرانی که پرونده‌شان برای این Actor قابل‌مشاهده است. * اگر undefined باشد فقط خود کاربر؛ اگر null یعنی همه (elevated/org). */ visibleUserIds?: string[] | null; /** سطح دید تفکیکی هر بخش (از Membership.dataVisibilityScopes) */ scopes?: Partial>; /** off = فیلتر خاموش؛ current = کار جاری؛ archive = بایگانی اطلاعات قبل */ priorLens?: 'off' | 'current' | 'archive'; /** لحظهٔ روشن شدن ماژول برای این محیط؛ فعالیت‌های ساخته‌شده قبل از آن می‌توانند قبلی باشند */ priorCutoff?: Date | null; }; export type ScopedActor = Pick; /** Actor با سطح دید مخصوص یک بخش (اگر تنظیم شده باشد). */ export function forScope(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)[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, scope?: DataScope, ): boolean { if (elevated(actor.role)) 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; } /** شناسه کاربرانی که پرونده‌شان در این بخش دیده می‌شود؛ null یعنی همه. */ export function scopedOwnerIds( actor: ScopedActor, scope?: DataScope, ): string[] | null { 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))]; } /** فیلتر مالکیت؛ پیش‌فرض بخش «مشتریان» (Person / Company). برای فاکتور/پیش‌فاکتور scope=finance. */ export function ownerWhere( actor: ScopedActor, scope: DataScope = 'contacts', ): { ownerId: string } | { ownerId: { in: string[] } } | Record { const ids = scopedOwnerIds(actor, scope); if (!ids) return {}; if (ids.length === 1) return { ownerId: ids[0]! }; return { ownerId: { in: ids } }; } /** معامله: مالک اصلی یا یکی از مسئولان مشترک در محدوده دید */ export function dealAccessWhere( actor: ScopedActor, ): Prisma.DealWhereInput { const ids = scopedOwnerIds(actor, 'deals'); if (!ids) return {}; return { OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }; } /** کارت پروژه: مالک یا یکی از مسئولان در محدوده دید */ export function projectCardAccessWhere( actor: ScopedActor, ): Prisma.ProjectCardWhereInput { const ids = scopedOwnerIds(actor, 'projects'); if (!ids) return {}; return { OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }; } /** فعالیت‌ها بر اساس مسئول اصلی یا مسئولان مشترک */ export function assigneeWhere( actor: ScopedActor, ): Prisma.ActivityWhereInput { const ids = scopedOwnerIds(actor, 'activities'); if (!ids) return {}; return { OR: [ { assigneeId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }; } /** دسترسی به فعالیت: مسئولان یا شرکت‌کنندگان (فیلد شرکت‌کنندگان) در محدوده دید */ export function activityAccessWhere( actor: ScopedActor, ): Prisma.ActivityWhereInput { const ids = scopedOwnerIds(actor, 'activities'); if (!ids) return {}; return { OR: [ { assigneeId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, { participantIds: { hasSome: ids } }, ], }; } /** یادداشت: نویسنده یا پرونده‌های تحت مالکیت در محدوده دید + محرمانگی شخصی */ export function noteAccessWhere( actor: ScopedActor, ): Prisma.NoteWhereInput { const ids = scopedOwnerIds(actor, 'notes'); const personalGate: Prisma.NoteWhereInput = { OR: [ { visibility: { not: 'personal' } }, { visibility: 'personal', authorId: actor.userId }, // رکوردهای قدیمی بدون فیلد visibility در DB با default team می‌آیند ], }; if (!ids) return personalGate; return { AND: [ personalGate, { OR: [ { authorId: { in: ids } }, { deal: { OR: [ { ownerId: { in: ids } }, { assignees: { some: { id: { in: ids } } } }, ], }, }, { person: { ownerId: { in: ids } } }, { company: { ownerId: { in: ids } } }, ], }, ], }; } export function resolveOwnerId( actor: Pick, requested?: string | null, ): string { if (elevated(actor.role) && requested?.trim()) { return requested.trim(); } return actor.userId; } /** تیکت: مسئول یا ثبت‌کننده در محدوده دید */ export function ticketAccessWhere( actor: ScopedActor, ): Prisma.TicketWhereInput { const ids = scopedOwnerIds(actor, 'tickets'); if (!ids) return {}; return { OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }], }; } export function canReassignOwner(role: string | null | undefined): boolean { return elevated(role); } export type OrgRole = UserRole; export function normalizeDataVisibility( value: string | null | undefined, ): DataVisibility { if (value === 'team' || value === 'org' || value === 'own') return value; return 'own'; }