11 Commits

Author SHA1 Message Date
morteza af606ce9a7 task manager
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-25 15:48:33 +03:30
morteza 7996617cf0 fix superadmin bug
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-25 10:34:06 +03:30
morteza 9e28dfd628 new perms for ticket category
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-23 12:06:34 +03:30
morteza c20e0ba684 sort customers
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 16:17:55 +03:30
morteza ced46e500d sort customers
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 15:43:08 +03:30
morteza c925d2e539 multi role
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 12:19:37 +03:30
morteza 10035f3f25 update users
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-22 11:24:46 +03:30
morteza 19c396e9d1 populate user
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-21 14:32:54 +03:30
morteza a13097bbd6 task : populate
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-20 19:34:14 +03:30
morteza 8aaac51b12 add : ticket comment
Build and Deploy Docker Images / build_and_deploy (push) Has been cancelled
2026-07-20 12:07:20 +03:30
morteza b855d81817 add : ticket to task 2026-07-20 11:54:39 +03:30
33 changed files with 729 additions and 134 deletions
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddTicketToTaskEntity1784600000000 implements MigrationInterface {
name = "AddTicketToTaskEntity1784600000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_tasks" ADD "ticketId" uuid`);
await queryRunner.query(
`ALTER TABLE "tm_tasks" ADD CONSTRAINT "FK_tm_tasks_ticket" FOREIGN KEY ("ticketId") REFERENCES "ticket"("id") ON DELETE SET NULL ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_tasks" DROP CONSTRAINT "FK_tm_tasks_ticket"`);
await queryRunner.query(`ALTER TABLE "tm_tasks" DROP COLUMN "ticketId"`);
}
}
@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddTaskCommentEntity1784700000000 implements MigrationInterface {
name = "AddTaskCommentEntity1784700000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "tm_task_comments" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "content" text NOT NULL, "userId" uuid NOT NULL, "taskId" uuid NOT NULL, CONSTRAINT "PK_tm_task_comments" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "tm_task_comments" ADD CONSTRAINT "FK_tm_task_comments_user" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "tm_task_comments" ADD CONSTRAINT "FK_tm_task_comments_task" FOREIGN KEY ("taskId") REFERENCES "tm_tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP CONSTRAINT "FK_tm_task_comments_task"`);
await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP CONSTRAINT "FK_tm_task_comments_user"`);
await queryRunner.query(`DROP TABLE "tm_task_comments"`);
}
}
@@ -0,0 +1,175 @@
import { MigrationInterface, QueryRunner } from "typeorm";
const NEW_PERMISSIONS = [
{ name: "ticket_categories_create", title: "ایجاد دسته‌بندی تیکت", source: "tickets_create" },
{ name: "ticket_categories_read", title: "مشاهده دسته‌بندی تیکت", source: "tickets_read" },
{ name: "ticket_categories_update", title: "ویرایش دسته‌بندی تیکت", source: "tickets_update" },
{ name: "ticket_categories_delete", title: "حذف دسته‌بندی تیکت", source: "tickets_delete" },
] as const;
const CURRENT_ENUM_VALUES = [
"services_create",
"services_read",
"services_update",
"services_delete",
"customers_create",
"customers_read",
"customers_update",
"customers_delete",
"agents_create",
"agents_read",
"agents_update",
"agents_delete",
"developers_create",
"developers_read",
"developers_update",
"developers_delete",
"invoices_create",
"invoices_read",
"invoices_update",
"invoices_delete",
"transactions_create",
"transactions_read",
"transactions_update",
"transactions_delete",
"discounts_create",
"discounts_read",
"discounts_update",
"discounts_delete",
"admins_create",
"admins_read",
"admins_update",
"admins_delete",
"tickets_create",
"tickets_read",
"tickets_update",
"tickets_delete",
"criticisms_create",
"criticisms_read",
"criticisms_update",
"criticisms_delete",
"contacts_us_create",
"contacts_us_read",
"contacts_us_update",
"contacts_us_delete",
"advertisements_create",
"advertisements_read",
"advertisements_update",
"advertisements_delete",
"announcements_create",
"announcements_read",
"announcements_update",
"announcements_delete",
"blogs_create",
"blogs_read",
"blogs_update",
"blogs_delete",
"learnings_create",
"learnings_read",
"learnings_update",
"learnings_delete",
"settings_create",
"settings_read",
"settings_update",
"settings_delete",
"bank_accounts_create",
"bank_accounts_read",
"bank_accounts_update",
"bank_accounts_delete",
"payments_create",
"payments_read",
"payments_update",
"payments_delete",
"support_plan_create",
"support_plan_read",
"support_plan_update",
"support_plan_delete",
"reseller_create",
"reseller_read",
"reseller_update",
"reseller_delete",
"workspace_create",
"workspace_read",
"workspace_update",
"workspace_delete",
"project_create",
"project_read",
"project_update",
"project_delete",
"task_create",
"task_read",
"task_update",
"task_delete",
"task_phase_create",
"task_phase_read",
"task_phase_update",
"task_phase_delete",
"logs",
"manage_sso_clients",
"dmenu",
"dkala",
"dpage",
"dmail",
"view_all_workspaces",
"view_all_projects",
];
const UPDATED_ENUM_VALUES = [
...CURRENT_ENUM_VALUES.slice(0, CURRENT_ENUM_VALUES.indexOf("tickets_delete") + 1),
...NEW_PERMISSIONS.map((p) => p.name),
...CURRENT_ENUM_VALUES.slice(CURRENT_ENUM_VALUES.indexOf("tickets_delete") + 1),
];
export class AddTicketCategoriesPermissions1784800000000 implements MigrationInterface {
name = "AddTicketCategoriesPermissions1784800000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
const enumList = UPDATED_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
await queryRunner.query(
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
);
for (const perm of NEW_PERMISSIONS) {
const inserted = await queryRunner.query(
`INSERT INTO "permission" ("id", "name", "title", "group", "createdAt", "updatedAt")
VALUES (uuid_generate_v4(), $1, $2, 'support', NOW(), NOW())
RETURNING id`,
[perm.name, perm.title],
);
const newId = inserted[0].id as string;
const sourcePerm = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.source]);
if (!sourcePerm.length) continue;
await queryRunner.query(
`INSERT INTO "role_permission_relation" ("roleId", "permissionId")
SELECT "roleId", $1 FROM "role_permission_relation" WHERE "permissionId" = $2
ON CONFLICT ("roleId", "permissionId") DO NOTHING`,
[newId, sourcePerm[0].id],
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
for (const perm of NEW_PERMISSIONS) {
const rows = await queryRunner.query(`SELECT id FROM "permission" WHERE "name" = $1`, [perm.name]);
if (!rows.length) continue;
await queryRunner.query(`DELETE FROM "role_permission_relation" WHERE "permissionId" = $1`, [rows[0].id]);
await queryRunner.query(`DELETE FROM "permission" WHERE "id" = $1`, [rows[0].id]);
}
await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "name" TYPE text USING "name"::text`);
await queryRunner.query(`DROP TYPE "public"."permission_name_enum"`);
const enumList = CURRENT_ENUM_VALUES.map((v) => `'${v}'`).join(", ");
await queryRunner.query(`CREATE TYPE "public"."permission_name_enum" AS ENUM(${enumList})`);
await queryRunner.query(
`ALTER TABLE "permission" ALTER COLUMN "name" TYPE "public"."permission_name_enum" USING "name"::"public"."permission_name_enum"`,
);
}
}
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAttachmentsToTaskComment1784900000000 implements MigrationInterface {
name = "AddAttachmentsToTaskComment1784900000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "tm_task_comments" ADD "attachments" json NOT NULL DEFAULT '[]'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tm_task_comments" DROP COLUMN "attachments"`);
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "./"
},
{
"path": "./danak-admin"
}
],
"settings": {}
}
+4
View File
@@ -962,4 +962,8 @@ export const enum timeMessage {
INVALID_TIME_RANGE = "زمان پایان باید بعد از زمان شروع باشد!",
TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!",
NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!"
}
export const enum taskCommentMessage {
TASK_COMMENT_NOT_FOUND = "نظر مورد نظر پیدا نشد!"
}
+3 -2
View File
@@ -7,6 +7,7 @@ import { SSOClientService } from "./sso-client.service";
import { TokensService } from "./tokens.service";
import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable()
@@ -46,7 +47,7 @@ export class SSOService {
const payload: ITokenPayload = {
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
permissions: getPermissionsFromRoles(user.roles),
audience: clientId,
};
@@ -105,7 +106,7 @@ export class SSOService {
const payload: ITokenPayload = {
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
permissions: getPermissionsFromRoles(user.roles),
audience: clientId,
};
+2 -1
View File
@@ -9,6 +9,7 @@ import { DataSource, QueryRunner } from "typeorm";
import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity";
import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload";
import { ResellerType } from "../DTO/verify-reseller-otp.dto";
@@ -28,7 +29,7 @@ export class TokensService {
{
id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin),
permissions: user.roles.flatMap((r) => (r?.permissions?.length ? r.permissions.map((p) => p.name) : [])),
permissions: getPermissionsFromRoles(user.roles),
},
queryRunner,
);
@@ -12,6 +12,7 @@ import { UserDec } from "../../../common/decorators/user.decorator";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { ParamDto } from "../../../common/DTO/param.dto";
import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
import { User } from "../../users/entities/user.entity";
@AuthGuards()
@AdminRoute()
@@ -80,4 +81,11 @@ export class ProjectController {
getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> {
return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions);
}
@Get(":id/users")
@ApiOperation({ summary: "Get project users" })
@ApiResponse({ status: 200, description: "Got Project Users Successfully!" })
getProjectUsers(@Param() paramDto: ParamDto): Promise<User[]> {
return this.projectService.getProjectUsers(paramDto.id);
}
}
@@ -0,0 +1,50 @@
import { Controller, Post, Patch, Delete, Param, Body, Get } from "@nestjs/common";
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger";
import { TaskCommentService } from "../providers/task-comment.service";
import { CreateTaskCommentDto } from "../dto/task-comment/create-task-comment.dto";
import { UpdateTaskCommentDto } from "../dto/task-comment/update-task-comment.dto";
import { TMTaskComment } from "../entities/task-comment.entity";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { AdminRoute } from "../../../common/decorators/admin.decorator";
import { UserDec } from "../../../common/decorators/user.decorator";
@AuthGuards()
@AdminRoute()
@ApiTags("Task Manager - Comments")
@Controller("task-manager/comments")
export class TaskCommentController {
constructor(private readonly taskCommentService: TaskCommentService) {}
@Get("task/:taskId")
@ApiOperation({ summary: "Get all comments for a task" })
@ApiParam({ name: "taskId", description: "Task ID" })
getTaskComments(@Param("taskId") taskId: string): Promise<TMTaskComment[]> {
return this.taskCommentService.getTaskComments(taskId);
}
@Post()
@ApiOperation({ summary: "Create a new comment" })
@ApiResponse({ status: 201, description: "Comment created", type: TMTaskComment })
@ApiResponse({ status: 404, description: "Task not found" })
create(@UserDec("id") userId: string, @Body() body: CreateTaskCommentDto): Promise<TMTaskComment> {
return this.taskCommentService.create(userId, body);
}
@Patch(":id")
@ApiOperation({ summary: "Update a comment" })
@ApiParam({ name: "id", description: "Comment ID" })
@ApiResponse({ status: 200, description: "Comment updated", type: TMTaskComment })
@ApiResponse({ status: 404, description: "Comment or Task not found" })
update(@Param("id") id: string, @Body() body: UpdateTaskCommentDto): Promise<TMTaskComment> {
return this.taskCommentService.update(id, body);
}
@Delete(":id")
@ApiOperation({ summary: "Delete a comment" })
@ApiParam({ name: "id", description: "Comment ID" })
@ApiResponse({ status: 200, description: "Comment deleted" })
@ApiResponse({ status: 404, description: "Comment not found" })
remove(@Param("id") id: string): Promise<void> {
return this.taskCommentService.remove(id);
}
}
@@ -0,0 +1,25 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString, IsNotEmpty, IsUUID, IsOptional, IsArray } from "class-validator";
export class CreateTaskCommentDto {
@ApiProperty({ description: "Comment content", example: "Please review this task before closing." })
@IsString()
@IsNotEmpty()
content: string;
@ApiProperty({ description: "ID of the task this comment belongs to", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" })
@IsUUID()
@IsNotEmpty()
taskId: string;
@ApiProperty({
description: "Attachment file URLs or paths",
type: [String],
required: false,
example: ["https://cdn.example.com/files/screenshot.png"],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
attachments?: string[];
}
@@ -0,0 +1,4 @@
import { PartialType } from "@nestjs/swagger";
import { CreateTaskCommentDto } from "./create-task-comment.dto";
export class UpdateTaskCommentDto extends PartialType(CreateTaskCommentDto) {}
@@ -34,6 +34,15 @@ export class CreateTaskDto {
@IsNotEmpty()
taskPhaseId: string;
@ApiProperty({
description: "ID of the related ticket",
required: false,
example: "c3d4e5f6-a7b8-9012-cdef-123456789012",
})
@IsUUID()
@IsOptional()
ticketId?: string;
@ApiProperty({
description: "IDs of users to assign to the task",
required: false,
@@ -0,0 +1,36 @@
import { Column, Entity, JoinColumn, ManyToOne } from "typeorm";
import { BaseEntity } from "../../../common/entities/base.entity";
import { User } from "../../users/entities/user.entity";
import { TMTask } from "./task.entity";
@Entity("tm_task_comments")
export class TMTaskComment extends BaseEntity {
@Column({ type: "text" })
content: string;
@Column({ type: "json", default: [] })
attachments: string[];
// --- Relations ---
@ManyToOne(() => User, {
nullable: false,
onDelete: "RESTRICT",
})
@JoinColumn({ name: "userId" })
user: User;
@Column({ type: "uuid" })
userId: string;
@ManyToOne(() => TMTask, (task) => task.comments, {
nullable: false,
onDelete: "CASCADE",
})
@JoinColumn({ name: "taskId" })
task: TMTask;
@Column({ type: "uuid" })
taskId: string;
}
@@ -5,7 +5,9 @@ import { TMRemark } from "./remark.entity";
import { TMAttachment } from "./attachment.entity";
import { TMCheckListItem } from "./check-list-item.entity";
import { TMTime } from "./time.entity";
import { TMTaskComment } from "./task-comment.entity";
import { User } from "../../users/entities/user.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
@Entity("tm_tasks")
export class TMTask extends BaseEntity {
@@ -27,6 +29,13 @@ export class TMTask extends BaseEntity {
@Column({type: 'int', default: 1})
priority: number;
@ManyToOne(() => Ticket, { nullable: true, onDelete: "SET NULL" })
@JoinColumn({ name: "ticketId" })
ticket: Ticket | null;
@Column({ type: "uuid", nullable: true })
ticketId: string | null;
// --- Relations ---
@ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, {
@@ -59,6 +68,11 @@ export class TMTask extends BaseEntity {
})
times: TMTime[];
@OneToMany(() => TMTaskComment, (comment) => comment.task, {
cascade: ["insert", "update"],
})
comments: TMTaskComment[];
@ManyToMany(() => User, (user) => user.tasks)
@JoinTable({
name: "tm_task_users",
@@ -31,7 +31,8 @@ export class ProjectService {
users: {
id: true,
firstName: true,
lastName: true
lastName: true,
profilePic: true,
},
taskPhases: {
id: true,
@@ -165,4 +166,9 @@ export class ProjectService {
return projectDetail;
}
async getProjectUsers(projectId: string): Promise<User[]> {
const project = await this.findOneOrFail(projectId);
return project.users;
}
}
@@ -0,0 +1,61 @@
import { Injectable, NotFoundException } from "@nestjs/common";
import { TaskCommentRepository } from "../repositories/task-comment.repository";
import { TMTaskComment } from "../entities/task-comment.entity";
import { CreateTaskCommentDto } from "../dto/task-comment/create-task-comment.dto";
import { UpdateTaskCommentDto } from "../dto/task-comment/update-task-comment.dto";
import { taskCommentMessage } from "../../../common/enums/message.enum";
import { TaskService } from "./task.service";
@Injectable()
export class TaskCommentService {
constructor(
private readonly taskCommentRepository: TaskCommentRepository,
private readonly taskService: TaskService,
) {}
async findOneOrFail(id: string): Promise<TMTaskComment> {
const comment = await this.taskCommentRepository.findOne({
where: { id },
relations: { user: true, task: true },
});
if (!comment) throw new NotFoundException(taskCommentMessage.TASK_COMMENT_NOT_FOUND);
return comment;
}
async getTaskComments(taskId: string): Promise<TMTaskComment[]> {
await this.taskService.findOneOrFail(taskId);
return this.taskCommentRepository.find({
where: { taskId },
relations: { user: true },
order: { createdAt: "DESC" },
});
}
async create(userId: string, dto: CreateTaskCommentDto): Promise<TMTaskComment> {
await this.taskService.findOneOrFail(dto.taskId);
const comment = this.taskCommentRepository.create({
...dto,
attachments: dto.attachments ?? [],
userId,
});
return this.taskCommentRepository.save(comment);
}
async update(id: string, dto: UpdateTaskCommentDto): Promise<TMTaskComment> {
const comment = await this.findOneOrFail(id);
if (dto.taskId) {
await this.taskService.findOneOrFail(dto.taskId);
}
Object.assign(comment, dto);
return this.taskCommentRepository.save(comment);
}
async remove(id: string): Promise<void> {
await this.findOneOrFail(id);
await this.taskCommentRepository.delete(id);
}
}
@@ -7,7 +7,8 @@ import { CreateTaskDto } from "../dto/task/create-task.dto";
import { UpdateTaskDto } from "../dto/task/update-task.dto";
import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
import { User } from "../../users/entities/user.entity";
import { TaskMessage, TaskPhaseMessage } from "../../../common/enums/message.enum";
import { Ticket } from "../../tickets/entities/ticket.entity";
import { TaskMessage, TaskPhaseMessage, TicketMessageEnum } from "../../../common/enums/message.enum";
import { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { TMTaskPhase } from "../entities/task-phase.entity";
import { PermissionEnum } from "../../users/enums/permission.enum";
@@ -21,6 +22,8 @@ export class TaskService {
private readonly taskPhaseRepository: TaskPhaseRepository,
@InjectRepository(User)
private readonly userRepository: Repository<User>,
@InjectRepository(Ticket)
private readonly ticketRepository: Repository<Ticket>,
) {}
async findOneOrFail(id: string): Promise<TMTask> {
@@ -29,12 +32,19 @@ export class TaskService {
relations: {
taskPhase: true,
users: true,
ticket: true,
},
});
if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND);
return task;
}
private async findTicketOrFail(ticketId: string): Promise<Ticket> {
const ticket = await this.ticketRepository.findOne({ where: { id: ticketId } });
if (!ticket) throw new NotFoundException(TicketMessageEnum.TICKET_NOT_FOUND);
return ticket;
}
async findTaskPhaseOrFail(taskPhaseId: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOne({
where: { id: taskPhaseId },
@@ -50,10 +60,14 @@ export class TaskService {
}
async create(dto: CreateTaskDto): Promise<TMTask> {
const { userIds, startDate, endDate, ...rest } = dto;
const { userIds, startDate, endDate, ticketId, ...rest } = dto;
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (ticketId) {
await this.findTicketOrFail(ticketId);
}
if (userIds?.length) {
const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId));
if (!isUsersValid) throw new BadRequestException(TaskMessage.USERS_ASSIGNED_TO_TASK_NOT_PROJECT_MEMBER);
@@ -61,6 +75,7 @@ export class TaskService {
const task = this.taskRepository.create({
...rest,
ticketId,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
});
@@ -74,7 +89,11 @@ export class TaskService {
async update(id: string, dto: UpdateTaskDto): Promise<TMTask> {
const task = await this.findOneOrFail(id);
const { userIds, startDate, endDate, ...rest } = dto;
const { userIds, startDate, endDate, ticketId, ...rest } = dto;
if (ticketId) {
await this.findTicketOrFail(ticketId);
}
if (dto.taskPhaseId) {
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
@@ -87,6 +106,7 @@ export class TaskService {
Object.assign(task, {
...rest,
...(ticketId !== undefined && { ticketId }),
startDate: startDate ? new Date(startDate) : task.startDate,
endDate: endDate ? new Date(endDate) : task.endDate,
});
@@ -0,0 +1,13 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { TMTaskComment } from "../entities/task-comment.entity";
@Injectable()
export class TaskCommentRepository extends Repository<TMTaskComment> {
constructor(
@InjectRepository(TMTaskComment) taskCommentRepository: Repository<TMTaskComment>,
) {
super(taskCommentRepository.target, taskCommentRepository.manager, taskCommentRepository.queryRunner);
}
}
@@ -19,7 +19,10 @@ export class TaskRepository extends Repository<TMTask> {
.leftJoinAndSelect("task.checkListItems", "checkListItem")
.leftJoinAndSelect("task.users", "user")
.leftJoinAndSelect("task.times", "time")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("time.admin", "timeAdmin")
.leftJoinAndSelect("task.comments", "comment")
.leftJoinAndSelect("comment.user", "commentUser")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
@@ -44,6 +47,12 @@ export class TaskRepository extends Repository<TMTask> {
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
"ticket.id",
"ticket.subject",
"ticket.numericId",
"ticket.status",
"time.id",
"time.startedAt",
@@ -53,6 +62,14 @@ export class TaskRepository extends Repository<TMTask> {
"timeAdmin.id",
"timeAdmin.firstName",
"timeAdmin.lastName",
"comment.id",
"comment.content",
"comment.attachments",
"comment.createdAt",
"commentUser.firstName",
"commentUser.lastName",
"commentUser.profilePic",
])
.where("task.id = :taskId", { taskId })
@@ -113,12 +130,27 @@ export class TaskRepository extends Repository<TMTask> {
// response needs. IN (:...ids) doesn't preserve order, so re-order to match seekQuery.
const detailRows = await this.createQueryBuilder("task")
.leftJoinAndSelect("task.remarks", "remark")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("task.users", "user")
.loadRelationCountAndMap("task.attachmentCount", "task.attachments")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
.loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }),
)
.select(["task", "remark.id", "remark.title", "remark.color"])
.select([
"task",
"remark.id",
"remark.title",
"remark.color",
"user.id",
"user.firstName",
"user.lastName",
"user.profilePic",
"ticket.id",
"ticket.subject",
"ticket.numericId",
"ticket.status",
])
.where("task.id IN (:...ids)", { ids })
.getMany();
@@ -9,10 +9,12 @@ import { TMRemark } from "./entities/remark.entity";
import { TMAttachment } from "./entities/attachment.entity";
import { TMCheckListItem } from "./entities/check-list-item.entity";
import { TMTime } from "./entities/time.entity";
import { TMTaskComment } from "./entities/task-comment.entity";
import { WorkspaceRepository } from "./repositories/workspace.repository";
import { WorkspaceController } from "./controllers/workspace.controller";
import { WorkspaceService } from "./providers/workspace.service";
import { User } from "../users/entities/user.entity";
import { Ticket } from "../tickets/entities/ticket.entity";
import { ProjectController } from "./controllers/project.controller";
import { ProjectRepository } from "./repositories/project.repository";
import { ProjectService } from "./providers/project.service";
@@ -34,10 +36,13 @@ import { AttachmentService } from "./providers/attachment.service";
import { TimeController } from "./controllers/time.controller";
import { TimeRepository } from "./repositories/time.repository";
import { TimeService } from "./providers/time.service";
import { TaskCommentController } from "./controllers/task-comment.controller";
import { TaskCommentRepository } from "./repositories/task-comment.repository";
import { TaskCommentService } from "./providers/task-comment.service";
@Module({
imports: [
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, User]),
TypeOrmModule.forFeature([TMWorkspace, TMProject, TMTaskPhase, TMTask, TMRemark, TMAttachment, TMCheckListItem, TMTime, TMTaskComment, User, Ticket]),
],
controllers: [
WorkspaceController,
@@ -48,6 +53,7 @@ import { TimeService } from "./providers/time.service";
RemarkController,
AttachmentController,
TimeController,
TaskCommentController,
],
providers: [
WorkspaceRepository,
@@ -66,6 +72,8 @@ import { TimeService } from "./providers/time.service";
AttachmentService,
TimeRepository,
TimeService,
]
TaskCommentRepository,
TaskCommentService,
],
})
export class TaskManagerModule {}
@@ -33,6 +33,7 @@ import { TicketStatus } from "../enums/ticket-status.enum";
import { TicketCategoryRepository } from "../repositories/tickets-category.repository";
import { TicketMessagesRepository } from "../repositories/tickets-message.repository";
import { TicketsRepository } from "../repositories/tickets.repository";
import { PermissionEnum } from "../../users/enums/permission.enum";
@Injectable()
export class TicketsService implements OnModuleInit {
@@ -326,10 +327,10 @@ export class TicketsService implements OnModuleInit {
return { tickets, count, paginate: true };
}
//******************************** */
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) {
const isSuperAdmin = await this.usersService.isSuperAdmin(adminId);
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminPermissions: PermissionEnum[],adminId: string) {
const canSeeAllTickets = adminPermissions.includes(PermissionEnum.TICKETS_READ);
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, isSuperAdmin);
const [tickets, count] = await this.ticketsRepository.findTicketsListForAdmin(queryDto, adminId, canSeeAllTickets);
return { tickets, count, paginate: true };
}
+7 -7
View File
@@ -15,6 +15,7 @@ import { PermissionsDec } from "../../common/decorators/permission.decorator";
import { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator";
import { ParamDto } from "../../common/DTO/param.dto";
import { PermissionEnum } from "../users/enums/permission.enum";
import { ITokenPayload } from "../auth/interfaces/IToken-payload";
@Controller("tickets")
@ApiTags("Tickets")
@AuthGuards()
@@ -24,14 +25,14 @@ export class TicketsController {
//----------------- ticket categories -------------------
@ApiOperation({ summary: "Create ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_CREATE)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_CREATE)
@Post("category")
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders);
}
@ApiOperation({ summary: "Update ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_UPDATE)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Patch("category/:id")
updateCategory(
@Param() paramDto: ParamDto,
@@ -49,7 +50,7 @@ export class TicketsController {
@ApiOperation({ summary: "Get ticket categories list => admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_READ)
@Get("categories-list")
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
return this.ticketsService.getCategoriesList(queryDto);
@@ -58,7 +59,7 @@ export class TicketsController {
@ApiOperation({ summary: "toggle status of categories => admin route" })
@AdminRoute()
@HttpCode(HttpStatus.OK)
@PermissionsDec(PermissionEnum.TICKETS_UPDATE)
@PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Post("category/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.toggleCategoryStatus(paramDto, userIpAndHeaders);
@@ -88,10 +89,9 @@ export class TicketsController {
@ApiOperation({ summary: "Get all tickets ==> admin route" })
@AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ)
@Get("admin")
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) {
return this.ticketsService.getTicketForAdmin(queryDto, adminId);
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec() admin: ITokenPayload) {
return this.ticketsService.getTicketForAdmin(queryDto, admin.permissions, admin.id);
}
@ApiOperation({ summary: "create ticket messages" })
+6 -4
View File
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator";
import { ArrayNotEmpty, IsArray, IsEmail, IsMobilePhone, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length, MinLength } from "class-validator";
import { AdminMessage, AuthMessage } from "../../../common/enums/message.enum";
@@ -22,9 +22,11 @@ export class CreateAdminDto {
phone: string;
@IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
@IsUUID("4", { message: AdminMessage.ROLE_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "Role id", example: "123e4567-e89b-12d3-a456-426614174000" })
roleId: string;
@IsArray({ message: AdminMessage.ROLE_REQUIRED })
@ArrayNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
@IsUUID("4", { each: true, message: AdminMessage.ROLE_ID_SHOULD_BE_UUID })
@ApiProperty({ description: "Role ids", example: ["123e4567-e89b-12d3-a456-426614174000"], type: [String] })
roleIds: string[];
@IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
@IsString({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
@@ -52,6 +52,11 @@ export const permissionMeta: Record<PermissionEnum, PermissionMeta> = {
[PermissionEnum.TICKETS_UPDATE]: { title: "ویرایش تیکت‌ها", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKETS_DELETE]: { title: "حذف تیکت‌ها", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_CREATE]: { title: "ایجاد دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_READ]: { title: "مشاهده دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_UPDATE]: { title: "ویرایش دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKET_CATEGORIES_DELETE]: { title: "حذف دسته‌بندی تیکت", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.CRITICISMS_CREATE]: { title: "ایجاد انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_READ]: { title: "مشاهده انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_UPDATE]: { title: "ویرایش انتقادات", group: PermissionGroupEnum.OTHER },
@@ -53,6 +53,12 @@ export enum PermissionEnum {
TICKETS_UPDATE = "tickets_update",
TICKETS_DELETE = "tickets_delete",
// ticket_categories
TICKET_CATEGORIES_CREATE = "ticket_categories_create",
TICKET_CATEGORIES_READ = "ticket_categories_read",
TICKET_CATEGORIES_UPDATE = "ticket_categories_update",
TICKET_CATEGORIES_DELETE = "ticket_categories_delete",
// criticisms
CRITICISMS_CREATE = "criticisms_create",
CRITICISMS_READ = "criticisms_read",
+19 -19
View File
@@ -18,6 +18,7 @@ import { RoleEnum } from "../enums/role.enum";
import { PermissionsRepository } from "../repositories/permissions.repository";
import { RoleRepository } from "../repositories/roles.repository";
import { UserRepository } from "../repositories/users.repository";
import { getPermissionEntitiesFromRoles } from "../utils/role-permissions.util";
@Injectable()
export class AdminsService {
@@ -43,22 +44,22 @@ export class AdminsService {
const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone });
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
const role = await queryRunner.manager.findOne(this.rolesRepository.target, {
where: { id: createDto.roleId },
const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: In(createDto.roleIds), isAdmin: true },
relations: { permissions: true },
});
if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
if (roles.length !== createDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
if (createDto.password !== createDto.repeatPassword) throw new BadRequestException(AdminMessage.PASSWORD_NOT_MATCH);
const hashedPassword = await this.passwordService.hashPassword(createDto.password);
const userName = slugify(`${createDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true });
await queryRunner.manager.save(this.rolesRepository.target, role);
const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...adminData } = createDto;
const adminUser = queryRunner.manager.create(this.userRepository.target, {
...createDto,
roles: [role],
...adminData,
roles,
password: hashedPassword,
userName,
});
@@ -75,7 +76,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin created new admin: ${createDto.email}`,
metadata: {
roleId: createDto.roleId,
roleIds: createDto.roleIds,
createdBy: "admin",
email: createDto.email,
phone: createDto.phone,
@@ -118,16 +119,14 @@ export class AdminsService {
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
}
if (updateDto.roleId) {
const role = await queryRunner.manager.findOne(this.rolesRepository.target, {
where: { id: updateDto.roleId },
if (updateDto.roleIds?.length) {
const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: In(updateDto.roleIds), isAdmin: true },
relations: { permissions: true },
});
if (!role) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
if (roles.length !== updateDto.roleIds.length) throw new BadRequestException(AdsMessage.ROLE_NOT_FOUND);
await queryRunner.manager.save(this.rolesRepository.target, role);
admin.roles = [role];
admin.roles = roles;
await queryRunner.manager.save(this.userRepository.target, admin);
}
@@ -138,7 +137,8 @@ export class AdminsService {
admin.password = await this.passwordService.hashPassword(updateDto.password);
}
await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateDto, password: admin.password });
const { roleIds: _roleIds, repeatPassword: _repeatPassword, ...updateData } = updateDto;
await queryRunner.manager.save(this.userRepository.target, { ...admin, ...updateData, password: admin.password });
await queryRunner.commitTransaction();
await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, {
@@ -150,7 +150,7 @@ export class AdminsService {
userType: UserType.ADMIN,
actionDescription: `Admin updated admin: ${admin.email}`,
metadata: {
roleId: updateDto.roleId,
roleIds: updateDto.roleIds,
},
oldValues: JSON.stringify({ ...admin }),
newValues: JSON.stringify({ ...updateDto }),
@@ -169,7 +169,7 @@ export class AdminsService {
/************************************************************ */
async deleteAdmin(id: string, userIpAndHeaders: IUserIpAndHeaders) {
const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } } });
const admin = await this.userRepository.findOne({ where: { id, roles: { isAdmin: true } }, relations: { roles: true } });
if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND);
await this.userRepository.softDelete(id);
@@ -182,7 +182,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin deleted admin: ${admin.email}`,
metadata: {
roleId: admin.roles[0].id,
roleIds: admin.roles.map((role) => role.id),
email: admin.email,
phone: admin.phone,
},
@@ -303,7 +303,7 @@ export class AdminsService {
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return {
permissions: user.roles[0].permissions,
permissions: getPermissionEntitiesFromRoles(user.roles),
};
}
/************************************************************ */
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { DataSource, Not } from "typeorm";
import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator";
import { CommonMessage, UserMessage } from "../../../common/enums/message.enum";
import { AuthMessage, CommonMessage, FinancialMessage, UserMessage } from "../../../common/enums/message.enum";
import { UserType } from "../../access-logs/enums/user-type.enum";
import { AccessLogService } from "../../access-logs/providers/access-log.service";
import { Address } from "../../address/entities/address.entity";
@@ -180,67 +180,100 @@ export class CustomersService {
await queryRunner.manager.update(this.userRepository.target, { id: customerId }, { password: hashPassword });
}
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { user: { id: customerId } },
relations: { address: true },
});
const hasLegalUpdateData = Boolean(
registrationName || economicCode || nationalIdentity || registrationCode || postalCode || address || cityId,
);
if (!legalUser) throw new BadRequestException(UserMessage.LEGAL_DATA_NOT_FOUND);
if (economicCode) {
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { economicCode, id: Not(legalUser.id) },
if (hasLegalUpdateData) {
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { user: { id: customerId } },
relations: { address: true },
});
if (existingEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST);
}
if (registrationCode) {
const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationCode, id: Not(legalUser.id) },
});
if (existingRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST);
}
if (!legalUser) {
if (user.financialType === FinancialType.REAL) {
throw new BadRequestException(UserMessage.FINANCIAL_TYPE_IS_REAL);
}
if (nationalIdentity) {
const existingNationalIdentity = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { nationalIdentity, id: Not(legalUser.id) },
});
if (existingNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST);
}
const legalPhone = phone ?? user.phone;
if (registrationName) {
const existingRegistrationName = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationName, id: Not(legalUser.id) },
});
if (existingRegistrationName) throw new BadRequestException(UserMessage.REGISTRATION_NAME_EXIST);
}
if (!economicCode) throw new BadRequestException(FinancialMessage.ECONOMIC_CODE_REQUIRED);
if (!registrationCode) throw new BadRequestException(FinancialMessage.REGISTRATION_CODE_REQUIRED);
if (!registrationName) throw new BadRequestException(FinancialMessage.REGISTRATION_NAME_REQUIRED);
if (!nationalIdentity) throw new BadRequestException(FinancialMessage.NATIONAL_ID_REQUIRED);
if (!legalPhone) throw new BadRequestException(AuthMessage.PHONE_NOT_EMPTY);
if (!address) throw new BadRequestException(FinancialMessage.ADDRESS_REQUIRED);
if (!postalCode) throw new BadRequestException(FinancialMessage.POSTAL_CODE_REQUIRED);
if (!cityId) throw new BadRequestException(FinancialMessage.CITY_ID_REQUIRED);
await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
await this.createLegalUserData(
{
economicCode,
registrationCode,
registrationName,
nationalIdentity,
phone: legalPhone,
address,
postalCode,
cityId: +cityId,
},
customerId,
queryRunner,
);
} else {
if (economicCode) {
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { economicCode, id: Not(legalUser.id) },
});
if (existingEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST);
}
// Update address if needed
if (cityId || address || postalCode) {
// Get the address entity
const addressEntity = legalUser.address;
if (registrationCode) {
const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationCode, id: Not(legalUser.id) },
});
if (existingRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST);
}
if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND);
if (nationalIdentity) {
const existingNationalIdentity = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { nationalIdentity, id: Not(legalUser.id) },
});
if (existingNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST);
}
// Prepare address updates
const addressUpdates: Record<string, unknown> = {};
if (registrationName) {
const existingRegistrationName = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { registrationName, id: Not(legalUser.id) },
});
if (existingRegistrationName) throw new BadRequestException(UserMessage.REGISTRATION_NAME_EXIST);
}
if (cityId) {
const { city } = await this.addressService.getCity(+cityId);
addressUpdates.city = city;
await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
if (cityId || address || postalCode) {
const addressEntity = legalUser.address;
if (!addressEntity) throw new BadRequestException(UserMessage.ADDRESS_NOT_FOUND);
const addressUpdates: Record<string, unknown> = {};
if (cityId) {
const { city } = await this.addressService.getCity(+cityId);
addressUpdates.city = city;
}
if (address) {
addressUpdates.fullAddress = address;
}
if (postalCode) {
addressUpdates.postalCode = postalCode;
}
await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates);
}
}
if (address) {
addressUpdates.fullAddress = address;
}
if (postalCode) {
addressUpdates.postalCode = postalCode;
}
await queryRunner.manager.update(Address, { id: addressEntity.id }, addressUpdates);
}
await queryRunner.commitTransaction();
@@ -359,6 +359,7 @@ export class UsersService {
async isSuperAdmin(adminId: string) {
const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
console.log("user.roles", user.roles.map((role) => role.name));
return user.roles.some((role) => role.name === RoleEnum.SUPER_ADMIN);
}
@@ -119,10 +119,21 @@ export class UserRepository extends Repository<User> {
.loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) {
queryBuilder
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` });
queryBuilder.andWhere(
`(
user.firstName ILIKE :search
OR user.lastName ILIKE :search
OR CONCAT(user.firstName, ' ', user.lastName) ILIKE :search
OR user.userName ILIKE :search
OR user.phone ILIKE :search
OR legalUser.registrationName ILIKE :search
OR legalUser.phone ILIKE :search
OR realUser.firstName ILIKE :search
OR realUser.lastName ILIKE :search
OR realUser.phone ILIKE :search
)`,
{ search: `%${queryDto.q}%` },
);
}
return queryBuilder.getMany();
@@ -166,13 +177,28 @@ export class UserRepository extends Repository<User> {
.loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) {
queryBuilder
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` })
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` });
queryBuilder.andWhere(
`(
user.firstName ILIKE :search
OR user.lastName ILIKE :search
OR CONCAT(user.firstName, ' ', user.lastName) ILIKE :search
OR user.userName ILIKE :search
OR user.phone ILIKE :search
OR legalUser.registrationName ILIKE :search
OR legalUser.phone ILIKE :search
OR realUser.firstName ILIKE :search
OR realUser.lastName ILIKE :search
OR realUser.phone ILIKE :search
)`,
{ search: `%${queryDto.q}%` },
);
}
return await queryBuilder.skip(skip).take(limit).getManyAndCount();
return await queryBuilder
.orderBy("user.createdAt", "DESC")
.skip(skip)
.take(limit)
.getManyAndCount();
}
//************* */
@@ -0,0 +1,19 @@
import { PermissionEnum } from "../enums/permission.enum";
import { Role } from "../entities/role.entity";
import { Permission } from "../entities/permission.entity";
export function getPermissionsFromRoles(roles: Role[]): PermissionEnum[] {
return [...new Set(roles.flatMap((role) => (role?.permissions?.length ? role.permissions.map((permission) => permission.name) : [])))];
}
export function getPermissionEntitiesFromRoles(roles: Role[]): Permission[] {
const permissionsById = new Map<string, Permission>();
for (const role of roles) {
for (const permission of role?.permissions ?? []) {
permissionsById.set(permission.id, permission);
}
}
return [...permissionsById.values()];
}
-22
View File
@@ -1,22 +0,0 @@
import { INestApplication } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import request from "supertest";
import { AppModule } from "./../src/app.module";
describe("AppController (e2e)", () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it("/ (GET)", () => {
return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
});
});
-9
View File
@@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}