245 lines
6.5 KiB
TypeScript
245 lines
6.5 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import type {
|
|
CreateTeamRequest,
|
|
SetTeamMembersRequest,
|
|
TeamDto,
|
|
TeamMemberDto,
|
|
TeamMemberRole,
|
|
UpdateTeamRequest,
|
|
} from '@mento/shared';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
@Injectable()
|
|
export class TeamsService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async list(organizationId: string): Promise<TeamDto[]> {
|
|
const rows = await this.prisma.team.findMany({
|
|
where: { organizationId },
|
|
include: {
|
|
members: {
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
fullName: true,
|
|
avatarStorageKey: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: [{ role: 'asc' }, { user: { fullName: 'asc' } }],
|
|
},
|
|
},
|
|
orderBy: { name: 'asc' },
|
|
});
|
|
return rows.map((row) => this.toDto(row));
|
|
}
|
|
|
|
async create(
|
|
organizationId: string,
|
|
dto: CreateTeamRequest,
|
|
): Promise<TeamDto> {
|
|
const name = dto.name?.trim();
|
|
if (!name) throw new BadRequestException('نام تیم الزامی است');
|
|
|
|
const managerIds = new Set((dto.managerIds ?? []).filter(Boolean));
|
|
const memberIds = [
|
|
...new Set([
|
|
...(dto.memberIds ?? []).filter(Boolean),
|
|
...managerIds,
|
|
]),
|
|
];
|
|
|
|
if (memberIds.length) {
|
|
await this.assertMembersInOrg(organizationId, memberIds);
|
|
}
|
|
|
|
try {
|
|
const row = await this.prisma.team.create({
|
|
data: {
|
|
organizationId,
|
|
name,
|
|
description: dto.description?.trim() ?? '',
|
|
members: memberIds.length
|
|
? {
|
|
create: memberIds.map((userId) => ({
|
|
userId,
|
|
role: managerIds.has(userId) ? 'manager' : 'member',
|
|
})),
|
|
}
|
|
: undefined,
|
|
},
|
|
include: {
|
|
members: {
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
fullName: true,
|
|
avatarStorageKey: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return this.toDto(row);
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
|
throw new BadRequestException('تیمی با این نام از قبل وجود دارد');
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async update(
|
|
organizationId: string,
|
|
id: string,
|
|
dto: UpdateTeamRequest,
|
|
): Promise<TeamDto> {
|
|
await this.ensureTeam(organizationId, id);
|
|
const data: Prisma.TeamUpdateInput = {};
|
|
if (dto.name !== undefined) {
|
|
const name = dto.name.trim();
|
|
if (!name) throw new BadRequestException('نام تیم الزامی است');
|
|
data.name = name;
|
|
}
|
|
if (dto.description !== undefined) {
|
|
data.description = dto.description.trim();
|
|
}
|
|
try {
|
|
await this.prisma.team.update({ where: { id }, data });
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
|
throw new BadRequestException('تیمی با این نام از قبل وجود دارد');
|
|
}
|
|
throw e;
|
|
}
|
|
return this.get(organizationId, id);
|
|
}
|
|
|
|
async remove(organizationId: string, id: string): Promise<void> {
|
|
await this.ensureTeam(organizationId, id);
|
|
await this.prisma.team.delete({ where: { id } });
|
|
}
|
|
|
|
async setMembers(
|
|
organizationId: string,
|
|
id: string,
|
|
dto: SetTeamMembersRequest,
|
|
): Promise<TeamDto> {
|
|
await this.ensureTeam(organizationId, id);
|
|
const members = dto.members ?? [];
|
|
const userIds = [...new Set(members.map((m) => m.userId).filter(Boolean))];
|
|
if (userIds.length) {
|
|
await this.assertMembersInOrg(organizationId, userIds);
|
|
}
|
|
|
|
const byUser = new Map<string, TeamMemberRole>();
|
|
for (const m of members) {
|
|
if (!m.userId) continue;
|
|
byUser.set(m.userId, m.role === 'manager' ? 'manager' : 'member');
|
|
}
|
|
|
|
await this.prisma.$transaction([
|
|
this.prisma.teamMember.deleteMany({ where: { teamId: id } }),
|
|
...(byUser.size
|
|
? [
|
|
this.prisma.teamMember.createMany({
|
|
data: [...byUser.entries()].map(([userId, role]) => ({
|
|
teamId: id,
|
|
userId,
|
|
role,
|
|
})),
|
|
}),
|
|
]
|
|
: []),
|
|
]);
|
|
|
|
return this.get(organizationId, id);
|
|
}
|
|
|
|
private async get(organizationId: string, id: string): Promise<TeamDto> {
|
|
const row = await this.prisma.team.findFirst({
|
|
where: { id, organizationId },
|
|
include: {
|
|
members: {
|
|
include: {
|
|
user: {
|
|
select: {
|
|
id: true,
|
|
fullName: true,
|
|
avatarStorageKey: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: [{ role: 'asc' }, { user: { fullName: 'asc' } }],
|
|
},
|
|
},
|
|
});
|
|
if (!row) throw new NotFoundException('تیم پیدا نشد');
|
|
return this.toDto(row);
|
|
}
|
|
|
|
private async ensureTeam(organizationId: string, id: string) {
|
|
const row = await this.prisma.team.findFirst({
|
|
where: { id, organizationId },
|
|
select: { id: true },
|
|
});
|
|
if (!row) throw new NotFoundException('تیم پیدا نشد');
|
|
}
|
|
|
|
private async assertMembersInOrg(
|
|
organizationId: string,
|
|
userIds: string[],
|
|
) {
|
|
const count = await this.prisma.membership.count({
|
|
where: {
|
|
organizationId,
|
|
userId: { in: userIds },
|
|
isActive: true,
|
|
},
|
|
});
|
|
if (count !== userIds.length) {
|
|
throw new BadRequestException('برخی کاربران عضو فعال این محیط نیستند');
|
|
}
|
|
}
|
|
|
|
private toDto(row: {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
members: Array<{
|
|
role: string;
|
|
user: {
|
|
id: string;
|
|
fullName: string;
|
|
avatarStorageKey: string | null;
|
|
};
|
|
}>;
|
|
}): TeamDto {
|
|
const members: TeamMemberDto[] = row.members.map((m) => ({
|
|
userId: m.user.id,
|
|
fullName: m.user.fullName,
|
|
role: m.role === 'manager' ? 'manager' : 'member',
|
|
hasAvatar: Boolean(m.user.avatarStorageKey),
|
|
}));
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
memberCount: members.length,
|
|
members,
|
|
createdAt: row.createdAt.toISOString(),
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
}
|