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 = "زمان پایان باید بعد از زمان شروع باشد!", INVALID_TIME_RANGE = "زمان پایان باید بعد از زمان شروع باشد!",
TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!", TIME_ALREADY_STARTED = "برای این تسک یک زمان در حال اجرا وجود دارد!",
NO_RUNNING_TIME = "زمان در حال اجرایی برای این تسک پیدا نشد!" 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 { TokensService } from "./tokens.service";
import { AuthMessage } from "../../../common/enums/message.enum"; import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload"; import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable() @Injectable()
@@ -46,7 +47,7 @@ export class SSOService {
const payload: ITokenPayload = { const payload: ITokenPayload = {
id: user.id, id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin), 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, audience: clientId,
}; };
@@ -105,7 +106,7 @@ export class SSOService {
const payload: ITokenPayload = { const payload: ITokenPayload = {
id: user.id, id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin), 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, audience: clientId,
}; };
+2 -1
View File
@@ -9,6 +9,7 @@ import { DataSource, QueryRunner } from "typeorm";
import { AuthMessage } from "../../../common/enums/message.enum"; import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository"; import { RefreshTokensRepository } from "../../users/repositories/refresh-token.repository";
import { getPermissionsFromRoles } from "../../users/utils/role-permissions.util";
import { ITokenPayload } from "../interfaces/IToken-payload"; import { ITokenPayload } from "../interfaces/IToken-payload";
import { ResellerType } from "../DTO/verify-reseller-otp.dto"; import { ResellerType } from "../DTO/verify-reseller-otp.dto";
@@ -28,7 +29,7 @@ export class TokensService {
{ {
id: user.id, id: user.id,
isAdmin: user.roles.some((r) => r.isAdmin), 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, queryRunner,
); );
@@ -12,6 +12,7 @@ import { UserDec } from "../../../common/decorators/user.decorator";
import { AuthGuards } from "../../../common/decorators/auth-guard.decorator"; import { AuthGuards } from "../../../common/decorators/auth-guard.decorator";
import { ParamDto } from "../../../common/DTO/param.dto"; import { ParamDto } from "../../../common/DTO/param.dto";
import { ITokenPayload } from "../../auth/interfaces/IToken-payload"; import { ITokenPayload } from "../../auth/interfaces/IToken-payload";
import { User } from "../../users/entities/user.entity";
@AuthGuards() @AuthGuards()
@AdminRoute() @AdminRoute()
@@ -80,4 +81,11 @@ export class ProjectController {
getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> { getProjectDetail(@Param() paramDto: ParamDto, @UserDec() user: ITokenPayload): Promise<TMProject | null> {
return this.projectService.getProjectDetail(paramDto.id, user.id, user.permissions); 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() @IsNotEmpty()
taskPhaseId: string; taskPhaseId: string;
@ApiProperty({
description: "ID of the related ticket",
required: false,
example: "c3d4e5f6-a7b8-9012-cdef-123456789012",
})
@IsUUID()
@IsOptional()
ticketId?: string;
@ApiProperty({ @ApiProperty({
description: "IDs of users to assign to the task", description: "IDs of users to assign to the task",
required: false, 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 { TMAttachment } from "./attachment.entity";
import { TMCheckListItem } from "./check-list-item.entity"; import { TMCheckListItem } from "./check-list-item.entity";
import { TMTime } from "./time.entity"; import { TMTime } from "./time.entity";
import { TMTaskComment } from "./task-comment.entity";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { Ticket } from "../../tickets/entities/ticket.entity";
@Entity("tm_tasks") @Entity("tm_tasks")
export class TMTask extends BaseEntity { export class TMTask extends BaseEntity {
@@ -27,6 +29,13 @@ export class TMTask extends BaseEntity {
@Column({type: 'int', default: 1}) @Column({type: 'int', default: 1})
priority: number; priority: number;
@ManyToOne(() => Ticket, { nullable: true, onDelete: "SET NULL" })
@JoinColumn({ name: "ticketId" })
ticket: Ticket | null;
@Column({ type: "uuid", nullable: true })
ticketId: string | null;
// --- Relations --- // --- Relations ---
@ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, { @ManyToOne(() => TMTaskPhase, (taskPhase) => taskPhase.tasks, {
@@ -59,6 +68,11 @@ export class TMTask extends BaseEntity {
}) })
times: TMTime[]; times: TMTime[];
@OneToMany(() => TMTaskComment, (comment) => comment.task, {
cascade: ["insert", "update"],
})
comments: TMTaskComment[];
@ManyToMany(() => User, (user) => user.tasks) @ManyToMany(() => User, (user) => user.tasks)
@JoinTable({ @JoinTable({
name: "tm_task_users", name: "tm_task_users",
@@ -31,7 +31,8 @@ export class ProjectService {
users: { users: {
id: true, id: true,
firstName: true, firstName: true,
lastName: true lastName: true,
profilePic: true,
}, },
taskPhases: { taskPhases: {
id: true, id: true,
@@ -165,4 +166,9 @@ export class ProjectService {
return projectDetail; 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 { UpdateTaskDto } from "../dto/task/update-task.dto";
import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto"; import { ChangeTaskPhaseDto } from "../dto/task/change-task-phase.dto";
import { User } from "../../users/entities/user.entity"; 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 { TaskPhaseRepository } from "../repositories/task-phase.repository";
import { TMTaskPhase } from "../entities/task-phase.entity"; import { TMTaskPhase } from "../entities/task-phase.entity";
import { PermissionEnum } from "../../users/enums/permission.enum"; import { PermissionEnum } from "../../users/enums/permission.enum";
@@ -21,6 +22,8 @@ export class TaskService {
private readonly taskPhaseRepository: TaskPhaseRepository, private readonly taskPhaseRepository: TaskPhaseRepository,
@InjectRepository(User) @InjectRepository(User)
private readonly userRepository: Repository<User>, private readonly userRepository: Repository<User>,
@InjectRepository(Ticket)
private readonly ticketRepository: Repository<Ticket>,
) {} ) {}
async findOneOrFail(id: string): Promise<TMTask> { async findOneOrFail(id: string): Promise<TMTask> {
@@ -29,12 +32,19 @@ export class TaskService {
relations: { relations: {
taskPhase: true, taskPhase: true,
users: true, users: true,
ticket: true,
}, },
}); });
if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND); if (!task) throw new NotFoundException(TaskMessage.TASK_NOT_FOUND);
return task; 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> { async findTaskPhaseOrFail(taskPhaseId: string): Promise<TMTaskPhase> {
const taskPhase = await this.taskPhaseRepository.findOne({ const taskPhase = await this.taskPhaseRepository.findOne({
where: { id: taskPhaseId }, where: { id: taskPhaseId },
@@ -50,10 +60,14 @@ export class TaskService {
} }
async create(dto: CreateTaskDto): Promise<TMTask> { 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); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
if (ticketId) {
await this.findTicketOrFail(ticketId);
}
if (userIds?.length) { if (userIds?.length) {
const isUsersValid = userIds.every((userId) => taskPhase.project.users.some((user) => user.id === userId)); 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); 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({ const task = this.taskRepository.create({
...rest, ...rest,
ticketId,
startDate: startDate ? new Date(startDate) : undefined, startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined, endDate: endDate ? new Date(endDate) : undefined,
}); });
@@ -74,7 +89,11 @@ export class TaskService {
async update(id: string, dto: UpdateTaskDto): Promise<TMTask> { async update(id: string, dto: UpdateTaskDto): Promise<TMTask> {
const task = await this.findOneOrFail(id); 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) { if (dto.taskPhaseId) {
const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId); const taskPhase = await this.findTaskPhaseOrFail(dto.taskPhaseId);
@@ -87,6 +106,7 @@ export class TaskService {
Object.assign(task, { Object.assign(task, {
...rest, ...rest,
...(ticketId !== undefined && { ticketId }),
startDate: startDate ? new Date(startDate) : task.startDate, startDate: startDate ? new Date(startDate) : task.startDate,
endDate: endDate ? new Date(endDate) : task.endDate, 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.checkListItems", "checkListItem")
.leftJoinAndSelect("task.users", "user") .leftJoinAndSelect("task.users", "user")
.leftJoinAndSelect("task.times", "time") .leftJoinAndSelect("task.times", "time")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("time.admin", "timeAdmin") .leftJoinAndSelect("time.admin", "timeAdmin")
.leftJoinAndSelect("task.comments", "comment")
.leftJoinAndSelect("comment.user", "commentUser")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
@@ -44,6 +47,12 @@ export class TaskRepository extends Repository<TMTask> {
"user.id", "user.id",
"user.firstName", "user.firstName",
"user.lastName", "user.lastName",
"user.profilePic",
"ticket.id",
"ticket.subject",
"ticket.numericId",
"ticket.status",
"time.id", "time.id",
"time.startedAt", "time.startedAt",
@@ -53,6 +62,14 @@ export class TaskRepository extends Repository<TMTask> {
"timeAdmin.id", "timeAdmin.id",
"timeAdmin.firstName", "timeAdmin.firstName",
"timeAdmin.lastName", "timeAdmin.lastName",
"comment.id",
"comment.content",
"comment.attachments",
"comment.createdAt",
"commentUser.firstName",
"commentUser.lastName",
"commentUser.profilePic",
]) ])
.where("task.id = :taskId", { taskId }) .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. // response needs. IN (:...ids) doesn't preserve order, so re-order to match seekQuery.
const detailRows = await this.createQueryBuilder("task") const detailRows = await this.createQueryBuilder("task")
.leftJoinAndSelect("task.remarks", "remark") .leftJoinAndSelect("task.remarks", "remark")
.leftJoinAndSelect("task.ticket", "ticket")
.leftJoinAndSelect("task.users", "user")
.loadRelationCountAndMap("task.attachmentCount", "task.attachments") .loadRelationCountAndMap("task.attachmentCount", "task.attachments")
.loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems") .loadRelationCountAndMap("task.checkListItemCount", "task.checkListItems")
.loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) => .loadRelationCountAndMap("task.completedCheckListItemCount", "task.checkListItems", "completedCheckListItems", (qb) =>
qb.where("completedCheckListItems.isDone = :isDone", { isDone: true }), 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 }) .where("task.id IN (:...ids)", { ids })
.getMany(); .getMany();
@@ -9,10 +9,12 @@ import { TMRemark } from "./entities/remark.entity";
import { TMAttachment } from "./entities/attachment.entity"; import { TMAttachment } from "./entities/attachment.entity";
import { TMCheckListItem } from "./entities/check-list-item.entity"; import { TMCheckListItem } from "./entities/check-list-item.entity";
import { TMTime } from "./entities/time.entity"; import { TMTime } from "./entities/time.entity";
import { TMTaskComment } from "./entities/task-comment.entity";
import { WorkspaceRepository } from "./repositories/workspace.repository"; import { WorkspaceRepository } from "./repositories/workspace.repository";
import { WorkspaceController } from "./controllers/workspace.controller"; import { WorkspaceController } from "./controllers/workspace.controller";
import { WorkspaceService } from "./providers/workspace.service"; import { WorkspaceService } from "./providers/workspace.service";
import { User } from "../users/entities/user.entity"; import { User } from "../users/entities/user.entity";
import { Ticket } from "../tickets/entities/ticket.entity";
import { ProjectController } from "./controllers/project.controller"; import { ProjectController } from "./controllers/project.controller";
import { ProjectRepository } from "./repositories/project.repository"; import { ProjectRepository } from "./repositories/project.repository";
import { ProjectService } from "./providers/project.service"; import { ProjectService } from "./providers/project.service";
@@ -34,10 +36,13 @@ import { AttachmentService } from "./providers/attachment.service";
import { TimeController } from "./controllers/time.controller"; import { TimeController } from "./controllers/time.controller";
import { TimeRepository } from "./repositories/time.repository"; import { TimeRepository } from "./repositories/time.repository";
import { TimeService } from "./providers/time.service"; 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({ @Module({
imports: [ 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: [ controllers: [
WorkspaceController, WorkspaceController,
@@ -48,6 +53,7 @@ import { TimeService } from "./providers/time.service";
RemarkController, RemarkController,
AttachmentController, AttachmentController,
TimeController, TimeController,
TaskCommentController,
], ],
providers: [ providers: [
WorkspaceRepository, WorkspaceRepository,
@@ -66,6 +72,8 @@ import { TimeService } from "./providers/time.service";
AttachmentService, AttachmentService,
TimeRepository, TimeRepository,
TimeService, TimeService,
] TaskCommentRepository,
TaskCommentService,
],
}) })
export class TaskManagerModule {} export class TaskManagerModule {}
@@ -33,6 +33,7 @@ import { TicketStatus } from "../enums/ticket-status.enum";
import { TicketCategoryRepository } from "../repositories/tickets-category.repository"; import { TicketCategoryRepository } from "../repositories/tickets-category.repository";
import { TicketMessagesRepository } from "../repositories/tickets-message.repository"; import { TicketMessagesRepository } from "../repositories/tickets-message.repository";
import { TicketsRepository } from "../repositories/tickets.repository"; import { TicketsRepository } from "../repositories/tickets.repository";
import { PermissionEnum } from "../../users/enums/permission.enum";
@Injectable() @Injectable()
export class TicketsService implements OnModuleInit { export class TicketsService implements OnModuleInit {
@@ -326,10 +327,10 @@ export class TicketsService implements OnModuleInit {
return { tickets, count, paginate: true }; return { tickets, count, paginate: true };
} }
//******************************** */ //******************************** */
async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminId: string) { async getTicketForAdmin(queryDto: SearchTicketQueryDto, adminPermissions: PermissionEnum[],adminId: string) {
const isSuperAdmin = await this.usersService.isSuperAdmin(adminId); 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 }; 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 { IUserIpAndHeaders, UserDec, UserIpAndHeaders } from "../../common/decorators/user.decorator";
import { ParamDto } from "../../common/DTO/param.dto"; import { ParamDto } from "../../common/DTO/param.dto";
import { PermissionEnum } from "../users/enums/permission.enum"; import { PermissionEnum } from "../users/enums/permission.enum";
import { ITokenPayload } from "../auth/interfaces/IToken-payload";
@Controller("tickets") @Controller("tickets")
@ApiTags("Tickets") @ApiTags("Tickets")
@AuthGuards() @AuthGuards()
@@ -24,14 +25,14 @@ export class TicketsController {
//----------------- ticket categories ------------------- //----------------- ticket categories -------------------
@ApiOperation({ summary: "Create ticket category => admin route" }) @ApiOperation({ summary: "Create ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_CREATE) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_CREATE)
@Post("category") @Post("category")
createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { createTicketCategory(@Body() createDto: CreateTicketCategoryDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders); return this.ticketsService.createTicketCategory(createDto, userIpAndHeaders);
} }
@ApiOperation({ summary: "Update ticket category => admin route" }) @ApiOperation({ summary: "Update ticket category => admin route" })
@PermissionsDec(PermissionEnum.TICKETS_UPDATE) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Patch("category/:id") @Patch("category/:id")
updateCategory( updateCategory(
@Param() paramDto: ParamDto, @Param() paramDto: ParamDto,
@@ -49,7 +50,7 @@ export class TicketsController {
@ApiOperation({ summary: "Get ticket categories list => admin route" }) @ApiOperation({ summary: "Get ticket categories list => admin route" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_READ)
@Get("categories-list") @Get("categories-list")
getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) { getCategoriesList(@Query() queryDto: SearchTicketCategoryDto) {
return this.ticketsService.getCategoriesList(queryDto); return this.ticketsService.getCategoriesList(queryDto);
@@ -58,7 +59,7 @@ export class TicketsController {
@ApiOperation({ summary: "toggle status of categories => admin route" }) @ApiOperation({ summary: "toggle status of categories => admin route" })
@AdminRoute() @AdminRoute()
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@PermissionsDec(PermissionEnum.TICKETS_UPDATE) @PermissionsDec(PermissionEnum.TICKET_CATEGORIES_UPDATE)
@Post("category/toggle-status/:id") @Post("category/toggle-status/:id")
toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) { toggleCategoryStatus(@Param() paramDto: ParamDto, @UserIpAndHeaders() userIpAndHeaders: IUserIpAndHeaders) {
return this.ticketsService.toggleCategoryStatus(paramDto, userIpAndHeaders); return this.ticketsService.toggleCategoryStatus(paramDto, userIpAndHeaders);
@@ -88,10 +89,9 @@ export class TicketsController {
@ApiOperation({ summary: "Get all tickets ==> admin route" }) @ApiOperation({ summary: "Get all tickets ==> admin route" })
@AdminRoute() @AdminRoute()
@PermissionsDec(PermissionEnum.TICKETS_READ)
@Get("admin") @Get("admin")
getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec("id") adminId: string) { getAllTickets(@Query() queryDto: SearchTicketQueryDto, @UserDec() admin: ITokenPayload) {
return this.ticketsService.getTicketForAdmin(queryDto, adminId); return this.ticketsService.getTicketForAdmin(queryDto, admin.permissions, admin.id);
} }
@ApiOperation({ summary: "create ticket messages" }) @ApiOperation({ summary: "create ticket messages" })
+6 -4
View File
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; 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"; import { AdminMessage, AuthMessage } from "../../../common/enums/message.enum";
@@ -22,9 +22,11 @@ export class CreateAdminDto {
phone: string; phone: string;
@IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED }) @IsNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
@IsUUID("4", { message: AdminMessage.ROLE_ID_SHOULD_BE_UUID }) @IsArray({ message: AdminMessage.ROLE_REQUIRED })
@ApiProperty({ description: "Role id", example: "123e4567-e89b-12d3-a456-426614174000" }) @ArrayNotEmpty({ message: AdminMessage.ROLE_REQUIRED })
roleId: string; @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 }) @IsNotEmpty({ message: AuthMessage.FIRST_NAME_NOT_EMPTY })
@IsString({ 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_UPDATE]: { title: "ویرایش تیکت‌ها", group: PermissionGroupEnum.SUPPORT },
[PermissionEnum.TICKETS_DELETE]: { 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_CREATE]: { title: "ایجاد انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_READ]: { title: "مشاهده انتقادات", group: PermissionGroupEnum.OTHER }, [PermissionEnum.CRITICISMS_READ]: { title: "مشاهده انتقادات", group: PermissionGroupEnum.OTHER },
[PermissionEnum.CRITICISMS_UPDATE]: { title: "ویرایش انتقادات", group: PermissionGroupEnum.OTHER }, [PermissionEnum.CRITICISMS_UPDATE]: { title: "ویرایش انتقادات", group: PermissionGroupEnum.OTHER },
@@ -53,6 +53,12 @@ export enum PermissionEnum {
TICKETS_UPDATE = "tickets_update", TICKETS_UPDATE = "tickets_update",
TICKETS_DELETE = "tickets_delete", 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
CRITICISMS_CREATE = "criticisms_create", CRITICISMS_CREATE = "criticisms_create",
CRITICISMS_READ = "criticisms_read", 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 { PermissionsRepository } from "../repositories/permissions.repository";
import { RoleRepository } from "../repositories/roles.repository"; import { RoleRepository } from "../repositories/roles.repository";
import { UserRepository } from "../repositories/users.repository"; import { UserRepository } from "../repositories/users.repository";
import { getPermissionEntitiesFromRoles } from "../utils/role-permissions.util";
@Injectable() @Injectable()
export class AdminsService { export class AdminsService {
@@ -43,22 +44,22 @@ export class AdminsService {
const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone }); const existPhone = await queryRunner.manager.findOneBy(this.userRepository.target, { phone: createDto.phone });
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
const role = await queryRunner.manager.findOne(this.rolesRepository.target, { const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: createDto.roleId }, where: { id: In(createDto.roleIds), isAdmin: true },
relations: { permissions: 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); if (createDto.password !== createDto.repeatPassword) throw new BadRequestException(AdminMessage.PASSWORD_NOT_MATCH);
const hashedPassword = await this.passwordService.hashPassword(createDto.password); const hashedPassword = await this.passwordService.hashPassword(createDto.password);
const userName = slugify(`${createDto.firstName} ${Date.now().toString().slice(-5)}`, { lower: true, trim: true }); 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, { const adminUser = queryRunner.manager.create(this.userRepository.target, {
...createDto, ...adminData,
roles: [role], roles,
password: hashedPassword, password: hashedPassword,
userName, userName,
}); });
@@ -75,7 +76,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"], userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin created new admin: ${createDto.email}`, actionDescription: `Admin created new admin: ${createDto.email}`,
metadata: { metadata: {
roleId: createDto.roleId, roleIds: createDto.roleIds,
createdBy: "admin", createdBy: "admin",
email: createDto.email, email: createDto.email,
phone: createDto.phone, phone: createDto.phone,
@@ -118,16 +119,14 @@ export class AdminsService {
if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST); if (existPhone) throw new BadRequestException(UserMessage.PHONE_EXIST);
} }
if (updateDto.roleId) { if (updateDto.roleIds?.length) {
const role = await queryRunner.manager.findOne(this.rolesRepository.target, { const roles = await queryRunner.manager.find(this.rolesRepository.target, {
where: { id: updateDto.roleId }, where: { id: In(updateDto.roleIds), isAdmin: true },
relations: { permissions: 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 = roles;
admin.roles = [role];
await queryRunner.manager.save(this.userRepository.target, admin); await queryRunner.manager.save(this.userRepository.target, admin);
} }
@@ -138,7 +137,8 @@ export class AdminsService {
admin.password = await this.passwordService.hashPassword(updateDto.password); 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 queryRunner.commitTransaction();
await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, { await this.accessLogService.logUpdate("User", admin.id, UserType.ADMIN, {
@@ -150,7 +150,7 @@ export class AdminsService {
userType: UserType.ADMIN, userType: UserType.ADMIN,
actionDescription: `Admin updated admin: ${admin.email}`, actionDescription: `Admin updated admin: ${admin.email}`,
metadata: { metadata: {
roleId: updateDto.roleId, roleIds: updateDto.roleIds,
}, },
oldValues: JSON.stringify({ ...admin }), oldValues: JSON.stringify({ ...admin }),
newValues: JSON.stringify({ ...updateDto }), newValues: JSON.stringify({ ...updateDto }),
@@ -169,7 +169,7 @@ export class AdminsService {
/************************************************************ */ /************************************************************ */
async deleteAdmin(id: string, userIpAndHeaders: IUserIpAndHeaders) { 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); if (!admin) throw new BadRequestException(AdminMessage.ADMIN_NOT_FOUND);
await this.userRepository.softDelete(id); await this.userRepository.softDelete(id);
@@ -182,7 +182,7 @@ export class AdminsService {
userAgent: userIpAndHeaders.headers["user-agent"], userAgent: userIpAndHeaders.headers["user-agent"],
actionDescription: `Admin deleted admin: ${admin.email}`, actionDescription: `Admin deleted admin: ${admin.email}`,
metadata: { metadata: {
roleId: admin.roles[0].id, roleIds: admin.roles.map((role) => role.id),
email: admin.email, email: admin.email,
phone: admin.phone, phone: admin.phone,
}, },
@@ -303,7 +303,7 @@ export class AdminsService {
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
return { 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 { DataSource, Not } from "typeorm";
import { IUserIpAndHeaders } from "../../../common/decorators/user.decorator"; 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 { UserType } from "../../access-logs/enums/user-type.enum";
import { AccessLogService } from "../../access-logs/providers/access-log.service"; import { AccessLogService } from "../../access-logs/providers/access-log.service";
import { Address } from "../../address/entities/address.entity"; 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 }); await queryRunner.manager.update(this.userRepository.target, { id: customerId }, { password: hashPassword });
} }
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, { const hasLegalUpdateData = Boolean(
where: { user: { id: customerId } }, registrationName || economicCode || nationalIdentity || registrationCode || postalCode || address || cityId,
relations: { address: true }, );
});
if (!legalUser) throw new BadRequestException(UserMessage.LEGAL_DATA_NOT_FOUND); if (hasLegalUpdateData) {
const legalUser = await queryRunner.manager.findOne(this.legalUserRepository.target, {
if (economicCode) { where: { user: { id: customerId } },
const existingEconomicCode = await queryRunner.manager.findOne(this.legalUserRepository.target, { relations: { address: true },
where: { economicCode, id: Not(legalUser.id) },
}); });
if (existingEconomicCode) throw new BadRequestException(UserMessage.ECONOMIC_CODE_EXIST);
}
if (registrationCode) { if (!legalUser) {
const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, { if (user.financialType === FinancialType.REAL) {
where: { registrationCode, id: Not(legalUser.id) }, throw new BadRequestException(UserMessage.FINANCIAL_TYPE_IS_REAL);
}); }
if (existingRegistrationCode) throw new BadRequestException(UserMessage.REGISTRATION_CODE_EXIST);
}
if (nationalIdentity) { const legalPhone = phone ?? user.phone;
const existingNationalIdentity = await queryRunner.manager.findOne(this.legalUserRepository.target, {
where: { nationalIdentity, id: Not(legalUser.id) },
});
if (existingNationalIdentity) throw new BadRequestException(UserMessage.NATIONAL_IDENTITY_EXIST);
}
if (registrationName) { if (!economicCode) throw new BadRequestException(FinancialMessage.ECONOMIC_CODE_REQUIRED);
const existingRegistrationName = await queryRunner.manager.findOne(this.legalUserRepository.target, { if (!registrationCode) throw new BadRequestException(FinancialMessage.REGISTRATION_CODE_REQUIRED);
where: { registrationName, id: Not(legalUser.id) }, if (!registrationName) throw new BadRequestException(FinancialMessage.REGISTRATION_NAME_REQUIRED);
}); if (!nationalIdentity) throw new BadRequestException(FinancialMessage.NATIONAL_ID_REQUIRED);
if (existingRegistrationName) throw new BadRequestException(UserMessage.REGISTRATION_NAME_EXIST); 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 (registrationCode) {
if (cityId || address || postalCode) { const existingRegistrationCode = await queryRunner.manager.findOne(this.legalUserRepository.target, {
// Get the address entity where: { registrationCode, id: Not(legalUser.id) },
const addressEntity = legalUser.address; });
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 if (registrationName) {
const addressUpdates: Record<string, unknown> = {}; 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) { await queryRunner.manager.update(this.legalUserRepository.target, { id: legalUser.id }, legalUserUpdateData);
const { city } = await this.addressService.getCity(+cityId);
addressUpdates.city = city; 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(); await queryRunner.commitTransaction();
@@ -359,6 +359,7 @@ export class UsersService {
async isSuperAdmin(adminId: string) { async isSuperAdmin(adminId: string) {
const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } }); const user = await this.userRepository.findOne({ where: { id: adminId }, relations: { roles: true } });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); 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); 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"); .loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) { if (queryDto.q) {
queryBuilder queryBuilder.andWhere(
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) `(
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) user.firstName ILIKE :search
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` }); 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(); return queryBuilder.getMany();
@@ -166,13 +177,28 @@ export class UserRepository extends Repository<User> {
.loadRelationCountAndMap("user.ticketsCount", "user.tickets"); .loadRelationCountAndMap("user.ticketsCount", "user.tickets");
if (queryDto.q) { if (queryDto.q) {
queryBuilder queryBuilder.andWhere(
.orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) `(
.orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) user.firstName ILIKE :search
.orWhere("user.userName ILIKE :search", { search: `%${queryDto.q}%` }); 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"
}
}