Add named teams and per-user data visibility (own/team/org) with settings UI and org module.

This commit is contained in:
2026-09-26 17:15:09 +03:30
parent 499d13d1df
commit 1b160c8cd3
17 changed files with 1097 additions and 28 deletions
+5
View File
@@ -67,3 +67,8 @@ export class UpdateUserCommissionDto {
@Max(100)
commissionPercent!: number;
}
export class UpdateUserVisibilityDto {
@IsIn(['own', 'team', 'org'])
dataVisibility!: 'own' | 'team' | 'org';
}
+16
View File
@@ -18,6 +18,7 @@ import {
UpdateUserCommissionDto,
UpdateUserRoleDto,
UpdateUserStatusDto,
UpdateUserVisibilityDto,
} from './dto';
import { UsersService } from './users.service';
@@ -79,6 +80,21 @@ export class UsersController {
);
}
@Patch(':id/visibility')
@Roles('owner', 'admin')
updateVisibility(
@CurrentUser() user: { organizationId: string; role: UserRole },
@Param('id') id: string,
@Body() dto: UpdateUserVisibilityDto,
) {
return this.usersService.updateVisibility(
user.organizationId,
user.role,
id,
dto.dataVisibility,
);
}
@Patch(':id/role')
@Roles('owner', 'admin')
updateRole(
+42 -1
View File
@@ -5,7 +5,13 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import type { InviteUserRequest, OrgUserDto, UserRole } from '@mento/shared';
import type {
DataVisibility,
InviteUserRequest,
OrgUserDto,
UserRole,
} from '@mento/shared';
import { DATA_VISIBILITIES } from '@mento/shared';
import * as bcrypt from 'bcryptjs';
import { Prisma } from '@prisma/client';
import { normalizeMobile } from '../auth/auth.service';
@@ -32,6 +38,7 @@ type MembershipRow = {
orgRoleId: string | null;
isActive: boolean;
commissionPercent: { toString(): string } | number | null;
dataVisibility?: string | null;
createdAt: Date;
orgRole: { id: string; name: string; color: string } | null;
user: {
@@ -155,6 +162,34 @@ export class UsersService {
return this.dtoOf(organizationId, targetId);
}
async updateVisibility(
organizationId: string,
actorRole: UserRole,
targetId: string,
dataVisibility: DataVisibility,
): Promise<OrgUserDto> {
if (actorRole !== 'owner' && actorRole !== 'admin') {
throw new ForbiddenException('اجازه تغییر سطح دید را ندارید');
}
if (!DATA_VISIBILITIES.includes(dataVisibility)) {
throw new BadRequestException('سطح دید نامعتبر است');
}
const target = await this.memberships.findUserInOrgById(
organizationId,
targetId,
);
if (target.role === 'owner' || target.role === 'admin') {
throw new BadRequestException(
'مالک و مدیر همیشه همه پرونده‌ها را می‌بینند',
);
}
await this.prisma.membership.update({
where: { id: target.id },
data: { dataVisibility },
});
return this.dtoOf(organizationId, targetId);
}
async updateRole(
organizationId: string,
actor: { userId: string; role: UserRole },
@@ -326,6 +361,12 @@ export class UsersService {
lastLoginAt: row.user.lastLoginAt ? row.user.lastLoginAt.toISOString() : null,
hasAvatar: Boolean(row.user.avatarStorageKey),
commissionPercent: Number(row.commissionPercent ?? 0),
dataVisibility:
row.role === 'owner' || row.role === 'admin'
? 'org'
: DATA_VISIBILITIES.includes(row.dataVisibility as DataVisibility)
? (row.dataVisibility as DataVisibility)
: 'own',
createdAt: row.createdAt.toISOString(),
};
}