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
+8 -1
View File
@@ -3,6 +3,7 @@ import {
IsBoolean,
IsIn,
IsNumber,
IsObject,
IsOptional,
IsString,
Matches,
@@ -89,6 +90,12 @@ export class UpdateUserCommissionDto {
}
export class UpdateUserVisibilityDto {
@IsOptional()
@IsIn(['own', 'team', 'org'])
dataVisibility!: 'own' | 'team' | 'org';
dataVisibility?: 'own' | 'team' | 'org';
/** سطح دید هر بخش؛ null یعنی «مثل پیش‌فرض». در سرویس اعتبارسنجی می‌شود. */
@IsOptional()
@IsObject()
scopes?: Record<string, 'own' | 'team' | 'org' | null>;
}
+1 -1
View File
@@ -107,7 +107,7 @@ export class UsersController {
user.organizationId,
user.role,
id,
dto.dataVisibility,
dto,
);
}
+40 -4
View File
@@ -6,13 +6,16 @@ import {
NotFoundException,
} from '@nestjs/common';
import type {
DataScope,
DataVisibility,
InviteUserRequest,
OrgUserDto,
UpdateOrgUserRequest,
UpdateUserVisibilityRequest,
UserRole,
} from '@mento/shared';
import { DATA_VISIBILITIES } from '@mento/shared';
import { DATA_SCOPES, DATA_VISIBILITIES } from '@mento/shared';
import { parseVisibilityScopes } from '../common/record-scope';
import * as bcrypt from 'bcryptjs';
import { Prisma } from '@prisma/client';
import { normalizeMobile } from '../auth/auth.service';
@@ -40,6 +43,7 @@ type MembershipRow = {
isActive: boolean;
commissionPercent: { toString(): string } | number | null;
dataVisibility?: string | null;
dataVisibilityScopes?: unknown;
createdAt: Date;
orgRole: { id: string; name: string; color: string } | null;
user: {
@@ -241,14 +245,28 @@ export class UsersService {
organizationId: string,
actorRole: UserRole,
targetId: string,
dataVisibility: DataVisibility,
dto: UpdateUserVisibilityRequest,
): Promise<OrgUserDto> {
if (actorRole !== 'owner' && actorRole !== 'admin') {
throw new ForbiddenException('اجازه تغییر سطح دسترسی به پرونده‌ها را ندارید');
}
if (!DATA_VISIBILITIES.includes(dataVisibility)) {
const { dataVisibility, scopes } = dto;
if (dataVisibility === undefined && !scopes) {
throw new BadRequestException('تغییری ارسال نشده است');
}
if (dataVisibility !== undefined && !DATA_VISIBILITIES.includes(dataVisibility)) {
throw new BadRequestException('سطح دسترسی به پرونده‌ها نامعتبر است');
}
if (scopes) {
for (const [key, value] of Object.entries(scopes)) {
if (!DATA_SCOPES.includes(key as DataScope)) {
throw new BadRequestException(`بخش نامعتبر: ${key}`);
}
if (value !== null && !DATA_VISIBILITIES.includes(value as DataVisibility)) {
throw new BadRequestException('سطح دسترسی به پرونده‌ها نامعتبر است');
}
}
}
const target = await this.memberships.findUserInOrgById(
organizationId,
targetId,
@@ -258,9 +276,23 @@ export class UsersService {
'مالک و مدیر همیشه همه پرونده‌ها را می‌بینند',
);
}
const data: Prisma.MembershipUpdateInput = {};
if (dataVisibility !== undefined) data.dataVisibility = dataVisibility;
if (scopes) {
const current = await this.prisma.membership.findUnique({
where: { id: target.id },
select: { dataVisibilityScopes: true },
});
const merged = parseVisibilityScopes(current?.dataVisibilityScopes);
for (const [key, value] of Object.entries(scopes)) {
if (value === null) delete merged[key as DataScope];
else merged[key as DataScope] = value;
}
data.dataVisibilityScopes = merged;
}
await this.prisma.membership.update({
where: { id: target.id },
data: { dataVisibility },
data,
});
return this.dtoOf(organizationId, targetId);
}
@@ -442,6 +474,10 @@ export class UsersService {
: DATA_VISIBILITIES.includes(row.dataVisibility as DataVisibility)
? (row.dataVisibility as DataVisibility)
: 'own',
dataVisibilityScopes:
row.role === 'owner' || row.role === 'admin'
? {}
: parseVisibilityScopes(row.dataVisibilityScopes),
createdAt: row.createdAt.toISOString(),
};
}