end fase 2
This commit is contained in:
@ -5,6 +5,8 @@ import { APP_GUARD } from '@nestjs/core';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { IdeasModule } from './ideas/ideas.module';
|
||||
import { CommentsModule } from './comments/comments.module';
|
||||
import { TeamModule } from './team/team.module';
|
||||
import { AuthModule, JwtAuthGuard } from './auth';
|
||||
|
||||
@Module({
|
||||
@ -30,6 +32,8 @@ import { AuthModule, JwtAuthGuard } from './auth';
|
||||
}),
|
||||
AuthModule,
|
||||
IdeasModule,
|
||||
CommentsModule,
|
||||
TeamModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
37
backend/src/comments/comments.controller.ts
Normal file
37
backend/src/comments/comments.controller.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { CommentsService } from './comments.service';
|
||||
import { CreateCommentDto } from './dto';
|
||||
|
||||
@Controller('api')
|
||||
export class CommentsController {
|
||||
constructor(private readonly commentsService: CommentsService) {}
|
||||
|
||||
@Get('ideas/:ideaId/comments')
|
||||
findByIdeaId(@Param('ideaId', ParseUUIDPipe) ideaId: string) {
|
||||
return this.commentsService.findByIdeaId(ideaId);
|
||||
}
|
||||
|
||||
@Post('ideas/:ideaId/comments')
|
||||
create(
|
||||
@Param('ideaId', ParseUUIDPipe) ideaId: string,
|
||||
@Body() createCommentDto: CreateCommentDto,
|
||||
) {
|
||||
return this.commentsService.create(ideaId, createCommentDto);
|
||||
}
|
||||
|
||||
@Delete('comments/:id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.commentsService.remove(id);
|
||||
}
|
||||
}
|
||||
13
backend/src/comments/comments.module.ts
Normal file
13
backend/src/comments/comments.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Comment } from './entities/comment.entity';
|
||||
import { CommentsService } from './comments.service';
|
||||
import { CommentsController } from './comments.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Comment])],
|
||||
controllers: [CommentsController],
|
||||
providers: [CommentsService],
|
||||
exports: [CommentsService],
|
||||
})
|
||||
export class CommentsModule {}
|
||||
36
backend/src/comments/comments.service.ts
Normal file
36
backend/src/comments/comments.service.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Comment } from './entities/comment.entity';
|
||||
import { CreateCommentDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class CommentsService {
|
||||
constructor(
|
||||
@InjectRepository(Comment)
|
||||
private readonly commentsRepository: Repository<Comment>,
|
||||
) {}
|
||||
|
||||
async findByIdeaId(ideaId: string): Promise<Comment[]> {
|
||||
return this.commentsRepository.find({
|
||||
where: { ideaId },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async create(ideaId: string, createCommentDto: CreateCommentDto): Promise<Comment> {
|
||||
const comment = this.commentsRepository.create({
|
||||
...createCommentDto,
|
||||
ideaId,
|
||||
});
|
||||
return this.commentsRepository.save(comment);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const comment = await this.commentsRepository.findOne({ where: { id } });
|
||||
if (!comment) {
|
||||
throw new NotFoundException(`Comment with ID "${id}" not found`);
|
||||
}
|
||||
await this.commentsRepository.remove(comment);
|
||||
}
|
||||
}
|
||||
12
backend/src/comments/dto/create-comment.dto.ts
Normal file
12
backend/src/comments/dto/create-comment.dto.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreateCommentDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
text: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
author?: string;
|
||||
}
|
||||
1
backend/src/comments/dto/index.ts
Normal file
1
backend/src/comments/dto/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './create-comment.dto';
|
||||
35
backend/src/comments/entities/comment.entity.ts
Normal file
35
backend/src/comments/entities/comment.entity.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Idea } from '../../ideas/entities/idea.entity';
|
||||
|
||||
@Entity('comments')
|
||||
export class Comment {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
text: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
author: string | null;
|
||||
|
||||
@Column({ name: 'idea_id', type: 'uuid' })
|
||||
ideaId: string;
|
||||
|
||||
@ManyToOne(() => Idea, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'idea_id' })
|
||||
idea: Idea;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
5
backend/src/comments/index.ts
Normal file
5
backend/src/comments/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './comments.module';
|
||||
export * from './comments.service';
|
||||
export * from './comments.controller';
|
||||
export * from './entities/comment.entity';
|
||||
export * from './dto';
|
||||
@ -18,6 +18,10 @@ export class QueryIdeasDto {
|
||||
@IsString()
|
||||
search?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
color?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sortBy?: string;
|
||||
|
||||
@ -26,6 +26,7 @@ export class IdeasService {
|
||||
priority,
|
||||
module,
|
||||
search,
|
||||
color,
|
||||
sortBy = 'order',
|
||||
sortOrder = 'ASC',
|
||||
page = 1,
|
||||
@ -60,6 +61,10 @@ export class IdeasService {
|
||||
queryBuilder.andWhere('idea.module = :module', { module });
|
||||
}
|
||||
|
||||
if (color) {
|
||||
queryBuilder.andWhere('idea.color = :color', { color });
|
||||
}
|
||||
|
||||
if (search) {
|
||||
queryBuilder.andWhere(
|
||||
'(idea.title ILIKE :search OR idea.description ILIKE :search)',
|
||||
|
||||
26
backend/src/migrations/1736899200000-CreateCommentsTable.ts
Normal file
26
backend/src/migrations/1736899200000-CreateCommentsTable.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateCommentsTable1736899200000 implements MigrationInterface {
|
||||
name = 'CreateCommentsTable1736899200000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "comments" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"text" text NOT NULL,
|
||||
"author" character varying(255),
|
||||
"idea_id" uuid NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_comments_id" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "FK_comments_idea_id" FOREIGN KEY ("idea_id") REFERENCES "ideas"("id") ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_comments_idea_id" ON "comments" ("idea_id")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_comments_idea_id"`);
|
||||
await queryRunner.query(`DROP TABLE "comments"`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateTeamMembersTable1736899300000 implements MigrationInterface {
|
||||
name = 'CreateTeamMembersTable1736899300000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "public"."team_members_role_enum" AS ENUM('backend', 'frontend', 'ai_ml', 'devops', 'qa', 'ui_ux', 'pm')`,
|
||||
);
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "team_members" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(255) NOT NULL,
|
||||
"role" "public"."team_members_role_enum" NOT NULL,
|
||||
"productivity" jsonb NOT NULL DEFAULT '{"trivial": 1, "simple": 4, "medium": 12, "complex": 32, "veryComplex": 60}',
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_team_members_id" PRIMARY KEY ("id")
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "team_members"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."team_members_role_enum"`);
|
||||
}
|
||||
}
|
||||
93
backend/src/migrations/1736899400000-CreateRolesTable.ts
Normal file
93
backend/src/migrations/1736899400000-CreateRolesTable.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateRolesTable1736899400000 implements MigrationInterface {
|
||||
name = 'CreateRolesTable1736899400000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. Создаём таблицу roles
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE "roles" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(100) NOT NULL,
|
||||
"label" character varying(255) NOT NULL,
|
||||
"sortOrder" integer NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_roles_id" PRIMARY KEY ("id"),
|
||||
CONSTRAINT "UQ_roles_name" UNIQUE ("name")
|
||||
)
|
||||
`);
|
||||
|
||||
// 2. Добавляем начальные роли (из старого enum)
|
||||
await queryRunner.query(`
|
||||
INSERT INTO "roles" ("name", "label", "sortOrder") VALUES
|
||||
('backend', 'Backend-разработчик', 0),
|
||||
('frontend', 'Frontend-разработчик', 1),
|
||||
('ai_ml', 'AI/ML-инженер', 2),
|
||||
('devops', 'DevOps-инженер', 3),
|
||||
('qa', 'QA-инженер', 4),
|
||||
('ui_ux', 'UI/UX-дизайнер', 5),
|
||||
('pm', 'Project Manager', 6)
|
||||
`);
|
||||
|
||||
// 3. Добавляем колонку role_id в team_members (nullable сначала)
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "team_members" ADD COLUMN "role_id" uuid
|
||||
`);
|
||||
|
||||
// 4. Мигрируем данные: связываем team_members с roles по name
|
||||
await queryRunner.query(`
|
||||
UPDATE "team_members" tm
|
||||
SET "role_id" = r."id"
|
||||
FROM "roles" r
|
||||
WHERE tm."role"::text = r."name"
|
||||
`);
|
||||
|
||||
// 5. Делаем role_id NOT NULL
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "team_members" ALTER COLUMN "role_id" SET NOT NULL
|
||||
`);
|
||||
|
||||
// 6. Добавляем foreign key
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "team_members"
|
||||
ADD CONSTRAINT "FK_team_members_role" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT
|
||||
`);
|
||||
|
||||
// 7. Удаляем старую колонку role и enum
|
||||
await queryRunner.query(`ALTER TABLE "team_members" DROP COLUMN "role"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."team_members_role_enum"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. Восстанавливаем enum
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "public"."team_members_role_enum" AS ENUM('backend', 'frontend', 'ai_ml', 'devops', 'qa', 'ui_ux', 'pm')`,
|
||||
);
|
||||
|
||||
// 2. Добавляем колонку role
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "team_members" ADD COLUMN "role" "public"."team_members_role_enum"
|
||||
`);
|
||||
|
||||
// 3. Мигрируем данные обратно
|
||||
await queryRunner.query(`
|
||||
UPDATE "team_members" tm
|
||||
SET "role" = r."name"::"public"."team_members_role_enum"
|
||||
FROM "roles" r
|
||||
WHERE tm."role_id" = r."id"
|
||||
`);
|
||||
|
||||
// 4. Делаем role NOT NULL
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE "team_members" ALTER COLUMN "role" SET NOT NULL
|
||||
`);
|
||||
|
||||
// 5. Удаляем foreign key и role_id
|
||||
await queryRunner.query(`ALTER TABLE "team_members" DROP CONSTRAINT "FK_team_members_role"`);
|
||||
await queryRunner.query(`ALTER TABLE "team_members" DROP COLUMN "role_id"`);
|
||||
|
||||
// 6. Удаляем таблицу roles
|
||||
await queryRunner.query(`DROP TABLE "roles"`);
|
||||
}
|
||||
}
|
||||
16
backend/src/team/dto/create-role.dto.ts
Normal file
16
backend/src/team/dto/create-role.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { IsString, IsNotEmpty, IsOptional, IsInt, Min } from 'class-validator';
|
||||
|
||||
export class CreateRoleDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
label: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
sortOrder?: number;
|
||||
}
|
||||
48
backend/src/team/dto/create-team-member.dto.ts
Normal file
48
backend/src/team/dto/create-team-member.dto.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsUUID,
|
||||
IsOptional,
|
||||
IsObject,
|
||||
IsNumber,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class ProductivityMatrixDto {
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
trivial: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
simple: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
medium: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
complex: number;
|
||||
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
veryComplex: number;
|
||||
}
|
||||
|
||||
export class CreateTeamMemberDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@IsUUID()
|
||||
roleId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
@Type(() => ProductivityMatrixDto)
|
||||
productivity?: ProductivityMatrixDto;
|
||||
}
|
||||
4
backend/src/team/dto/index.ts
Normal file
4
backend/src/team/dto/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './create-team-member.dto';
|
||||
export * from './update-team-member.dto';
|
||||
export * from './create-role.dto';
|
||||
export * from './update-role.dto';
|
||||
4
backend/src/team/dto/update-role.dto.ts
Normal file
4
backend/src/team/dto/update-role.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateRoleDto } from './create-role.dto';
|
||||
|
||||
export class UpdateRoleDto extends PartialType(CreateRoleDto) {}
|
||||
4
backend/src/team/dto/update-team-member.dto.ts
Normal file
4
backend/src/team/dto/update-team-member.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateTeamMemberDto } from './create-team-member.dto';
|
||||
|
||||
export class UpdateTeamMemberDto extends PartialType(CreateTeamMemberDto) {}
|
||||
33
backend/src/team/entities/role.entity.ts
Normal file
33
backend/src/team/entities/role.entity.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { TeamMember } from './team-member.entity';
|
||||
|
||||
@Entity('roles')
|
||||
export class Role {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
label: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
sortOrder: number;
|
||||
|
||||
@OneToMany(() => TeamMember, (member) => member.role)
|
||||
members: TeamMember[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
44
backend/src/team/entities/team-member.entity.ts
Normal file
44
backend/src/team/entities/team-member.entity.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Role } from './role.entity';
|
||||
|
||||
// Матрица производительности: время в часах на задачи разной сложности
|
||||
export interface ProductivityMatrix {
|
||||
trivial: number; // < 1 часа
|
||||
simple: number; // 1-4 часа
|
||||
medium: number; // 4-16 часов
|
||||
complex: number; // 16-40 часов
|
||||
veryComplex: number; // > 40 часов
|
||||
}
|
||||
|
||||
@Entity('team_members')
|
||||
export class TeamMember {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
name: string;
|
||||
|
||||
@ManyToOne(() => Role, (role) => role.members, { eager: true })
|
||||
@JoinColumn({ name: 'role_id' })
|
||||
role: Role;
|
||||
|
||||
@Column({ name: 'role_id', type: 'uuid' })
|
||||
roleId: string;
|
||||
|
||||
@Column({ type: 'jsonb', default: { trivial: 1, simple: 4, medium: 12, complex: 32, veryComplex: 60 } })
|
||||
productivity: ProductivityMatrix;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
5
backend/src/team/index.ts
Normal file
5
backend/src/team/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './team.module';
|
||||
export * from './team.service';
|
||||
export * from './team.controller';
|
||||
export * from './entities/team-member.entity';
|
||||
export * from './dto';
|
||||
49
backend/src/team/roles.controller.ts
Normal file
49
backend/src/team/roles.controller.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { RolesService } from './roles.service';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||
|
||||
@Controller('api/roles')
|
||||
export class RolesController {
|
||||
constructor(private readonly rolesService: RolesService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.rolesService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.rolesService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() createDto: CreateRoleDto) {
|
||||
return this.rolesService.create(createDto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() updateDto: UpdateRoleDto,
|
||||
) {
|
||||
return this.rolesService.update(id, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.rolesService.remove(id);
|
||||
}
|
||||
}
|
||||
69
backend/src/team/roles.service.ts
Normal file
69
backend/src/team/roles.service.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||
|
||||
@Injectable()
|
||||
export class RolesService {
|
||||
constructor(
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<Role[]> {
|
||||
return this.roleRepository.find({
|
||||
order: { sortOrder: 'ASC', name: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<Role> {
|
||||
const role = await this.roleRepository.findOne({ where: { id } });
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role with ID "${id}" not found`);
|
||||
}
|
||||
return role;
|
||||
}
|
||||
|
||||
async create(createDto: CreateRoleDto): Promise<Role> {
|
||||
const existing = await this.roleRepository.findOne({
|
||||
where: { name: createDto.name },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(`Role with name "${createDto.name}" already exists`);
|
||||
}
|
||||
|
||||
const maxSortOrder = await this.roleRepository
|
||||
.createQueryBuilder('role')
|
||||
.select('MAX(role.sortOrder)', 'max')
|
||||
.getRawOne<{ max: number | null }>();
|
||||
|
||||
const role = this.roleRepository.create({
|
||||
...createDto,
|
||||
sortOrder: createDto.sortOrder ?? (maxSortOrder?.max ?? -1) + 1,
|
||||
});
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
async update(id: string, updateDto: UpdateRoleDto): Promise<Role> {
|
||||
const role = await this.findOne(id);
|
||||
|
||||
if (updateDto.name && updateDto.name !== role.name) {
|
||||
const existing = await this.roleRepository.findOne({
|
||||
where: { name: updateDto.name },
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException(`Role with name "${updateDto.name}" already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(role, updateDto);
|
||||
return this.roleRepository.save(role);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const role = await this.findOne(id);
|
||||
await this.roleRepository.remove(role);
|
||||
}
|
||||
}
|
||||
53
backend/src/team/team.controller.ts
Normal file
53
backend/src/team/team.controller.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
ParseUUIDPipe,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { TeamService } from './team.service';
|
||||
import { CreateTeamMemberDto, UpdateTeamMemberDto } from './dto';
|
||||
|
||||
@Controller('api/team')
|
||||
export class TeamController {
|
||||
constructor(private readonly teamService: TeamService) {}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.teamService.findAll();
|
||||
}
|
||||
|
||||
@Get('summary')
|
||||
getSummary() {
|
||||
return this.teamService.getSummary();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.teamService.findOne(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() createDto: CreateTeamMemberDto) {
|
||||
return this.teamService.create(createDto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() updateDto: UpdateTeamMemberDto,
|
||||
) {
|
||||
return this.teamService.update(id, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.teamService.remove(id);
|
||||
}
|
||||
}
|
||||
16
backend/src/team/team.module.ts
Normal file
16
backend/src/team/team.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TeamMember } from './entities/team-member.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { TeamService } from './team.service';
|
||||
import { TeamController } from './team.controller';
|
||||
import { RolesService } from './roles.service';
|
||||
import { RolesController } from './roles.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([TeamMember, Role])],
|
||||
controllers: [TeamController, RolesController],
|
||||
providers: [TeamService, RolesService],
|
||||
exports: [TeamService, RolesService],
|
||||
})
|
||||
export class TeamModule {}
|
||||
93
backend/src/team/team.service.ts
Normal file
93
backend/src/team/team.service.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { TeamMember } from './entities/team-member.entity';
|
||||
import { Role } from './entities/role.entity';
|
||||
import { CreateTeamMemberDto } from './dto/create-team-member.dto';
|
||||
import { UpdateTeamMemberDto } from './dto/update-team-member.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TeamService {
|
||||
constructor(
|
||||
@InjectRepository(TeamMember)
|
||||
private readonly teamRepository: Repository<TeamMember>,
|
||||
@InjectRepository(Role)
|
||||
private readonly roleRepository: Repository<Role>,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<TeamMember[]> {
|
||||
return this.teamRepository.find({
|
||||
order: { role: { sortOrder: 'ASC' }, name: 'ASC' },
|
||||
relations: ['role'],
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string): Promise<TeamMember> {
|
||||
const member = await this.teamRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['role'],
|
||||
});
|
||||
if (!member) {
|
||||
throw new NotFoundException(`Team member with ID "${id}" not found`);
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
async create(createDto: CreateTeamMemberDto): Promise<TeamMember> {
|
||||
// Проверяем что роль существует
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id: createDto.roleId },
|
||||
});
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role with ID "${createDto.roleId}" not found`);
|
||||
}
|
||||
|
||||
const member = this.teamRepository.create(createDto);
|
||||
const saved = await this.teamRepository.save(member);
|
||||
return this.findOne(saved.id);
|
||||
}
|
||||
|
||||
async update(id: string, updateDto: UpdateTeamMemberDto): Promise<TeamMember> {
|
||||
const member = await this.findOne(id);
|
||||
|
||||
if (updateDto.roleId) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: { id: updateDto.roleId },
|
||||
});
|
||||
if (!role) {
|
||||
throw new NotFoundException(`Role with ID "${updateDto.roleId}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(member, updateDto);
|
||||
await this.teamRepository.save(member);
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const member = await this.findOne(id);
|
||||
await this.teamRepository.remove(member);
|
||||
}
|
||||
|
||||
async getSummary(): Promise<{ roleId: string; label: string; count: number }[]> {
|
||||
// Получаем все роли
|
||||
const roles = await this.roleRepository.find({
|
||||
order: { sortOrder: 'ASC' },
|
||||
});
|
||||
|
||||
// Получаем количество участников по ролям
|
||||
const result = await this.teamRepository
|
||||
.createQueryBuilder('member')
|
||||
.select('member.role_id', 'roleId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('member.role_id')
|
||||
.getRawMany<{ roleId: string; count: string }>();
|
||||
|
||||
// Возвращаем все роли с количеством
|
||||
return roles.map((role) => ({
|
||||
roleId: role.id,
|
||||
label: role.label,
|
||||
count: parseInt(result.find((r) => r.roleId === role.id)?.count ?? '0', 10),
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user