Files
teamwallet/myteamwallet_backend/src/users/users.service.ts
2026-07-31 22:29:15 +02:00

346 lines
9.5 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Player } from 'src/players/entities/player.entity';
import { EntityCondition } from 'src/utils/types/entity-condition.type';
import { IPaginationOptions } from 'src/utils/types/pagination-options';
import { Brackets, Repository, SelectQueryBuilder } from 'typeorm';
import { RoleEnum } from '../roles/roles.enum';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { UserDirectoryQueryDto } from './dto/user-directory-query.dto';
import {
AdminUserDirectorySummaryDto,
UserDirectoryAssignmentDto,
UserDirectoryPageDto,
UserDirectoryReferenceDto,
UserDirectorySummaryDto,
} from './dto/user-directory-response.dto';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
@InjectRepository(Player)
private playersRepository: Repository<Player>,
) {}
create(createProfileDto: CreateUserDto) {
return this.usersRepository.save(
this.usersRepository.create(createProfileDto),
);
}
findManyWithPagination(paginationOptions: IPaginationOptions) {
return this.usersRepository.find({
skip: (paginationOptions.page - 1) * paginationOptions.limit,
take: paginationOptions.limit,
});
}
async findDirectory(
requester: Pick<User, 'id' | 'role'>,
query: UserDirectoryQueryDto,
): Promise<UserDirectoryPageDto> {
const isAdmin = requester.role?.id === RoleEnum.admin;
const page = query.page ?? 1;
const limit = query.limit ?? 20;
const sharedTeamsQuery = isAdmin
? undefined
: this.createSharedTeamsQuery(requester.id);
const directoryQuery = this.createDirectoryUsersQuery(
isAdmin,
sharedTeamsQuery,
query.search,
);
const totalResult = await directoryQuery
.clone()
.select('COUNT(DISTINCT user.id)', 'total')
.getRawOne<{ total: string }>();
const userRows = await directoryQuery
.select(this.directoryUserSelects(isAdmin))
.distinct(true)
.orderBy('user.id', 'ASC')
.offset((page - 1) * limit)
.limit(limit)
.getRawMany<DirectoryUserRow>();
const assignments = await this.findDirectoryAssignments(
userRows.map((user) => Number(user.user_id)),
isAdmin,
sharedTeamsQuery,
);
const assignmentsByUserId = assignments.reduce((byUserId, assignment) => {
const userAssignments = byUserId.get(Number(assignment.user_id)) ?? [];
userAssignments.push(assignment);
byUserId.set(Number(assignment.user_id), userAssignments);
return byUserId;
}, new Map<number, DirectoryAssignmentRow[]>());
const total = Number(totalResult?.total ?? 0);
const data = userRows.map((user) =>
this.mapDirectoryUser(
user,
assignmentsByUserId.get(Number(user.user_id)) ?? [],
isAdmin,
),
);
return {
data,
page,
limit,
total,
hasNextPage: page * limit < total,
};
}
findOne(fields: EntityCondition<User>) {
return this.usersRepository.findOne({
where: fields,
});
}
update(id: number, updateProfileDto: UpdateUserDto) {
return this.usersRepository.save(
this.usersRepository.create({
id,
...updateProfileDto,
}),
);
}
async softDelete(id: number): Promise<void> {
await this.usersRepository.softDelete(id);
}
async findTeams(fields: EntityCondition<User>) {
const user = await this.findOne(fields);
if (!user) {
return [];
}
const players = await this.playersRepository.find({
where: {
user: {
id: user.id,
},
},
relations: ['team'],
});
return players;
}
async linkPlayerToUserId(user: User, playerId: number): Promise<boolean> {
return new Promise<boolean>(async (resolve) => {
const player = await this.playersRepository.findOneByOrFail({
id: playerId,
});
player.user = user;
await this.playersRepository.save(player);
return resolve(true);
});
}
private createSharedTeamsQuery(requesterId: number) {
return this.playersRepository
.createQueryBuilder('requesterPlayer')
.select('requesterPlayer.teamId')
.where('requesterPlayer.userId = :requesterId', { requesterId });
}
private createDirectoryUsersQuery(
isAdmin: boolean,
sharedTeamsQuery: SelectQueryBuilder<Player> | undefined,
search: string | undefined,
) {
const query = this.usersRepository
.createQueryBuilder('user')
.leftJoin('user.status', 'status')
.leftJoin('user.role', 'role');
if (!isAdmin && sharedTeamsQuery) {
query
.innerJoin(
'user.players',
'visibleAssignment',
`visibleAssignment.teamId IN (${sharedTeamsQuery.getQuery()})`,
)
.setParameters(sharedTeamsQuery.getParameters());
}
const term = search?.trim().toLocaleLowerCase();
if (!term) {
return query;
}
const assignmentAlias = isAdmin ? 'searchAssignment' : 'visibleAssignment';
if (isAdmin) {
query.leftJoin('user.players', assignmentAlias);
}
const searchColumns = [
'user.firstName',
'user.lastName',
`${assignmentAlias}.firstName`,
`${assignmentAlias}.lastName`,
...(isAdmin ? ['user.email'] : []),
];
query.andWhere(
new Brackets((where) => {
searchColumns.forEach((column, index) => {
const expression = `LOWER(${column}) LIKE :directorySearch`;
if (index === 0) {
where.where(expression);
} else {
where.orWhere(expression);
}
});
}),
);
return query.setParameter('directorySearch', `%${term}%`);
}
private directoryUserSelects(isAdmin: boolean): string[] {
return [
'user.id AS user_id',
'user.firstName AS first_name',
'user.lastName AS last_name',
'status.id AS status_id',
'status.name AS status_name',
...(isAdmin
? [
'user.email AS email',
'role.id AS role_id',
'role.name AS role_name',
]
: []),
];
}
private async findDirectoryAssignments(
userIds: number[],
isAdmin: boolean,
sharedTeamsQuery: SelectQueryBuilder<Player> | undefined,
): Promise<DirectoryAssignmentRow[]> {
if (userIds.length === 0) {
return [];
}
const query = this.playersRepository
.createQueryBuilder('player')
.innerJoin('player.team', 'team')
.leftJoin('player.teamRole', 'teamRole')
.select([
'player.id AS player_id',
'player.userId AS user_id',
'player.firstName AS first_name',
'player.lastName AS last_name',
'player.active AS active',
'team.id AS team_id',
'team.name AS team_name',
'team.alias AS team_alias',
'teamRole.id AS team_role_id',
'teamRole.name AS team_role_name',
])
.where('player.userId IN (:...userIds)', { userIds })
.orderBy('player.id', 'ASC');
if (!isAdmin && sharedTeamsQuery) {
query
.andWhere(`player.teamId IN (${sharedTeamsQuery.getQuery()})`)
.setParameters(sharedTeamsQuery.getParameters());
}
return query.getRawMany<DirectoryAssignmentRow>();
}
private mapDirectoryUser(
user: DirectoryUserRow,
assignments: DirectoryAssignmentRow[],
includeAdminFields: boolean,
): UserDirectorySummaryDto | AdminUserDirectorySummaryDto {
const summary: UserDirectorySummaryDto = {
id: Number(user.user_id),
firstName: user.first_name,
lastName: user.last_name,
status: this.mapDirectoryReference(user.status_id, user.status_name),
assignments: assignments.map((assignment) =>
this.mapDirectoryAssignment(assignment),
),
};
if (!includeAdminFields) {
return summary;
}
return {
...summary,
email: user.email,
role: this.mapDirectoryReference(user.role_id, user.role_name),
};
}
private mapDirectoryAssignment(
player: DirectoryAssignmentRow,
): UserDirectoryAssignmentDto {
return {
id: Number(player.player_id),
firstName: player.first_name,
lastName: player.last_name,
active: this.asBoolean(player.active),
team: {
id: Number(player.team_id),
name: player.team_name,
alias: player.team_alias,
},
teamRole: this.mapDirectoryReference(
player.team_role_id,
player.team_role_name,
),
};
}
private asBoolean(value: boolean | number | string): boolean {
return value === true || value === 1 || value === '1';
}
private mapDirectoryReference(
id: number | string | null,
name: string | null,
): UserDirectoryReferenceDto | null {
if (id == null) {
return null;
}
return {
id: Number(id),
name: name ?? undefined,
};
}
}
interface DirectoryUserRow {
user_id: number | string;
first_name: string | null;
last_name: string | null;
status_id: number | string | null;
status_name: string | null;
email?: string | null;
role_id?: number | string | null;
role_name?: string | null;
}
interface DirectoryAssignmentRow {
player_id: number | string;
user_id: number | string;
first_name: string;
last_name: string;
active: boolean | number | string;
team_id: number | string;
team_name: string;
team_alias: string;
team_role_id: number | string | null;
team_role_name: string | null;
}