[
  {
    "path": ".agents/skills/nestjs-best-practices/.github/workflows/branch-protection.yml",
    "content": "name: Branch Protection\n\non:\n  pull_request:\n    branches: [main]\n\njobs:\n  check-branch:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Block docs branch merge to main\n        if: github.head_ref == 'docs'\n        run: |\n          echo \"::error::Merging 'docs' branch into 'main' is not allowed.\"\n          echo \"\"\n          echo \"The 'docs' branch contains the documentation website which should\"\n          echo \"remain separate from the main skill files to keep installations lightweight.\"\n          echo \"\"\n          echo \"If you need to sync changes, cherry-pick specific commits instead.\"\n          exit 1\n\n      - name: Branch check passed\n        if: github.head_ref != 'docs'\n        run: echo \"Branch check passed - not merging from docs branch\"\n"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/.github/workflows/deploy.yml",
    "content": "name: Deploy to GitHub Pages\n\non:\n  push:\n    branches: [docs]\n    paths:\n      - 'website/**'\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: pages\n  cancel-in-progress: false\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: npm\n          cache-dependency-path: website/package-lock.json\n\n      - name: Setup Pages\n        uses: actions/configure-pages@v4\n\n      - name: Install dependencies\n        run: npm ci\n        working-directory: website\n\n      - name: Build\n        run: npm run build\n        working-directory: website\n\n      - name: Copy index.html to 404.html for SPA routing\n        run: cp website/dist/index.html website/dist/404.html\n\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: website/dist\n\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/.gitignore",
    "content": "# Dependencies\nnode_modules/\n\n# Website (lives on docs branch)\nwebsite/\n\n# Build outputs\ndist/\n*.js\n*.d.ts\n*.js.map\n\n# IDE\n.idea/\n.vscode/\n*.swp\n*.swo\n\n# OS\n.DS_Store\nThumbs.db\n\n# Logs\n*.log\nnpm-debug.log*\n\n# Environment\n.env\n.env.local\n"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/AGENTS.md",
    "content": "# NestJS Best Practices\n\n**Version 1.1.0**\nNestJS Best Practices\nJanuary 2026\n\n> **Note:**\n> This document is mainly for agents and LLMs to follow when maintaining,\n> generating, or refactoring NestJS codebases. Humans may also find it\n> useful, but guidance here is optimized for automation and consistency\n> by AI-assisted workflows.\n\n---\n\n## Abstract\n\nComprehensive best practices and architecture guide for NestJS applications, designed for AI agents and LLMs. Contains 40 rules across 10 categories, prioritized by impact from critical (architecture, dependency injection) to incremental (DevOps patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.\n\n---\n\n## Table of Contents\n\n1. [Architecture](#1-architecture) — **CRITICAL**\n   - 1.1 [Avoid Circular Dependencies](#11-avoid-circular-dependencies)\n   - 1.2 [Organize by Feature Modules](#12-organize-by-feature-modules)\n   - 1.3 [Use Proper Module Sharing Patterns](#13-use-proper-module-sharing-patterns)\n   - 1.4 [Single Responsibility for Services](#14-single-responsibility-for-services)\n   - 1.5 [Use Event-Driven Architecture for Decoupling](#15-use-event-driven-architecture-for-decoupling)\n   - 1.6 [Use Repository Pattern for Data Access](#16-use-repository-pattern-for-data-access)\n2. [Dependency Injection](#2-dependency-injection) — **CRITICAL**\n   - 2.1 [Avoid Service Locator Anti-Pattern](#21-avoid-service-locator-anti-pattern)\n   - 2.2 [Apply Interface Segregation Principle](#22-apply-interface-segregation-principle)\n   - 2.3 [Honor Liskov Substitution Principle](#23-honor-liskov-substitution-principle)\n   - 2.4 [Prefer Constructor Injection](#24-prefer-constructor-injection)\n   - 2.5 [Understand Provider Scopes](#25-understand-provider-scopes)\n   - 2.6 [Use Injection Tokens for Interfaces](#26-use-injection-tokens-for-interfaces)\n3. [Error Handling](#3-error-handling) — **HIGH**\n   - 3.1 [Handle Async Errors Properly](#31-handle-async-errors-properly)\n   - 3.2 [Throw HTTP Exceptions from Services](#32-throw-http-exceptions-from-services)\n   - 3.3 [Use Exception Filters for Error Handling](#33-use-exception-filters-for-error-handling)\n4. [Security](#4-security) — **HIGH**\n   - 4.1 [Implement Secure JWT Authentication](#41-implement-secure-jwt-authentication)\n   - 4.2 [Implement Rate Limiting](#42-implement-rate-limiting)\n   - 4.3 [Sanitize Output to Prevent XSS](#43-sanitize-output-to-prevent-xss)\n   - 4.4 [Use Guards for Authentication and Authorization](#44-use-guards-for-authentication-and-authorization)\n   - 4.5 [Validate All Input with DTOs and Pipes](#45-validate-all-input-with-dtos-and-pipes)\n5. [Performance](#5-performance) — **HIGH**\n   - 5.1 [Use Async Lifecycle Hooks Correctly](#51-use-async-lifecycle-hooks-correctly)\n   - 5.2 [Use Lazy Loading for Large Modules](#52-use-lazy-loading-for-large-modules)\n   - 5.3 [Optimize Database Queries](#53-optimize-database-queries)\n   - 5.4 [Use Caching Strategically](#54-use-caching-strategically)\n6. [Testing](#6-testing) — **MEDIUM-HIGH**\n   - 6.1 [Use Supertest for E2E Testing](#61-use-supertest-for-e2e-testing)\n   - 6.2 [Mock External Services in Tests](#62-mock-external-services-in-tests)\n   - 6.3 [Use Testing Module for Unit Tests](#63-use-testing-module-for-unit-tests)\n7. [Database & ORM](#7-database-orm) — **MEDIUM-HIGH**\n   - 7.1 [Avoid N+1 Query Problems](#71-avoid-n-1-query-problems)\n   - 7.2 [Use Database Migrations](#72-use-database-migrations)\n   - 7.3 [Use Transactions for Multi-Step Operations](#73-use-transactions-for-multi-step-operations)\n8. [API Design](#8-api-design) — **MEDIUM**\n   - 8.1 [Use DTOs and Serialization for API Responses](#81-use-dtos-and-serialization-for-api-responses)\n   - 8.2 [Use Interceptors for Cross-Cutting Concerns](#82-use-interceptors-for-cross-cutting-concerns)\n   - 8.3 [Use Pipes for Input Transformation](#83-use-pipes-for-input-transformation)\n   - 8.4 [Use API Versioning for Breaking Changes](#84-use-api-versioning-for-breaking-changes)\n9. [Microservices](#9-microservices) — **MEDIUM**\n   - 9.1 [Implement Health Checks for Microservices](#91-implement-health-checks-for-microservices)\n   - 9.2 [Use Message and Event Patterns Correctly](#92-use-message-and-event-patterns-correctly)\n   - 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs)\n10. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM**\n\n- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)\n- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)\n- 10.3 [Use Structured Logging](#103-use-structured-logging)\n\n---\n\n## 1. Architecture\n\n**Section Impact: CRITICAL**\n\n### 1.1 Avoid Circular Dependencies\n\n**Impact: CRITICAL** — \"#1 cause of runtime crashes\"\n\nCircular dependencies occur when Module A imports Module B, and Module B imports Module A (directly or transitively). NestJS can sometimes resolve these through forward references, but they indicate architectural problems and should be avoided. This is the #1 cause of runtime crashes in NestJS applications.\n\n**Incorrect (circular module imports):**\n\n```typescript\n// users.module.ts\n@Module({\n  imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular\n  providers: [UsersService],\n  exports: [UsersService],\n})\nexport class UsersModule {}\n\n// orders.module.ts\n@Module({\n  imports: [UsersModule], // Circular dependency!\n  providers: [OrdersService],\n  exports: [OrdersService],\n})\nexport class OrdersModule {}\n```\n\n**Correct (extract shared logic or use events):**\n\n```typescript\n// Option 1: Extract shared logic to a third module\n// shared.module.ts\n@Module({\n  providers: [SharedService],\n  exports: [SharedService],\n})\nexport class SharedModule {}\n\n// users.module.ts\n@Module({\n  imports: [SharedModule],\n  providers: [UsersService],\n})\nexport class UsersModule {}\n\n// orders.module.ts\n@Module({\n  imports: [SharedModule],\n  providers: [OrdersService],\n})\nexport class OrdersModule {}\n\n// Option 2: Use events for decoupled communication\n// users.service.ts\n@Injectable()\nexport class UsersService {\n  constructor(private eventEmitter: EventEmitter2) {}\n\n  async createUser(data: CreateUserDto) {\n    const user = await this.userRepo.save(data);\n    this.eventEmitter.emit('user.created', user);\n    return user;\n  }\n}\n\n// orders.service.ts\n@Injectable()\nexport class OrdersService {\n  @OnEvent('user.created')\n  handleUserCreated(user: User) {\n    // React to user creation without direct dependency\n  }\n}\n```\n\nReference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency)\n\n---\n\n### 1.2 Organize by Feature Modules\n\n**Impact: CRITICAL** — \"3-5x faster onboarding and development\"\n\nOrganize your application into feature modules that encapsulate related functionality. Each feature module should be self-contained with its own controllers, services, entities, and DTOs. Avoid organizing by technical layer (all controllers together, all services together). This enables 3-5x faster onboarding and feature development.\n\n**Incorrect (technical layer organization):**\n\n```typescript\n// Technical layer organization (anti-pattern)\nsrc/\n├── controllers/\n│   ├── users.controller.ts\n│   ├── orders.controller.ts\n│   └── products.controller.ts\n├── services/\n│   ├── users.service.ts\n│   ├── orders.service.ts\n│   └── products.service.ts\n├── entities/\n│   ├── user.entity.ts\n│   ├── order.entity.ts\n│   └── product.entity.ts\n└── app.module.ts  // Imports everything directly\n```\n\n**Correct (feature module organization):**\n\n```typescript\n// Feature module organization\nsrc/\n├── users/\n│   ├── dto/\n│   │   ├── create-user.dto.ts\n│   │   └── update-user.dto.ts\n│   ├── entities/\n│   │   └── user.entity.ts\n│   ├── users.controller.ts\n│   ├── users.service.ts\n│   ├── users.repository.ts\n│   └── users.module.ts\n├── orders/\n│   ├── dto/\n│   ├── entities/\n│   ├── orders.controller.ts\n│   ├── orders.service.ts\n│   └── orders.module.ts\n├── shared/\n│   ├── guards/\n│   ├── interceptors/\n│   ├── filters/\n│   └── shared.module.ts\n└── app.module.ts\n\n// users.module.ts\n@Module({\n  imports: [TypeOrmModule.forFeature([User])],\n  controllers: [UsersController],\n  providers: [UsersService, UsersRepository],\n  exports: [UsersService], // Only export what others need\n})\nexport class UsersModule {}\n\n// app.module.ts\n@Module({\n  imports: [\n    ConfigModule.forRoot(),\n    TypeOrmModule.forRoot(),\n    UsersModule,\n    OrdersModule,\n    SharedModule,\n  ],\n})\nexport class AppModule {}\n```\n\nReference: [NestJS Modules](https://docs.nestjs.com/modules)\n\n---\n\n### 1.3 Use Proper Module Sharing Patterns\n\n**Impact: CRITICAL** — Prevents duplicate instances, memory leaks, and state inconsistency\n\nNestJS modules are singletons by default. When a service is properly exported from a module and that module is imported elsewhere, the same instance is shared. However, providing a service in multiple modules creates separate instances, leading to memory waste, state inconsistency, and confusing behavior. Always encapsulate services in dedicated modules, export them explicitly, and import the module where needed.\n\n**Incorrect (service provided in multiple modules):**\n\n```typescript\n// StorageService provided directly in multiple modules - WRONG\n// storage.service.ts\n@Injectable()\nexport class StorageService {\n  private cache = new Map(); // Each instance has separate state!\n\n  store(key: string, value: any) {\n    this.cache.set(key, value);\n  }\n}\n\n// app.module.ts\n@Module({\n  providers: [StorageService], // Instance #1\n  controllers: [AppController],\n})\nexport class AppModule {}\n\n// videos.module.ts\n@Module({\n  providers: [StorageService], // Instance #2 - different from AppModule!\n  controllers: [VideosController],\n})\nexport class VideosModule {}\n\n// Problems:\n// 1. Two separate StorageService instances exist\n// 2. cache.set() in VideosModule doesn't affect AppModule's cache\n// 3. Memory wasted on duplicate instances\n// 4. Debugging nightmares when state doesn't sync\n```\n\n**Correct (dedicated module with exports):**\n\n```typescript\n// storage/storage.module.ts\n@Module({\n  providers: [StorageService],\n  exports: [StorageService], // Make available to importers\n})\nexport class StorageModule {}\n\n// videos/videos.module.ts\n@Module({\n  imports: [StorageModule], // Import the module, not the service\n  controllers: [VideosController],\n  providers: [VideosService],\n})\nexport class VideosModule {}\n\n// channels/channels.module.ts\n@Module({\n  imports: [StorageModule], // Same instance shared\n  controllers: [ChannelsController],\n  providers: [ChannelsService],\n})\nexport class ChannelsModule {}\n\n// app.module.ts\n@Module({\n  imports: [\n    StorageModule, // Only if AppModule itself needs StorageService\n    VideosModule,\n    ChannelsModule,\n  ],\n})\nexport class AppModule {}\n\n// Now all modules share the SAME StorageService instance\n```\n\n**When to use @Global() (sparingly):**\n\n```typescript\n// ONLY for truly cross-cutting concerns\n@Global()\n@Module({\n  providers: [ConfigService, LoggerService],\n  exports: [ConfigService, LoggerService],\n})\nexport class CoreModule {}\n\n// Import once in AppModule\n@Module({\n  imports: [CoreModule], // Registered globally, available everywhere\n})\nexport class AppModule {}\n\n// Other modules don't need to import CoreModule\n@Module({\n  controllers: [UsersController],\n  providers: [UsersService], // Can inject ConfigService without importing\n})\nexport class UsersModule {}\n\n// WARNING: Don't make everything global!\n// - Hides dependencies (can't see what a module needs from imports)\n// - Makes testing harder\n// - Reserve for: config, logging, database connections\n```\n\n**Module re-exporting pattern:**\n\n```typescript\n// common.module.ts - shared utilities\n@Module({\n  providers: [DateService, ValidationService],\n  exports: [DateService, ValidationService],\n})\nexport class CommonModule {}\n\n// core.module.ts - re-exports common for convenience\n@Module({\n  imports: [CommonModule, DatabaseModule],\n  exports: [CommonModule, DatabaseModule], // Re-export for consumers\n})\nexport class CoreModule {}\n\n// feature.module.ts - imports CoreModule, gets both\n@Module({\n  imports: [CoreModule], // Gets CommonModule + DatabaseModule\n  controllers: [FeatureController],\n})\nexport class FeatureModule {}\n```\n\nReference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules)\n\n---\n\n### 1.4 Single Responsibility for Services\n\n**Impact: CRITICAL** — \"40%+ improvement in testability\"\n\nEach service should have a single, well-defined responsibility. Avoid \"god services\" that handle multiple unrelated concerns. If a service name includes \"And\" or handles more than one domain concept, it likely violates single responsibility. This reduces complexity and improves testability by 40%+.\n\n**Incorrect (god service anti-pattern):**\n\n```typescript\n// God service anti-pattern\n@Injectable()\nexport class UserAndOrderService {\n  constructor(\n    private userRepo: UserRepository,\n    private orderRepo: OrderRepository,\n    private mailer: MailService,\n    private payment: PaymentService,\n  ) {}\n\n  async createUser(dto: CreateUserDto) {\n    const user = await this.userRepo.save(dto);\n    await this.mailer.sendWelcome(user);\n    return user;\n  }\n\n  async createOrder(userId: string, dto: CreateOrderDto) {\n    const order = await this.orderRepo.save({ userId, ...dto });\n    await this.payment.charge(order);\n    await this.mailer.sendOrderConfirmation(order);\n    return order;\n  }\n\n  async calculateOrderStats(userId: string) {\n    // Stats logic mixed in\n  }\n\n  async validatePayment(orderId: string) {\n    // Payment logic mixed in\n  }\n}\n```\n\n**Correct (focused services with single responsibility):**\n\n```typescript\n// Focused services with single responsibility\n@Injectable()\nexport class UsersService {\n  constructor(private userRepo: UserRepository) {}\n\n  async create(dto: CreateUserDto): Promise<User> {\n    return this.userRepo.save(dto);\n  }\n\n  async findById(id: string): Promise<User> {\n    return this.userRepo.findOneOrFail({ where: { id } });\n  }\n}\n\n@Injectable()\nexport class OrdersService {\n  constructor(private orderRepo: OrderRepository) {}\n\n  async create(userId: string, dto: CreateOrderDto): Promise<Order> {\n    return this.orderRepo.save({ userId, ...dto });\n  }\n\n  async findByUser(userId: string): Promise<Order[]> {\n    return this.orderRepo.find({ where: { userId } });\n  }\n}\n\n@Injectable()\nexport class OrderStatsService {\n  constructor(private orderRepo: OrderRepository) {}\n\n  async calculateForUser(userId: string): Promise<OrderStats> {\n    // Focused stats calculation\n  }\n}\n\n// Orchestration in controller or dedicated orchestrator\n@Controller('orders')\nexport class OrdersController {\n  constructor(\n    private orders: OrdersService,\n    private payment: PaymentService,\n    private notifications: NotificationService,\n  ) {}\n\n  @Post()\n  async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) {\n    const order = await this.orders.create(user.id, dto);\n    await this.payment.charge(order);\n    await this.notifications.sendOrderConfirmation(order);\n    return order;\n  }\n}\n```\n\nReference: [NestJS Providers](https://docs.nestjs.com/providers)\n\n---\n\n### 1.5 Use Event-Driven Architecture for Decoupling\n\n**Impact: MEDIUM-HIGH** — Enables async processing and modularity\n\nUse `@nestjs/event-emitter` for intra-service events and message brokers for inter-service communication. Events allow modules to react to changes without direct dependencies, improving modularity and enabling async processing.\n\n**Incorrect (direct service coupling):**\n\n```typescript\n// Direct service coupling\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private inventoryService: InventoryService,\n    private emailService: EmailService,\n    private analyticsService: AnalyticsService,\n    private notificationService: NotificationService,\n    private loyaltyService: LoyaltyService,\n  ) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const order = await this.repo.save(dto);\n\n    // Tight coupling - OrdersService knows about all consumers\n    await this.inventoryService.reserve(order.items);\n    await this.emailService.sendConfirmation(order);\n    await this.analyticsService.track('order_created', order);\n    await this.notificationService.push(order.userId, 'Order placed');\n    await this.loyaltyService.addPoints(order.userId, order.total);\n\n    // Adding new behavior requires modifying this service\n    return order;\n  }\n}\n```\n\n**Correct (event-driven decoupling):**\n\n```typescript\n// Use EventEmitter for decoupling\nimport { EventEmitter2 } from '@nestjs/event-emitter';\n\n// Define event\nexport class OrderCreatedEvent {\n  constructor(\n    public readonly orderId: string,\n    public readonly userId: string,\n    public readonly items: OrderItem[],\n    public readonly total: number,\n  ) {}\n}\n\n// Service emits events\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private eventEmitter: EventEmitter2,\n    private repo: Repository<Order>,\n  ) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const order = await this.repo.save(dto);\n\n    // Emit event - no knowledge of consumers\n    this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));\n\n    return order;\n  }\n}\n\n// Listeners in separate modules\n@Injectable()\nexport class InventoryListener {\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    await this.inventoryService.reserve(event.items);\n  }\n}\n\n@Injectable()\nexport class EmailListener {\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    await this.emailService.sendConfirmation(event.orderId);\n  }\n}\n\n@Injectable()\nexport class AnalyticsListener {\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    await this.analyticsService.track('order_created', {\n      orderId: event.orderId,\n      total: event.total,\n    });\n  }\n}\n```\n\nReference: [NestJS Events](https://docs.nestjs.com/techniques/events)\n\n---\n\n### 1.6 Use Repository Pattern for Data Access\n\n**Impact: HIGH** — Decouples business logic from database\n\nCreate custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code.\n\n**Incorrect (complex queries in services):**\n\n```typescript\n// Complex queries in services\n@Injectable()\nexport class UsersService {\n  constructor(@InjectRepository(User) private repo: Repository<User>) {}\n\n  async findActiveWithOrders(minOrders: number): Promise<User[]> {\n    // Complex query logic mixed with business logic\n    return this.repo\n      .createQueryBuilder('user')\n      .leftJoinAndSelect('user.orders', 'order')\n      .where('user.isActive = :active', { active: true })\n      .andWhere('user.deletedAt IS NULL')\n      .groupBy('user.id')\n      .having('COUNT(order.id) >= :min', { min: minOrders })\n      .orderBy('user.createdAt', 'DESC')\n      .getMany();\n  }\n\n  // Service becomes bloated with query logic\n}\n```\n\n**Correct (custom repository with encapsulated queries):**\n\n```typescript\n// Custom repository with encapsulated queries\n@Injectable()\nexport class UsersRepository {\n  constructor(@InjectRepository(User) private repo: Repository<User>) {}\n\n  async findById(id: string): Promise<User | null> {\n    return this.repo.findOne({ where: { id } });\n  }\n\n  async findByEmail(email: string): Promise<User | null> {\n    return this.repo.findOne({ where: { email } });\n  }\n\n  async findActiveWithMinOrders(minOrders: number): Promise<User[]> {\n    return this.repo\n      .createQueryBuilder('user')\n      .leftJoinAndSelect('user.orders', 'order')\n      .where('user.isActive = :active', { active: true })\n      .andWhere('user.deletedAt IS NULL')\n      .groupBy('user.id')\n      .having('COUNT(order.id) >= :min', { min: minOrders })\n      .orderBy('user.createdAt', 'DESC')\n      .getMany();\n  }\n\n  async save(user: User): Promise<User> {\n    return this.repo.save(user);\n  }\n}\n\n// Clean service with business logic only\n@Injectable()\nexport class UsersService {\n  constructor(private usersRepo: UsersRepository) {}\n\n  async getActiveUsersWithOrders(): Promise<User[]> {\n    return this.usersRepo.findActiveWithMinOrders(1);\n  }\n\n  async create(dto: CreateUserDto): Promise<User> {\n    const existing = await this.usersRepo.findByEmail(dto.email);\n    if (existing) {\n      throw new ConflictException('Email already registered');\n    }\n\n    const user = new User();\n    user.email = dto.email;\n    user.name = dto.name;\n    return this.usersRepo.save(user);\n  }\n}\n```\n\nReference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)\n\n---\n\n## 2. Dependency Injection\n\n**Section Impact: CRITICAL**\n\n### 2.1 Avoid Service Locator Anti-Pattern\n\n**Impact: HIGH** — Hides dependencies and breaks testability\n\nAvoid using `ModuleRef.get()` or global containers to resolve dependencies at runtime. This hides dependencies, makes code harder to test, and breaks the benefits of dependency injection. Use constructor injection instead.\n\n**Incorrect (service locator anti-pattern):**\n\n```typescript\n// Use ModuleRef to get dependencies dynamically\n@Injectable()\nexport class OrdersService {\n  constructor(private moduleRef: ModuleRef) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    // Dependencies are hidden - not visible in constructor\n    const usersService = this.moduleRef.get(UsersService);\n    const inventoryService = this.moduleRef.get(InventoryService);\n    const paymentService = this.moduleRef.get(PaymentService);\n\n    const user = await usersService.findOne(dto.userId);\n    // ... rest of logic\n  }\n}\n\n// Global singleton container\nclass ServiceContainer {\n  private static instance: ServiceContainer;\n  private services = new Map<string, any>();\n\n  static getInstance(): ServiceContainer {\n    if (!this.instance) {\n      this.instance = new ServiceContainer();\n    }\n    return this.instance;\n  }\n\n  get<T>(key: string): T {\n    return this.services.get(key);\n  }\n}\n```\n\n**Correct (constructor injection with explicit dependencies):**\n\n```typescript\n// Use constructor injection - dependencies are explicit\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private usersService: UsersService,\n    private inventoryService: InventoryService,\n    private paymentService: PaymentService,\n  ) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const user = await this.usersService.findOne(dto.userId);\n    const inventory = await this.inventoryService.check(dto.items);\n    // Dependencies are clear and testable\n  }\n}\n\n// Easy to test with mocks\ndescribe('OrdersService', () => {\n  let service: OrdersService;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        OrdersService,\n        { provide: UsersService, useValue: mockUsersService },\n        { provide: InventoryService, useValue: mockInventoryService },\n        { provide: PaymentService, useValue: mockPaymentService },\n      ],\n    }).compile();\n\n    service = module.get(OrdersService);\n  });\n});\n\n// VALID: Factory pattern for dynamic instantiation\n@Injectable()\nexport class HandlerFactory {\n  constructor(private moduleRef: ModuleRef) {}\n\n  getHandler(type: string): Handler {\n    switch (type) {\n      case 'email':\n        return this.moduleRef.get(EmailHandler);\n      case 'sms':\n        return this.moduleRef.get(SmsHandler);\n      default:\n        return this.moduleRef.get(DefaultHandler);\n    }\n  }\n}\n```\n\nReference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref)\n\n---\n\n### 2.2 Apply Interface Segregation Principle\n\n**Impact: HIGH** — Reduces coupling and improves testability by 30-50%\n\nClients should not be forced to depend on interfaces they don't use. In NestJS, this means keeping interfaces small and focused on specific capabilities rather than creating \"fat\" interfaces that bundle unrelated methods. When a service only needs to send emails, it shouldn't depend on an interface that also includes SMS, push notifications, and logging. Split large interfaces into role-based ones.\n\n**Incorrect (fat interface forcing unused dependencies):**\n\n```typescript\n// Fat interface - forces all consumers to depend on everything\ninterface NotificationService {\n  sendEmail(to: string, subject: string, body: string): Promise<void>;\n  sendSms(phone: string, message: string): Promise<void>;\n  sendPush(userId: string, notification: PushPayload): Promise<void>;\n  sendSlack(channel: string, message: string): Promise<void>;\n  logNotification(type: string, payload: any): Promise<void>;\n  getDeliveryStatus(id: string): Promise<DeliveryStatus>;\n  retryFailed(id: string): Promise<void>;\n  scheduleNotification(dto: ScheduleDto): Promise<string>;\n}\n\n// Consumer only needs email, but must mock everything for tests\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private notifications: NotificationService, // Depends on 8 methods, uses 1\n  ) {}\n\n  async confirmOrder(order: Order): Promise<void> {\n    await this.notifications.sendEmail(\n      order.customer.email,\n      'Order Confirmed',\n      `Your order ${order.id} has been confirmed.`,\n    );\n  }\n}\n\n// Testing is painful - must mock unused methods\nconst mockNotificationService = {\n  sendEmail: jest.fn(),\n  sendSms: jest.fn(), // Never used, but required\n  sendPush: jest.fn(), // Never used, but required\n  sendSlack: jest.fn(), // Never used, but required\n  logNotification: jest.fn(), // Never used, but required\n  getDeliveryStatus: jest.fn(), // Never used, but required\n  retryFailed: jest.fn(), // Never used, but required\n  scheduleNotification: jest.fn(), // Never used, but required\n};\n```\n\n**Correct (segregated interfaces by capability):**\n\n```typescript\n// Segregated interfaces - each focused on one capability\ninterface EmailSender {\n  sendEmail(to: string, subject: string, body: string): Promise<void>;\n}\n\ninterface SmsSender {\n  sendSms(phone: string, message: string): Promise<void>;\n}\n\ninterface PushSender {\n  sendPush(userId: string, notification: PushPayload): Promise<void>;\n}\n\ninterface NotificationLogger {\n  logNotification(type: string, payload: any): Promise<void>;\n}\n\ninterface NotificationScheduler {\n  scheduleNotification(dto: ScheduleDto): Promise<string>;\n}\n\n// Implementation can implement multiple interfaces\n@Injectable()\nexport class NotificationService implements EmailSender, SmsSender, PushSender {\n  async sendEmail(to: string, subject: string, body: string): Promise<void> {\n    // Email implementation\n  }\n\n  async sendSms(phone: string, message: string): Promise<void> {\n    // SMS implementation\n  }\n\n  async sendPush(userId: string, notification: PushPayload): Promise<void> {\n    // Push implementation\n  }\n}\n\n// Or separate implementations\n@Injectable()\nexport class SendGridEmailService implements EmailSender {\n  async sendEmail(to: string, subject: string, body: string): Promise<void> {\n    // SendGrid-specific implementation\n  }\n}\n\n// Consumer depends only on what it needs\n@Injectable()\nexport class OrdersService {\n  constructor(\n    @Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency\n  ) {}\n\n  async confirmOrder(order: Order): Promise<void> {\n    await this.emailSender.sendEmail(\n      order.customer.email,\n      'Order Confirmed',\n      `Your order ${order.id} has been confirmed.`,\n    );\n  }\n}\n\n// Testing is simple - only mock what's used\nconst mockEmailSender: EmailSender = {\n  sendEmail: jest.fn(),\n};\n\n// Module registration with tokens\nexport const EMAIL_SENDER = Symbol('EMAIL_SENDER');\nexport const SMS_SENDER = Symbol('SMS_SENDER');\n\n@Module({\n  providers: [\n    { provide: EMAIL_SENDER, useClass: SendGridEmailService },\n    { provide: SMS_SENDER, useClass: TwilioSmsService },\n  ],\n  exports: [EMAIL_SENDER, SMS_SENDER],\n})\nexport class NotificationModule {}\n```\n\n**Combining interfaces when needed:**\n\n```typescript\n// Sometimes a consumer legitimately needs multiple capabilities\ninterface EmailAndSmsSender extends EmailSender, SmsSender {}\n\n// Or use intersection types\ntype MultiChannelSender = EmailSender & SmsSender & PushSender;\n\n// Consumer that genuinely needs multiple channels\n@Injectable()\nexport class AlertService {\n  constructor(\n    @Inject(MULTI_CHANNEL_SENDER)\n    private sender: EmailSender & SmsSender,\n  ) {}\n\n  async sendCriticalAlert(user: User, message: string): Promise<void> {\n    await Promise.all([\n      this.sender.sendEmail(user.email, 'Critical Alert', message),\n      this.sender.sendSms(user.phone, message),\n    ]);\n  }\n}\n```\n\nReference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)\n\n---\n\n### 2.3 Honor Liskov Substitution Principle\n\n**Impact: HIGH** — Ensures implementations are truly interchangeable without breaking callers\n\nSubtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations.\n\n**Incorrect (implementation violates the contract):**\n\n```typescript\n// Base interface with clear contract\ninterface PaymentGateway {\n  /**\n   * Charges the specified amount.\n   * @returns PaymentResult on success\n   * @throws PaymentFailedException on payment failure\n   */\n  charge(amount: number, currency: string): Promise<PaymentResult>;\n}\n\n// Production implementation - follows the contract\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    const response = await this.stripe.charges.create({ amount, currency });\n    return { success: true, transactionId: response.id, amount };\n  }\n}\n\n// Mock that violates LSP - different behavior!\n@Injectable()\nexport class MockPaymentService implements PaymentGateway {\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    // VIOLATION 1: Throws for valid input (contract says return PaymentResult)\n    if (amount > 1000) {\n      throw new Error('Mock does not support large amounts');\n    }\n\n    // VIOLATION 2: Returns null instead of PaymentResult\n    if (currency !== 'USD') {\n      return null as any; // Real service would convert or reject properly\n    }\n\n    // VIOLATION 3: Missing required field\n    return { success: true } as PaymentResult; // Missing transactionId!\n  }\n}\n\n// Consumer trusts the contract\n@Injectable()\nexport class OrdersService {\n  constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}\n\n  async checkout(order: Order): Promise<void> {\n    const result = await this.payment.charge(order.total, order.currency);\n    // These fail with MockPaymentService:\n    await this.saveTransaction(result.transactionId); // undefined!\n    await this.sendReceipt(result); // might be null!\n  }\n}\n```\n\n**Correct (implementations honor the contract):**\n\n```typescript\n// Well-defined interface with documented behavior\ninterface PaymentGateway {\n  /**\n   * Charges the specified amount.\n   * @param amount - Amount in smallest currency unit (cents)\n   * @param currency - ISO 4217 currency code\n   * @returns PaymentResult with transactionId, success status, and amount\n   * @throws PaymentFailedException if charge is declined\n   * @throws InvalidCurrencyException if currency is not supported\n   */\n  charge(amount: number, currency: string): Promise<PaymentResult>;\n\n  /**\n   * Refunds a previous charge.\n   * @throws TransactionNotFoundException if transactionId is invalid\n   */\n  refund(transactionId: string, amount?: number): Promise<RefundResult>;\n}\n\n// Production implementation\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    try {\n      const response = await this.stripe.charges.create({ amount, currency });\n      return {\n        success: true,\n        transactionId: response.id,\n        amount: response.amount,\n      };\n    } catch (error) {\n      if (error.type === 'card_error') {\n        throw new PaymentFailedException(error.message);\n      }\n      throw error;\n    }\n  }\n\n  async refund(transactionId: string, amount?: number): Promise<RefundResult> {\n    // Implementation...\n  }\n}\n\n// Mock that honors LSP - same contract, same behavior shape\n@Injectable()\nexport class MockPaymentService implements PaymentGateway {\n  private transactions = new Map<string, PaymentResult>();\n\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    // Honor the contract: validate currency like real service would\n    if (!['USD', 'EUR', 'GBP'].includes(currency)) {\n      throw new InvalidCurrencyException(`Unsupported currency: ${currency}`);\n    }\n\n    // Simulate decline for specific test scenarios\n    if (amount === 99999) {\n      throw new PaymentFailedException('Card declined (test scenario)');\n    }\n\n    // Return same shape as production\n    const result: PaymentResult = {\n      success: true,\n      transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`,\n      amount,\n    };\n\n    this.transactions.set(result.transactionId, result);\n    return result;\n  }\n\n  async refund(transactionId: string, amount?: number): Promise<RefundResult> {\n    // Honor the contract: throw if transaction not found\n    if (!this.transactions.has(transactionId)) {\n      throw new TransactionNotFoundException(transactionId);\n    }\n\n    return {\n      success: true,\n      refundId: `refund_${transactionId}`,\n      amount: amount ?? this.transactions.get(transactionId)!.amount,\n    };\n  }\n}\n\n// Consumer can swap implementations safely\n@Injectable()\nexport class OrdersService {\n  constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}\n\n  async checkout(order: Order): Promise<Order> {\n    try {\n      const result = await this.payment.charge(order.total, order.currency);\n      // Works with both StripeService and MockPaymentService\n      order.transactionId = result.transactionId;\n      order.status = 'paid';\n      return order;\n    } catch (error) {\n      if (error instanceof PaymentFailedException) {\n        order.status = 'payment_failed';\n        return order;\n      }\n      throw error;\n    }\n  }\n}\n```\n\n**Testing LSP compliance:**\n\n```typescript\n// Shared test suite that any implementation must pass\nfunction testPaymentGatewayContract(createGateway: () => PaymentGateway) {\n  describe('PaymentGateway contract', () => {\n    let gateway: PaymentGateway;\n\n    beforeEach(() => {\n      gateway = createGateway();\n    });\n\n    it('returns PaymentResult with all required fields', async () => {\n      const result = await gateway.charge(1000, 'USD');\n      expect(result).toHaveProperty('success');\n      expect(result).toHaveProperty('transactionId');\n      expect(result).toHaveProperty('amount');\n      expect(typeof result.transactionId).toBe('string');\n    });\n\n    it('throws InvalidCurrencyException for unsupported currency', async () => {\n      await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);\n    });\n\n    it('throws TransactionNotFoundException for invalid refund', async () => {\n      await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);\n    });\n  });\n}\n\n// Run against all implementations\ndescribe('StripeService', () => {\n  testPaymentGatewayContract(() => new StripeService(mockStripeClient));\n});\n\ndescribe('MockPaymentService', () => {\n  testPaymentGatewayContract(() => new MockPaymentService());\n});\n```\n\nReference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)\n\n---\n\n### 2.4 Prefer Constructor Injection\n\n**Impact: CRITICAL** — Required for proper DI and testing\n\nAlways use constructor injection over property injection. Constructor injection makes dependencies explicit, enables TypeScript type checking, ensures dependencies are available when the class is instantiated, and improves testability. This is required for proper DI, testing, and TypeScript support.\n\n**Incorrect (property injection with hidden dependencies):**\n\n```typescript\n// Property injection - avoid unless necessary\n@Injectable()\nexport class UsersService {\n  @Inject()\n  private userRepo: UserRepository; // Hidden dependency\n\n  @Inject('CONFIG')\n  private config: ConfigType; // Also hidden\n\n  async findAll() {\n    return this.userRepo.find();\n  }\n}\n\n// Problems:\n// 1. Dependencies not visible in constructor\n// 2. Service can be instantiated without dependencies in tests\n// 3. TypeScript can't enforce dependency types at instantiation\n```\n\n**Correct (constructor injection with explicit dependencies):**\n\n```typescript\n// Constructor injection - explicit and testable\n@Injectable()\nexport class UsersService {\n  constructor(\n    private readonly userRepo: UserRepository,\n    @Inject('CONFIG') private readonly config: ConfigType,\n  ) {}\n\n  async findAll(): Promise<User[]> {\n    return this.userRepo.find();\n  }\n}\n\n// Testing is straightforward\ndescribe('UsersService', () => {\n  let service: UsersService;\n  let mockRepo: jest.Mocked<UserRepository>;\n\n  beforeEach(() => {\n    mockRepo = {\n      find: jest.fn(),\n      save: jest.fn(),\n    } as any;\n\n    service = new UsersService(mockRepo, { dbUrl: 'test' });\n  });\n\n  it('should find all users', async () => {\n    mockRepo.find.mockResolvedValue([{ id: '1', name: 'Test' }]);\n    const result = await service.findAll();\n    expect(result).toHaveLength(1);\n  });\n});\n\n// Only use property injection for optional dependencies\n@Injectable()\nexport class LoggingService {\n  @Optional()\n  @Inject('ANALYTICS')\n  private analytics?: AnalyticsService;\n\n  log(message: string) {\n    console.log(message);\n    this.analytics?.track('log', message); // Optional enhancement\n  }\n}\n```\n\nReference: [NestJS Providers](https://docs.nestjs.com/providers)\n\n---\n\n### 2.5 Understand Provider Scopes\n\n**Impact: CRITICAL** — Prevents data leaks and performance issues\n\nNestJS has three provider scopes: DEFAULT (singleton), REQUEST (per-request instance), and TRANSIENT (new instance for each injection). Most providers should be singletons. Request-scoped providers have performance implications as they bubble up through the dependency tree. Understanding scopes prevents memory leaks and incorrect data sharing.\n\n**Incorrect (wrong scope usage):**\n\n```typescript\n// Request-scoped when not needed (performance hit)\n@Injectable({ scope: Scope.REQUEST })\nexport class UsersService {\n  // This creates a new instance for EVERY request\n  // All dependencies also become request-scoped\n  async findAll() {\n    return this.userRepo.find();\n  }\n}\n\n// Singleton with mutable request state\n@Injectable() // Default: singleton\nexport class RequestContextService {\n  private userId: string; // DANGER: Shared across all requests!\n\n  setUser(userId: string) {\n    this.userId = userId; // Overwrites for all concurrent requests\n  }\n\n  getUser() {\n    return this.userId; // Returns wrong user!\n  }\n}\n```\n\n**Correct (appropriate scope for each use case):**\n\n```typescript\n// Singleton for stateless services (default, most common)\n@Injectable()\nexport class UsersService {\n  constructor(private readonly userRepo: UserRepository) {}\n\n  async findById(id: string): Promise<User> {\n    return this.userRepo.findOne({ where: { id } });\n  }\n}\n\n// Request-scoped ONLY when you need request context\n@Injectable({ scope: Scope.REQUEST })\nexport class RequestContextService {\n  private userId: string;\n\n  setUser(userId: string) {\n    this.userId = userId;\n  }\n\n  getUser(): string {\n    return this.userId;\n  }\n}\n\n// Better: Use NestJS built-in request context\nimport { REQUEST } from '@nestjs/core';\nimport { Request } from 'express';\n\n@Injectable({ scope: Scope.REQUEST })\nexport class AuditService {\n  constructor(@Inject(REQUEST) private request: Request) {}\n\n  log(action: string) {\n    console.log(`User ${this.request.user?.id} performed ${action}`);\n  }\n}\n\n// Best: Use ClsModule for async context (no scope bubble-up)\nimport { ClsService } from 'nestjs-cls';\n\n@Injectable() // Stays singleton!\nexport class AuditService {\n  constructor(private cls: ClsService) {}\n\n  log(action: string) {\n    const userId = this.cls.get('userId');\n    console.log(`User ${userId} performed ${action}`);\n  }\n}\n```\n\nReference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes)\n\n---\n\n### 2.6 Use Injection Tokens for Interfaces\n\n**Impact: HIGH** — Enables interface-based DI at runtime\n\nTypeScript interfaces are erased at compile time and can't be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces. This enables swapping implementations for testing or different environments.\n\n**Incorrect (interface can't be used as token):**\n\n```typescript\n// Interface can't be used as injection token\ninterface PaymentGateway {\n  charge(amount: number): Promise<PaymentResult>;\n}\n\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  charge(amount: number) {\n    /* ... */\n  }\n}\n\n@Injectable()\nexport class OrdersService {\n  // This WON'T work - PaymentGateway doesn't exist at runtime\n  constructor(private payment: PaymentGateway) {}\n}\n```\n\n**Correct (symbol tokens or abstract classes):**\n\n```typescript\n// Option 1: String/Symbol tokens (most flexible)\nexport const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY');\n\nexport interface PaymentGateway {\n  charge(amount: number): Promise<PaymentResult>;\n}\n\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  async charge(amount: number): Promise<PaymentResult> {\n    // Stripe implementation\n  }\n}\n\n@Injectable()\nexport class MockPaymentService implements PaymentGateway {\n  async charge(amount: number): Promise<PaymentResult> {\n    return { success: true, id: 'mock-id' };\n  }\n}\n\n// Module registration\n@Module({\n  providers: [\n    {\n      provide: PAYMENT_GATEWAY,\n      useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,\n    },\n  ],\n  exports: [PAYMENT_GATEWAY],\n})\nexport class PaymentModule {}\n\n// Injection\n@Injectable()\nexport class OrdersService {\n  constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}\n\n  async createOrder(dto: CreateOrderDto) {\n    await this.payment.charge(dto.amount);\n  }\n}\n\n// Option 2: Abstract class (carries runtime type info)\nexport abstract class PaymentGateway {\n  abstract charge(amount: number): Promise<PaymentResult>;\n}\n\n@Injectable()\nexport class StripeService extends PaymentGateway {\n  async charge(amount: number): Promise<PaymentResult> {\n    // Implementation\n  }\n}\n\n// No @Inject needed with abstract class\n@Injectable()\nexport class OrdersService {\n  constructor(private payment: PaymentGateway) {}\n}\n```\n\nReference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)\n\n---\n\n## 3. Error Handling\n\n**Section Impact: HIGH**\n\n### 3.1 Handle Async Errors Properly\n\n**Impact: HIGH** — Prevents process crashes from unhandled rejections\n\nNestJS automatically catches errors from async route handlers, but errors from background tasks, event handlers, and manually created promises can crash your application. Always handle async errors explicitly and use global handlers as a safety net.\n\n**Incorrect (fire-and-forget without error handling):**\n\n```typescript\n// Fire-and-forget without error handling\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Fire and forget - if this fails, error is unhandled!\n    this.emailService.sendWelcome(user.email);\n\n    return user;\n  }\n}\n\n// Unhandled promise in event handler\n@Injectable()\nexport class OrdersService {\n  @OnEvent('order.created')\n  handleOrderCreated(event: OrderCreatedEvent) {\n    // This returns a promise but it's not awaited!\n    this.processOrder(event);\n    // Errors will crash the process\n  }\n\n  private async processOrder(event: OrderCreatedEvent): Promise<void> {\n    await this.inventoryService.reserve(event.items);\n    await this.notificationService.send(event.userId);\n  }\n}\n\n// Missing try-catch in scheduled tasks\n@Cron('0 0 * * *')\nasync dailyCleanup(): Promise<void> {\n  await this.cleanupService.run();\n  // If this throws, no error handling\n}\n```\n\n**Correct (explicit async error handling):**\n\n```typescript\n// Handle fire-and-forget with explicit catch\n@Injectable()\nexport class UsersService {\n  private readonly logger = new Logger(UsersService.name);\n\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Explicitly catch and log errors\n    this.emailService.sendWelcome(user.email).catch(error => {\n      this.logger.error('Failed to send welcome email', error.stack);\n      // Optionally queue for retry\n    });\n\n    return user;\n  }\n}\n\n// Properly handle async event handlers\n@Injectable()\nexport class OrdersService {\n  private readonly logger = new Logger(OrdersService.name);\n\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    try {\n      await this.processOrder(event);\n    } catch (error) {\n      this.logger.error('Failed to process order', { event, error });\n      // Don't rethrow - would crash the process\n      await this.deadLetterQueue.add('order.created', event);\n    }\n  }\n}\n\n// Safe scheduled tasks\n@Injectable()\nexport class CleanupService {\n  private readonly logger = new Logger(CleanupService.name);\n\n  @Cron('0 0 * * *')\n  async dailyCleanup(): Promise<void> {\n    try {\n      await this.cleanupService.run();\n      this.logger.log('Daily cleanup completed');\n    } catch (error) {\n      this.logger.error('Daily cleanup failed', error.stack);\n      // Alert or retry logic\n    }\n  }\n}\n\n// Global unhandled rejection handler in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  const logger = new Logger('Bootstrap');\n\n  process.on('unhandledRejection', (reason, promise) => {\n    logger.error('Unhandled Rejection at:', promise, 'reason:', reason);\n  });\n\n  process.on('uncaughtException', error => {\n    logger.error('Uncaught Exception:', error);\n    process.exit(1);\n  });\n\n  await app.listen(3000);\n}\n```\n\nReference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection)\n\n---\n\n### 3.2 Throw HTTP Exceptions from Services\n\n**Impact: HIGH** — Keeps controllers thin and simplifies error handling\n\nIt's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes.\n\n**Incorrect (return error objects instead of throwing):**\n\n```typescript\n// Return error objects instead of throwing\n@Injectable()\nexport class UsersService {\n  async findById(id: string): Promise<{ user?: User; error?: string }> {\n    const user = await this.repo.findOne({ where: { id } });\n    if (!user) {\n      return { error: 'User not found' }; // Controller must check this\n    }\n    return { user };\n  }\n}\n\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string) {\n    const result = await this.usersService.findById(id);\n    if (result.error) {\n      throw new NotFoundException(result.error);\n    }\n    return result.user;\n  }\n}\n```\n\n**Correct (throw exceptions directly from service):**\n\n```typescript\n// Throw exceptions directly from service\n@Injectable()\nexport class UsersService {\n  constructor(private readonly repo: UserRepository) {}\n\n  async findById(id: string): Promise<User> {\n    const user = await this.repo.findOne({ where: { id } });\n    if (!user) {\n      throw new NotFoundException(`User #${id} not found`);\n    }\n    return user;\n  }\n\n  async create(dto: CreateUserDto): Promise<User> {\n    const existing = await this.repo.findOne({\n      where: { email: dto.email },\n    });\n    if (existing) {\n      throw new ConflictException('Email already registered');\n    }\n    return this.repo.save(dto);\n  }\n\n  async update(id: string, dto: UpdateUserDto): Promise<User> {\n    const user = await this.findById(id); // Throws if not found\n    Object.assign(user, dto);\n    return this.repo.save(user);\n  }\n}\n\n// Controller stays thin\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findById(id);\n  }\n\n  @Post()\n  create(@Body() dto: CreateUserDto): Promise<User> {\n    return this.usersService.create(dto);\n  }\n}\n\n// For layer-agnostic services, use domain exceptions\nexport class EntityNotFoundException extends Error {\n  constructor(\n    public readonly entity: string,\n    public readonly id: string,\n  ) {\n    super(`${entity} with ID \"${id}\" not found`);\n  }\n}\n\n// Map to HTTP in exception filter\n@Catch(EntityNotFoundException)\nexport class EntityNotFoundFilter implements ExceptionFilter {\n  catch(exception: EntityNotFoundException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse<Response>();\n\n    response.status(404).json({\n      statusCode: 404,\n      message: exception.message,\n      entity: exception.entity,\n      id: exception.id,\n    });\n  }\n}\n```\n\nReference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)\n\n---\n\n### 3.3 Use Exception Filters for Error Handling\n\n**Impact: HIGH** — Consistent, centralized error handling\n\nNever catch exceptions and manually format error responses in controllers. Use NestJS exception filters to handle errors consistently across your application. Create custom exception filters for specific error types and a global filter for unhandled exceptions.\n\n**Incorrect (manual error handling in controllers):**\n\n```typescript\n// Manual error handling in controllers\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string, @Res() res: Response) {\n    try {\n      const user = await this.usersService.findById(id);\n      if (!user) {\n        return res.status(404).json({\n          statusCode: 404,\n          message: 'User not found',\n        });\n      }\n      return res.json(user);\n    } catch (error) {\n      console.error(error);\n      return res.status(500).json({\n        statusCode: 500,\n        message: 'Internal server error',\n      });\n    }\n  }\n}\n```\n\n**Correct (exception filters with consistent handling):**\n\n```typescript\n// Use built-in and custom exceptions\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    const user = await this.usersService.findById(id);\n    if (!user) {\n      throw new NotFoundException(`User #${id} not found`);\n    }\n    return user;\n  }\n}\n\n// Custom domain exception\nexport class UserNotFoundException extends NotFoundException {\n  constructor(userId: string) {\n    super({\n      statusCode: 404,\n      error: 'Not Found',\n      message: `User with ID \"${userId}\" not found`,\n      code: 'USER_NOT_FOUND',\n    });\n  }\n}\n\n// Custom exception filter for domain errors\n@Catch(DomainException)\nexport class DomainExceptionFilter implements ExceptionFilter {\n  catch(exception: DomainException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse<Response>();\n    const request = ctx.getRequest<Request>();\n\n    const status = exception.getStatus?.() || 400;\n\n    response.status(status).json({\n      statusCode: status,\n      code: exception.code,\n      message: exception.message,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n\n// Global exception filter for unhandled errors\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n  constructor(private readonly logger: Logger) {}\n\n  catch(exception: unknown, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse<Response>();\n    const request = ctx.getRequest<Request>();\n\n    const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;\n\n    const message = exception instanceof HttpException ? exception.message : 'Internal server error';\n\n    this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);\n\n    response.status(status).json({\n      statusCode: status,\n      message,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n\n// Register globally in main.ts\napp.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());\n\n// Or via module\n@Module({\n  providers: [\n    {\n      provide: APP_FILTER,\n      useClass: AllExceptionsFilter,\n    },\n  ],\n})\nexport class AppModule {}\n```\n\nReference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)\n\n---\n\n## 4. Security\n\n**Section Impact: HIGH**\n\n### 4.1 Implement Secure JWT Authentication\n\n**Impact: CRITICAL** — Essential for secure APIs\n\nUse `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads.\n\n**Incorrect (insecure JWT implementation):**\n\n```typescript\n// Hardcode secrets\n@Module({\n  imports: [\n    JwtModule.register({\n      secret: 'my-secret-key', // Exposed in code\n      signOptions: { expiresIn: '7d' }, // Too long\n    }),\n  ],\n})\nexport class AuthModule {}\n\n// Store sensitive data in JWT\nasync login(user: User): Promise<{ accessToken: string }> {\n  const payload = {\n    sub: user.id,\n    email: user.email,\n    password: user.password, // NEVER include password!\n    ssn: user.ssn, // NEVER include sensitive data!\n    isAdmin: user.isAdmin, // Can be tampered if not verified\n  };\n  return { accessToken: this.jwtService.sign(payload) };\n}\n\n// Skip token validation\n@Injectable()\nexport class JwtStrategy extends PassportStrategy(Strategy) {\n  constructor() {\n    super({\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n      secretOrKey: 'my-secret',\n    });\n  }\n\n  async validate(payload: any): Promise<any> {\n    return payload; // No validation of user existence\n  }\n}\n```\n\n**Correct (secure JWT with refresh tokens):**\n\n```typescript\n// Secure JWT configuration\n@Module({\n  imports: [\n    JwtModule.registerAsync({\n      imports: [ConfigModule],\n      inject: [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        secret: config.get<string>('JWT_SECRET'),\n        signOptions: {\n          expiresIn: '15m', // Short-lived access tokens\n          issuer: config.get<string>('JWT_ISSUER'),\n          audience: config.get<string>('JWT_AUDIENCE'),\n        },\n      }),\n    }),\n    PassportModule.register({ defaultStrategy: 'jwt' }),\n  ],\n})\nexport class AuthModule {}\n\n// Minimal JWT payload\n@Injectable()\nexport class AuthService {\n  async login(user: User): Promise<TokenResponse> {\n    // Only include necessary, non-sensitive data\n    const payload: JwtPayload = {\n      sub: user.id,\n      email: user.email,\n      roles: user.roles,\n      iat: Math.floor(Date.now() / 1000),\n    };\n\n    const accessToken = this.jwtService.sign(payload);\n    const refreshToken = await this.createRefreshToken(user.id);\n\n    return { accessToken, refreshToken, expiresIn: 900 };\n  }\n\n  private async createRefreshToken(userId: string): Promise<string> {\n    const token = randomBytes(32).toString('hex');\n    const hashedToken = await bcrypt.hash(token, 10);\n\n    await this.refreshTokenRepo.save({\n      userId,\n      token: hashedToken,\n      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days\n    });\n\n    return token;\n  }\n}\n\n// Proper JWT strategy with validation\n@Injectable()\nexport class JwtStrategy extends PassportStrategy(Strategy) {\n  constructor(\n    private config: ConfigService,\n    private usersService: UsersService,\n  ) {\n    super({\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n      secretOrKey: config.get<string>('JWT_SECRET'),\n      ignoreExpiration: false,\n      issuer: config.get<string>('JWT_ISSUER'),\n      audience: config.get<string>('JWT_AUDIENCE'),\n    });\n  }\n\n  async validate(payload: JwtPayload): Promise<User> {\n    // Verify user still exists and is active\n    const user = await this.usersService.findById(payload.sub);\n\n    if (!user || !user.isActive) {\n      throw new UnauthorizedException('User not found or inactive');\n    }\n\n    // Verify token wasn't issued before password change\n    if (user.passwordChangedAt) {\n      const tokenIssuedAt = new Date(payload.iat * 1000);\n      if (tokenIssuedAt < user.passwordChangedAt) {\n        throw new UnauthorizedException('Token invalidated by password change');\n      }\n    }\n\n    return user;\n  }\n}\n```\n\nReference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)\n\n---\n\n### 4.2 Implement Rate Limiting\n\n**Impact: HIGH** — Protects against abuse and ensures fair resource usage\n\nUse `@nestjs/throttler` to limit request rates per client. Apply different limits for different endpoints - stricter for auth endpoints, more relaxed for read operations. Consider using Redis for distributed rate limiting in clustered deployments.\n\n**Incorrect (no rate limiting on sensitive endpoints):**\n\n```typescript\n// No rate limiting on sensitive endpoints\n@Controller('auth')\nexport class AuthController {\n  @Post('login')\n  async login(@Body() dto: LoginDto): Promise<TokenResponse> {\n    // Attackers can brute-force credentials\n    return this.authService.login(dto);\n  }\n\n  @Post('forgot-password')\n  async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {\n    // Can be abused to spam users with emails\n    return this.authService.sendResetEmail(dto.email);\n  }\n}\n\n// Same limits for all endpoints\n@UseGuards(ThrottlerGuard)\n@Controller('api')\nexport class ApiController {\n  @Get('public-data')\n  async getPublic() {} // Should allow more requests\n\n  @Post('process-payment')\n  async payment() {} // Should be more restrictive\n}\n```\n\n**Correct (configured throttler with endpoint-specific limits):**\n\n```typescript\n// Configure throttler globally with multiple limits\nimport { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';\n\n@Module({\n  imports: [\n    ThrottlerModule.forRoot([\n      {\n        name: 'short',\n        ttl: 1000, // 1 second\n        limit: 3, // 3 requests per second\n      },\n      {\n        name: 'medium',\n        ttl: 10000, // 10 seconds\n        limit: 20, // 20 requests per 10 seconds\n      },\n      {\n        name: 'long',\n        ttl: 60000, // 1 minute\n        limit: 100, // 100 requests per minute\n      },\n    ]),\n  ],\n  providers: [\n    {\n      provide: APP_GUARD,\n      useClass: ThrottlerGuard,\n    },\n  ],\n})\nexport class AppModule {}\n\n// Override limits per endpoint\n@Controller('auth')\nexport class AuthController {\n  @Post('login')\n  @Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute\n  async login(@Body() dto: LoginDto): Promise<TokenResponse> {\n    return this.authService.login(dto);\n  }\n\n  @Post('forgot-password')\n  @Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour\n  async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {\n    return this.authService.sendResetEmail(dto.email);\n  }\n}\n\n// Skip throttling for certain routes\n@Controller('health')\nexport class HealthController {\n  @Get()\n  @SkipThrottle()\n  check(): string {\n    return 'OK';\n  }\n}\n\n// Custom throttle per user type\n@Injectable()\nexport class CustomThrottlerGuard extends ThrottlerGuard {\n  protected async getTracker(req: Request): Promise<string> {\n    // Use user ID if authenticated, IP otherwise\n    return req.user?.id || req.ip;\n  }\n\n  protected async getLimit(context: ExecutionContext): Promise<number> {\n    const request = context.switchToHttp().getRequest();\n\n    // Higher limits for authenticated users\n    if (request.user) {\n      return request.user.isPremium ? 1000 : 200;\n    }\n\n    return 50; // Anonymous users\n  }\n}\n```\n\nReference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)\n\n---\n\n### 4.3 Sanitize Output to Prevent XSS\n\n**Impact: HIGH** — XSS vulnerabilities can compromise user sessions and data\n\nWhile NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers.\n\n**Incorrect (storing raw HTML without sanitization):**\n\n```typescript\n// Store raw HTML from users\n@Injectable()\nexport class CommentsService {\n  async create(dto: CreateCommentDto): Promise<Comment> {\n    // User can inject: <script>steal(document.cookie)</script>\n    return this.repo.save({\n      content: dto.content, // Raw, unsanitized\n      authorId: dto.authorId,\n    });\n  }\n}\n\n// Return HTML without sanitization\n@Controller('pages')\nexport class PagesController {\n  @Get(':slug')\n  @Header('Content-Type', 'text/html')\n  async getPage(@Param('slug') slug: string): Promise<string> {\n    const page = await this.pagesService.findBySlug(slug);\n    // If page.content contains user input, XSS is possible\n    return `<html><body>${page.content}</body></html>`;\n  }\n}\n\n// Reflect user input in errors\n@Get(':id')\nasync findOne(@Param('id') id: string): Promise<User> {\n  const user = await this.repo.findOne({ where: { id } });\n  if (!user) {\n    // XSS if id contains malicious content and error is rendered\n    throw new NotFoundException(`User ${id} not found`);\n  }\n  return user;\n}\n```\n\n**Correct (sanitize content and use proper headers):**\n\n```typescript\n// Sanitize HTML content before storage\nimport * as sanitizeHtml from 'sanitize-html';\n\n@Injectable()\nexport class CommentsService {\n  private readonly sanitizeOptions: sanitizeHtml.IOptions = {\n    allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],\n    allowedAttributes: {\n      a: ['href', 'title'],\n    },\n    allowedSchemes: ['http', 'https', 'mailto'],\n  };\n\n  async create(dto: CreateCommentDto): Promise<Comment> {\n    return this.repo.save({\n      content: sanitizeHtml(dto.content, this.sanitizeOptions),\n      authorId: dto.authorId,\n    });\n  }\n}\n\n// Use validation pipe to strip HTML\nimport { Transform } from 'class-transformer';\n\nexport class CreatePostDto {\n  @IsString()\n  @MaxLength(1000)\n  @Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))\n  title: string;\n\n  @IsString()\n  @Transform(({ value }) =>\n    sanitizeHtml(value, {\n      allowedTags: ['p', 'br', 'b', 'i', 'a'],\n      allowedAttributes: { a: ['href'] },\n    }),\n  )\n  content: string;\n}\n\n// Set proper Content-Type headers\n@Controller('api')\nexport class ApiController {\n  @Get('data')\n  @Header('Content-Type', 'application/json')\n  async getData(): Promise<DataResponse> {\n    // JSON response - browser won't execute scripts\n    return this.service.getData();\n  }\n}\n\n// Sanitize error messages\n@Get(':id')\nasync findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {\n  const user = await this.repo.findOne({ where: { id } });\n  if (!user) {\n    // UUID validation ensures safe format\n    throw new NotFoundException('User not found');\n  }\n  return user;\n}\n\n// Use Helmet for CSP headers\nimport helmet from 'helmet';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  app.use(\n    helmet({\n      contentSecurityPolicy: {\n        directives: {\n          defaultSrc: [\"'self'\"],\n          scriptSrc: [\"'self'\"],\n          styleSrc: [\"'self'\", \"'unsafe-inline'\"],\n          imgSrc: [\"'self'\", 'data:', 'https:'],\n        },\n      },\n    }),\n  );\n\n  await app.listen(3000);\n}\n```\n\nReference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)\n\n---\n\n### 4.4 Use Guards for Authentication and Authorization\n\n**Impact: HIGH** — Enforces access control before handlers execute\n\nGuards determine whether a request should be handled based on authentication state, roles, permissions, or other conditions. They run after middleware but before pipes and interceptors, making them ideal for access control. Use guards instead of manual checks in controllers.\n\n**Incorrect (manual auth checks in every handler):**\n\n```typescript\n// Manual auth checks in every handler\n@Controller('admin')\nexport class AdminController {\n  @Get('users')\n  async getUsers(@Request() req) {\n    if (!req.user) {\n      throw new UnauthorizedException();\n    }\n    if (!req.user.roles.includes('admin')) {\n      throw new ForbiddenException();\n    }\n    return this.adminService.getUsers();\n  }\n\n  @Delete('users/:id')\n  async deleteUser(@Request() req, @Param('id') id: string) {\n    if (!req.user) {\n      throw new UnauthorizedException();\n    }\n    if (!req.user.roles.includes('admin')) {\n      throw new ForbiddenException();\n    }\n    return this.adminService.deleteUser(id);\n  }\n}\n```\n\n**Correct (guards with declarative decorators):**\n\n```typescript\n// JWT Auth Guard\n@Injectable()\nexport class JwtAuthGuard implements CanActivate {\n  constructor(\n    private jwtService: JwtService,\n    private reflector: Reflector,\n  ) {}\n\n  async canActivate(context: ExecutionContext): Promise<boolean> {\n    // Check for @Public() decorator\n    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);\n    if (isPublic) return true;\n\n    const request = context.switchToHttp().getRequest();\n    const token = this.extractToken(request);\n\n    if (!token) {\n      throw new UnauthorizedException('No token provided');\n    }\n\n    try {\n      request.user = await this.jwtService.verifyAsync(token);\n      return true;\n    } catch {\n      throw new UnauthorizedException('Invalid token');\n    }\n  }\n\n  private extractToken(request: Request): string | undefined {\n    const [type, token] = request.headers.authorization?.split(' ') ?? [];\n    return type === 'Bearer' ? token : undefined;\n  }\n}\n\n// Roles Guard\n@Injectable()\nexport class RolesGuard implements CanActivate {\n  constructor(private reflector: Reflector) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);\n\n    if (!requiredRoles) return true;\n\n    const { user } = context.switchToHttp().getRequest();\n    return requiredRoles.some(role => user.roles?.includes(role));\n  }\n}\n\n// Decorators\nexport const Public = () => SetMetadata('isPublic', true);\nexport const Roles = (...roles: Role[]) => SetMetadata('roles', roles);\n\n// Register guards globally\n@Module({\n  providers: [\n    { provide: APP_GUARD, useClass: JwtAuthGuard },\n    { provide: APP_GUARD, useClass: RolesGuard },\n  ],\n})\nexport class AppModule {}\n\n// Clean controller\n@Controller('admin')\n@Roles(Role.Admin) // Applied to all routes\nexport class AdminController {\n  @Get('users')\n  getUsers(): Promise<User[]> {\n    return this.adminService.getUsers();\n  }\n\n  @Delete('users/:id')\n  deleteUser(@Param('id') id: string): Promise<void> {\n    return this.adminService.deleteUser(id);\n  }\n\n  @Public() // Override: no auth required\n  @Get('health')\n  health() {\n    return { status: 'ok' };\n  }\n}\n```\n\nReference: [NestJS Guards](https://docs.nestjs.com/guards)\n\n---\n\n### 4.5 Validate All Input with DTOs and Pipes\n\n**Impact: HIGH** — First line of defense against attacks\n\nAlways validate incoming data using class-validator decorators on DTOs and the global ValidationPipe. Never trust user input. Validate all request bodies, query parameters, and route parameters before processing.\n\n**Incorrect (trust raw input without validation):**\n\n```typescript\n// Trust raw input without validation\n@Controller('users')\nexport class UsersController {\n  @Post()\n  create(@Body() body: any) {\n    // body could contain anything - SQL injection, XSS, etc.\n    return this.usersService.create(body);\n  }\n\n  @Get()\n  findAll(@Query() query: any) {\n    // query.limit could be \"'; DROP TABLE users; --\"\n    return this.usersService.findAll(query.limit);\n  }\n}\n\n// DTOs without validation decorators\nexport class CreateUserDto {\n  name: string; // No validation\n  email: string; // Could be \"not-an-email\"\n  age: number; // Could be \"abc\" or -999\n}\n```\n\n**Correct (validated DTOs with global ValidationPipe):**\n\n```typescript\n// Enable ValidationPipe globally in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  app.useGlobalPipes(\n    new ValidationPipe({\n      whitelist: true, // Strip unknown properties\n      forbidNonWhitelisted: true, // Throw on unknown properties\n      transform: true, // Auto-transform to DTO types\n      transformOptions: {\n        enableImplicitConversion: true,\n      },\n    }),\n  );\n\n  await app.listen(3000);\n}\n\n// Create well-validated DTOs\nimport {\n  IsString,\n  IsEmail,\n  IsInt,\n  Min,\n  Max,\n  IsOptional,\n  MinLength,\n  MaxLength,\n  Matches,\n  IsNotEmpty,\n} from 'class-validator';\nimport { Transform, Type } from 'class-transformer';\n\nexport class CreateUserDto {\n  @IsString()\n  @IsNotEmpty()\n  @MinLength(2)\n  @MaxLength(100)\n  @Transform(({ value }) => value?.trim())\n  name: string;\n\n  @IsEmail()\n  @Transform(({ value }) => value?.toLowerCase().trim())\n  email: string;\n\n  @IsInt()\n  @Min(0)\n  @Max(150)\n  age: number;\n\n  @IsString()\n  @MinLength(8)\n  @MaxLength(100)\n  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/, {\n    message: 'Password must contain uppercase, lowercase, and number',\n  })\n  password: string;\n}\n\n// Query DTO with defaults and transformation\nexport class FindUsersQueryDto {\n  @IsOptional()\n  @IsString()\n  @MaxLength(100)\n  search?: string;\n\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  @Max(100)\n  limit: number = 20;\n\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(0)\n  offset: number = 0;\n}\n\n// Param validation\nexport class UserIdParamDto {\n  @IsUUID('4')\n  id: string;\n}\n\n@Controller('users')\nexport class UsersController {\n  @Post()\n  create(@Body() dto: CreateUserDto): Promise<User> {\n    // dto is guaranteed to be valid\n    return this.usersService.create(dto);\n  }\n\n  @Get()\n  findAll(@Query() query: FindUsersQueryDto): Promise<User[]> {\n    // query.limit is a number, query.search is sanitized\n    return this.usersService.findAll(query);\n  }\n\n  @Get(':id')\n  findOne(@Param() params: UserIdParamDto): Promise<User> {\n    // params.id is a valid UUID\n    return this.usersService.findById(params.id);\n  }\n}\n```\n\nReference: [NestJS Validation](https://docs.nestjs.com/techniques/validation)\n\n---\n\n## 5. Performance\n\n**Section Impact: HIGH**\n\n### 5.1 Use Async Lifecycle Hooks Correctly\n\n**Impact: HIGH** — Improper async handling blocks application startup\n\nNestJS lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.) support async operations. However, misusing them can block application startup or cause race conditions. Understand the lifecycle order and use hooks appropriately.\n\n**Incorrect (fire-and-forget async without await):**\n\n```typescript\n// Fire-and-forget async without await\n@Injectable()\nexport class DatabaseService implements OnModuleInit {\n  onModuleInit() {\n    // This runs but doesn't block - app starts before DB is ready!\n    this.connect();\n  }\n\n  private async connect() {\n    await this.pool.connect();\n    console.log('Database connected');\n  }\n}\n\n// Heavy blocking operations in constructor\n@Injectable()\nexport class ConfigService {\n  private config: Config;\n\n  constructor() {\n    // BLOCKS entire module instantiation synchronously\n    this.config = fs.readFileSync('config.json');\n  }\n}\n```\n\n**Correct (return promises from async hooks):**\n\n```typescript\n// Return promise from async hooks\n@Injectable()\nexport class DatabaseService implements OnModuleInit {\n  private pool: Pool;\n\n  async onModuleInit(): Promise<void> {\n    // NestJS waits for this to complete before continuing\n    await this.pool.connect();\n    console.log('Database connected');\n  }\n\n  async onModuleDestroy(): Promise<void> {\n    // Clean up resources on shutdown\n    await this.pool.end();\n    console.log('Database disconnected');\n  }\n}\n\n// Use onApplicationBootstrap for cross-module dependencies\n@Injectable()\nexport class CacheWarmerService implements OnApplicationBootstrap {\n  constructor(\n    private cache: CacheService,\n    private products: ProductsService,\n  ) {}\n\n  async onApplicationBootstrap(): Promise<void> {\n    // All modules are initialized, safe to warm cache\n    const products = await this.products.findPopular();\n    await this.cache.warmup(products);\n  }\n}\n\n// Heavy init in async hooks, not constructor\n@Injectable()\nexport class ConfigService implements OnModuleInit {\n  private config: Config;\n\n  constructor() {\n    // Keep constructor synchronous and fast\n  }\n\n  async onModuleInit(): Promise<void> {\n    // Async loading in lifecycle hook\n    this.config = await this.loadConfig();\n  }\n\n  private async loadConfig(): Promise<Config> {\n    const file = await fs.promises.readFile('config.json');\n    return JSON.parse(file.toString());\n  }\n\n  get<T>(key: string): T {\n    return this.config[key];\n  }\n}\n\n// Enable shutdown hooks in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling\n  await app.listen(3000);\n}\n```\n\nReference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)\n\n---\n\n### 5.2 Use Lazy Loading for Large Modules\n\n**Impact: MEDIUM** — Improves startup time for large applications\n\nNestJS supports lazy-loading modules, which defers initialization until first use. This is valuable for large applications where some features are rarely used, serverless deployments where cold start time matters, or when certain modules have heavy initialization costs.\n\n**Incorrect (loading everything eagerly):**\n\n```typescript\n// Load everything eagerly in a large app\n@Module({\n  imports: [\n    UsersModule,\n    OrdersModule,\n    PaymentsModule,\n    ReportsModule, // Heavy, rarely used\n    AnalyticsModule, // Heavy, rarely used\n    AdminModule, // Only admins use this\n    LegacyModule, // Migration module, rarely used\n    BulkImportModule, // Used once a month\n  ],\n})\nexport class AppModule {}\n\n// All modules initialize at startup, even if never used\n// Slow cold starts in serverless\n// Memory wasted on unused modules\n```\n\n**Correct (lazy load rarely-used modules):**\n\n```typescript\n// Use LazyModuleLoader for optional modules\nimport { LazyModuleLoader } from '@nestjs/core';\n\n@Injectable()\nexport class ReportsService {\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  async generateReport(type: string): Promise<Report> {\n    // Load module only when needed\n    const { ReportsModule } = await import('./reports/reports.module');\n    const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule);\n\n    const reportsService = moduleRef.get(ReportsGeneratorService);\n    return reportsService.generate(type);\n  }\n}\n\n// Lazy load admin features with caching\n@Injectable()\nexport class AdminService {\n  private adminModule: ModuleRef | null = null;\n\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  private async getAdminModule(): Promise<ModuleRef> {\n    if (!this.adminModule) {\n      const { AdminModule } = await import('./admin/admin.module');\n      this.adminModule = await this.lazyModuleLoader.load(() => AdminModule);\n    }\n    return this.adminModule;\n  }\n\n  async runAdminTask(task: string): Promise<void> {\n    const moduleRef = await this.getAdminModule();\n    const taskRunner = moduleRef.get(AdminTaskRunner);\n    await taskRunner.run(task);\n  }\n}\n\n// Reusable lazy loader service\n@Injectable()\nexport class ModuleLoaderService {\n  private loadedModules = new Map<string, ModuleRef>();\n\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {\n    if (!this.loadedModules.has(key)) {\n      const module = await importFn();\n      const moduleType = 'default' in module ? module.default : module;\n      const moduleRef = await this.lazyModuleLoader.load(() => moduleType);\n      this.loadedModules.set(key, moduleRef);\n    }\n    return this.loadedModules.get(key)!;\n  }\n}\n\n// Preload modules in background after startup\n@Injectable()\nexport class ModulePreloader implements OnApplicationBootstrap {\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  async onApplicationBootstrap(): Promise<void> {\n    setTimeout(async () => {\n      await this.preloadModule(() => import('./reports/reports.module'));\n    }, 5000); // 5 seconds after startup\n  }\n\n  private async preloadModule(importFn: () => Promise<any>): Promise<void> {\n    try {\n      const module = await importFn();\n      const moduleType = module.default || Object.values(module)[0];\n      await this.lazyModuleLoader.load(() => moduleType);\n    } catch (error) {\n      console.warn('Failed to preload module', error);\n    }\n  }\n}\n```\n\nReference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules)\n\n---\n\n### 5.3 Optimize Database Queries\n\n**Impact: HIGH** — Database queries are typically the largest source of latency\n\nSelect only needed columns, use proper indexes, avoid over-fetching relations, and consider query performance when designing your data access. Most API slowness traces back to inefficient database queries.\n\n**Incorrect (over-fetching data and missing indexes):**\n\n```typescript\n// Select everything when you need few fields\n@Injectable()\nexport class UsersService {\n  async findAllEmails(): Promise<string[]> {\n    const users = await this.repo.find();\n    // Fetches ALL columns for ALL users\n    return users.map(u => u.email);\n  }\n\n  async getUserSummary(id: string): Promise<UserSummary> {\n    const user = await this.repo.findOne({\n      where: { id },\n      relations: ['posts', 'posts.comments', 'posts.comments.author', 'followers'],\n    });\n    // Over-fetches massive relation tree\n    return { name: user.name, postCount: user.posts.length };\n  }\n}\n\n// No indexes on frequently queried columns\n@Entity()\nexport class Order {\n  @Column()\n  userId: string; // No index - full table scan on every lookup\n\n  @Column()\n  status: string; // No index - slow status filtering\n}\n```\n\n**Correct (select only needed data with proper indexes):**\n\n```typescript\n// Select only needed columns\n@Injectable()\nexport class UsersService {\n  async findAllEmails(): Promise<string[]> {\n    const users = await this.repo.find({\n      select: ['email'], // Only fetch email column\n    });\n    return users.map(u => u.email);\n  }\n\n  // Use QueryBuilder for complex selections\n  async getUserSummary(id: string): Promise<UserSummary> {\n    return this.repo\n      .createQueryBuilder('user')\n      .select('user.name', 'name')\n      .addSelect('COUNT(post.id)', 'postCount')\n      .leftJoin('user.posts', 'post')\n      .where('user.id = :id', { id })\n      .groupBy('user.id')\n      .getRawOne();\n  }\n\n  // Fetch relations only when needed\n  async getFullProfile(id: string): Promise<User> {\n    return this.repo.findOne({\n      where: { id },\n      relations: ['posts'], // Only immediate relation\n      select: {\n        id: true,\n        name: true,\n        email: true,\n        posts: {\n          id: true,\n          title: true,\n        },\n      },\n    });\n  }\n}\n\n// Add indexes on frequently queried columns\n@Entity()\n@Index(['userId'])\n@Index(['status'])\n@Index(['createdAt'])\n@Index(['userId', 'status']) // Composite index for common query pattern\nexport class Order {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Column()\n  userId: string;\n\n  @Column()\n  status: string;\n\n  @CreateDateColumn()\n  createdAt: Date;\n}\n\n// Always paginate large datasets\n@Injectable()\nexport class OrdersService {\n  async findAll(page = 1, limit = 20): Promise<PaginatedResult<Order>> {\n    const [items, total] = await this.repo.findAndCount({\n      skip: (page - 1) * limit,\n      take: limit,\n      order: { createdAt: 'DESC' },\n    });\n\n    return {\n      items,\n      meta: {\n        page,\n        limit,\n        total,\n        totalPages: Math.ceil(total / limit),\n      },\n    };\n  }\n}\n```\n\nReference: [TypeORM Query Builder](https://typeorm.io/select-query-builder)\n\n---\n\n### 5.4 Use Caching Strategically\n\n**Impact: HIGH** — Dramatically reduces database load and response times\n\nImplement caching for expensive operations, frequently accessed data, and external API calls. Use NestJS CacheModule with appropriate TTLs and cache invalidation strategies. Don't cache everything - focus on high-impact areas.\n\n**Incorrect (no caching or caching everything):**\n\n```typescript\n// No caching for expensive, repeated queries\n@Injectable()\nexport class ProductsService {\n  async getPopular(): Promise<Product[]> {\n    // Runs complex aggregation query EVERY request\n    return this.productsRepo\n      .createQueryBuilder('p')\n      .leftJoin('p.orders', 'o')\n      .select('p.*, COUNT(o.id) as orderCount')\n      .groupBy('p.id')\n      .orderBy('orderCount', 'DESC')\n      .limit(20)\n      .getMany();\n  }\n}\n\n// Cache everything without thought\n@Injectable()\nexport class UsersService {\n  @CacheKey('users')\n  @CacheTTL(3600)\n  @UseInterceptors(CacheInterceptor)\n  async findAll(): Promise<User[]> {\n    // Caching user list for 1 hour is wrong if data changes frequently\n    return this.usersRepo.find();\n  }\n}\n```\n\n**Correct (strategic caching with proper invalidation):**\n\n```typescript\n// Setup caching module\n@Module({\n  imports: [\n    CacheModule.registerAsync({\n      imports: [ConfigModule],\n      inject: [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        stores: [new KeyvRedis(config.get('REDIS_URL'))],\n        ttl: 60 * 1000, // Default 60s\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Manual caching for granular control\n@Injectable()\nexport class ProductsService {\n  constructor(\n    @Inject(CACHE_MANAGER) private cache: Cache,\n    private productsRepo: ProductRepository,\n  ) {}\n\n  async getPopular(): Promise<Product[]> {\n    const cacheKey = 'products:popular';\n\n    // Try cache first\n    const cached = await this.cache.get<Product[]>(cacheKey);\n    if (cached) return cached;\n\n    // Cache miss - fetch and cache\n    const products = await this.fetchPopularProducts();\n    await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL\n    return products;\n  }\n\n  // Invalidate cache on changes\n  async updateProduct(id: string, dto: UpdateProductDto): Promise<Product> {\n    const product = await this.productsRepo.save({ id, ...dto });\n    await this.cache.del('products:popular'); // Invalidate\n    return product;\n  }\n}\n\n// Decorator-based caching with auto-interceptor\n@Controller('categories')\n@UseInterceptors(CacheInterceptor)\nexport class CategoriesController {\n  @Get()\n  @CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change\n  findAll(): Promise<Category[]> {\n    return this.categoriesService.findAll();\n  }\n\n  @Get(':id')\n  @CacheTTL(60 * 1000) // 1 minute\n  @CacheKey('category')\n  findOne(@Param('id') id: string): Promise<Category> {\n    return this.categoriesService.findOne(id);\n  }\n}\n\n// Event-based cache invalidation\n@Injectable()\nexport class CacheInvalidationService {\n  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}\n\n  @OnEvent('product.created')\n  @OnEvent('product.updated')\n  @OnEvent('product.deleted')\n  async invalidateProductCaches(event: ProductEvent) {\n    await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);\n  }\n}\n```\n\nReference: [NestJS Caching](https://docs.nestjs.com/techniques/caching)\n\n---\n\n## 6. Testing\n\n**Section Impact: MEDIUM-HIGH**\n\n### 6.1 Use Supertest for E2E Testing\n\n**Impact: HIGH** — Validates the full request/response cycle\n\nEnd-to-end tests use Supertest to make real HTTP requests against your NestJS application. They test the full stack including middleware, guards, pipes, and interceptors. E2E tests catch integration issues that unit tests miss.\n\n**Incorrect (no proper E2E setup or teardown):**\n\n```typescript\n// Only unit test controllers\ndescribe('UsersController', () => {\n  it('should return users', async () => {\n    const service = { findAll: jest.fn().mockResolvedValue([]) };\n    const controller = new UsersController(service as any);\n\n    const result = await controller.findAll();\n\n    expect(result).toEqual([]);\n    // Doesn't test: routes, guards, pipes, serialization\n  });\n});\n\n// E2E tests without proper setup/teardown\ndescribe('Users API', () => {\n  it('should create user', async () => {\n    const app = await NestFactory.create(AppModule);\n    // No proper initialization\n    // No cleanup after test\n    // Hits real database\n  });\n});\n```\n\n**Correct (proper E2E setup with Supertest):**\n\n```typescript\n// Proper E2E test setup\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { INestApplication, ValidationPipe } from '@nestjs/common';\nimport * as request from 'supertest';\nimport { AppModule } from '../src/app.module';\n\ndescribe('UsersController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const moduleFixture: TestingModule = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n\n    // Apply same config as production\n    app.useGlobalPipes(\n      new ValidationPipe({\n        whitelist: true,\n        transform: true,\n        forbidNonWhitelisted: true,\n      }),\n    );\n\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  describe('/users (POST)', () => {\n    it('should create a user', () => {\n      return request(app.getHttpServer())\n        .post('/users')\n        .send({ name: 'John', email: 'john@test.com' })\n        .expect(201)\n        .expect(res => {\n          expect(res.body).toHaveProperty('id');\n          expect(res.body.name).toBe('John');\n          expect(res.body.email).toBe('john@test.com');\n        });\n    });\n\n    it('should return 400 for invalid email', () => {\n      return request(app.getHttpServer())\n        .post('/users')\n        .send({ name: 'John', email: 'invalid-email' })\n        .expect(400)\n        .expect(res => {\n          expect(res.body.message).toContain('email');\n        });\n    });\n  });\n\n  describe('/users/:id (GET)', () => {\n    it('should return 404 for non-existent user', () => {\n      return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);\n    });\n  });\n});\n\n// Testing with authentication\ndescribe('Protected Routes (e2e)', () => {\n  let app: INestApplication;\n  let authToken: string;\n\n  beforeAll(async () => {\n    const moduleFixture = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));\n    await app.init();\n\n    // Get auth token\n    const loginResponse = await request(app.getHttpServer())\n      .post('/auth/login')\n      .send({ email: 'test@test.com', password: 'password' });\n\n    authToken = loginResponse.body.accessToken;\n  });\n\n  it('should return 401 without token', () => {\n    return request(app.getHttpServer()).get('/users/me').expect(401);\n  });\n\n  it('should return user profile with valid token', () => {\n    return request(app.getHttpServer())\n      .get('/users/me')\n      .set('Authorization', `Bearer ${authToken}`)\n      .expect(200)\n      .expect(res => {\n        expect(res.body.email).toBe('test@test.com');\n      });\n  });\n});\n\n// Database isolation for E2E tests\ndescribe('Orders API (e2e)', () => {\n  let app: INestApplication;\n  let dataSource: DataSource;\n\n  beforeAll(async () => {\n    const moduleFixture = await Test.createTestingModule({\n      imports: [\n        ConfigModule.forRoot({\n          envFilePath: '.env.test', // Test database config\n        }),\n        AppModule,\n      ],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n    dataSource = moduleFixture.get(DataSource);\n    await app.init();\n  });\n\n  beforeEach(async () => {\n    // Clean database between tests\n    await dataSource.synchronize(true);\n  });\n\n  afterAll(async () => {\n    await dataSource.destroy();\n    await app.close();\n  });\n});\n```\n\nReference: [NestJS E2E Testing](https://docs.nestjs.com/fundamentals/testing#end-to-end-testing)\n\n---\n\n### 6.2 Mock External Services in Tests\n\n**Impact: HIGH** — Ensures fast, reliable, deterministic tests\n\nNever call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors.\n\n**Incorrect (calling real APIs and databases):**\n\n```typescript\n// Call real APIs in tests\ndescribe('PaymentService', () => {\n  it('should process payment', async () => {\n    const service = new PaymentService(new StripeClient(realApiKey));\n    // Hits real Stripe API!\n    const result = await service.charge('tok_visa', 1000);\n    // Slow, costs money, flaky\n  });\n});\n\n// Use real database\ndescribe('UsersService', () => {\n  beforeEach(async () => {\n    await connection.query('DELETE FROM users'); // Modifies real DB\n  });\n\n  it('should create user', async () => {\n    const user = await service.create({ email: 'test@test.com' });\n    // Side effects on shared database\n  });\n});\n\n// Incomplete mocks\nconst mockHttpService = {\n  get: jest.fn().mockResolvedValue({ data: {} }),\n  // Missing error scenarios, missing other methods\n};\n```\n\n**Correct (mock all external dependencies):**\n\n```typescript\n// Mock HTTP service properly\ndescribe('WeatherService', () => {\n  let service: WeatherService;\n  let httpService: jest.Mocked<HttpService>;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        WeatherService,\n        {\n          provide: HttpService,\n          useValue: {\n            get: jest.fn(),\n            post: jest.fn(),\n          },\n        },\n      ],\n    }).compile();\n\n    service = module.get(WeatherService);\n    httpService = module.get(HttpService);\n  });\n\n  it('should return weather data', async () => {\n    const mockResponse = {\n      data: { temperature: 72, humidity: 45 },\n      status: 200,\n      statusText: 'OK',\n      headers: {},\n      config: {},\n    };\n\n    httpService.get.mockReturnValue(of(mockResponse));\n\n    const result = await service.getWeather('NYC');\n\n    expect(result).toEqual({ temperature: 72, humidity: 45 });\n  });\n\n  it('should handle API timeout', async () => {\n    httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));\n\n    await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');\n  });\n\n  it('should handle rate limiting', async () => {\n    httpService.get.mockReturnValue(\n      throwError(() => ({\n        response: { status: 429, data: { message: 'Rate limited' } },\n      })),\n    );\n\n    await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);\n  });\n});\n\n// Mock repository instead of database\ndescribe('UsersService', () => {\n  let service: UsersService;\n  let repo: jest.Mocked<Repository<User>>;\n\n  beforeEach(async () => {\n    const mockRepo = {\n      find: jest.fn(),\n      findOne: jest.fn(),\n      save: jest.fn(),\n      delete: jest.fn(),\n      createQueryBuilder: jest.fn(),\n    };\n\n    const module = await Test.createTestingModule({\n      providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],\n    }).compile();\n\n    service = module.get(UsersService);\n    repo = module.get(getRepositoryToken(User));\n  });\n\n  it('should find user by id', async () => {\n    const mockUser = { id: '1', name: 'John', email: 'john@test.com' };\n    repo.findOne.mockResolvedValue(mockUser);\n\n    const result = await service.findById('1');\n\n    expect(result).toEqual(mockUser);\n    expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });\n  });\n});\n\n// Create mock factory for complex SDKs\nfunction createMockStripe(): jest.Mocked<Stripe> {\n  return {\n    paymentIntents: {\n      create: jest.fn(),\n      retrieve: jest.fn(),\n      confirm: jest.fn(),\n      cancel: jest.fn(),\n    },\n    customers: {\n      create: jest.fn(),\n      retrieve: jest.fn(),\n    },\n  } as any;\n}\n\n// Mock time for time-dependent tests\ndescribe('TokenService', () => {\n  beforeEach(() => {\n    jest.useFakeTimers();\n    jest.setSystemTime(new Date('2024-01-15'));\n  });\n\n  afterEach(() => {\n    jest.useRealTimers();\n  });\n\n  it('should expire token after 1 hour', async () => {\n    const token = await service.createToken();\n\n    // Fast-forward time\n    jest.advanceTimersByTime(61 * 60 * 1000);\n\n    expect(await service.isValid(token)).toBe(false);\n  });\n});\n```\n\nReference: [Jest Mocking](https://jestjs.io/docs/mock-functions)\n\n---\n\n### 6.3 Use Testing Module for Unit Tests\n\n**Impact: HIGH** — Enables proper isolated testing with mocked dependencies\n\nUse `@nestjs/testing` module to create isolated test environments with mocked dependencies. This ensures your tests run fast, don't depend on external services, and properly test your business logic in isolation.\n\n**Incorrect (manual instantiation bypassing DI):**\n\n```typescript\n// Instantiate services manually without DI\ndescribe('UsersService', () => {\n  it('should create user', async () => {\n    // Manual instantiation bypasses DI\n    const repo = new UserRepository(); // Real repo!\n    const service = new UsersService(repo);\n\n    const user = await service.create({ name: 'Test' });\n    // This hits the real database!\n  });\n});\n\n// Test implementation details\ndescribe('UsersController', () => {\n  it('should call service', async () => {\n    const service = { create: jest.fn() };\n    const controller = new UsersController(service as any);\n\n    await controller.create({ name: 'Test' });\n\n    expect(service.create).toHaveBeenCalled(); // Tests implementation, not behavior\n  });\n});\n```\n\n**Correct (use Test.createTestingModule with mocked dependencies):**\n\n```typescript\n// Use Test.createTestingModule for proper DI\nimport { Test, TestingModule } from '@nestjs/testing';\n\ndescribe('UsersService', () => {\n  let service: UsersService;\n  let repo: jest.Mocked<UserRepository>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        UsersService,\n        {\n          provide: UserRepository,\n          useValue: {\n            save: jest.fn(),\n            findOne: jest.fn(),\n            find: jest.fn(),\n          },\n        },\n      ],\n    }).compile();\n\n    service = module.get<UsersService>(UsersService);\n    repo = module.get(UserRepository);\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe('create', () => {\n    it('should save and return user', async () => {\n      const dto = { name: 'John', email: 'john@test.com' };\n      const expectedUser = { id: '1', ...dto };\n\n      repo.save.mockResolvedValue(expectedUser);\n\n      const result = await service.create(dto);\n\n      expect(result).toEqual(expectedUser);\n      expect(repo.save).toHaveBeenCalledWith(dto);\n    });\n\n    it('should throw on duplicate email', async () => {\n      repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });\n\n      await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);\n    });\n  });\n\n  describe('findById', () => {\n    it('should return user when found', async () => {\n      const user = { id: '1', name: 'John' };\n      repo.findOne.mockResolvedValue(user);\n\n      const result = await service.findById('1');\n\n      expect(result).toEqual(user);\n    });\n\n    it('should throw NotFoundException when not found', async () => {\n      repo.findOne.mockResolvedValue(null);\n\n      await expect(service.findById('999')).rejects.toThrow(NotFoundException);\n    });\n  });\n});\n\n// Testing guards and interceptors\ndescribe('RolesGuard', () => {\n  let guard: RolesGuard;\n  let reflector: Reflector;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [RolesGuard, Reflector],\n    }).compile();\n\n    guard = module.get<RolesGuard>(RolesGuard);\n    reflector = module.get<Reflector>(Reflector);\n  });\n\n  it('should allow when no roles required', () => {\n    const context = createMockExecutionContext({ user: { roles: [] } });\n    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);\n\n    expect(guard.canActivate(context)).toBe(true);\n  });\n\n  it('should allow admin for admin-only route', () => {\n    const context = createMockExecutionContext({ user: { roles: ['admin'] } });\n    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);\n\n    expect(guard.canActivate(context)).toBe(true);\n  });\n});\n\nfunction createMockExecutionContext(request: Partial<Request>): ExecutionContext {\n  return {\n    switchToHttp: () => ({\n      getRequest: () => request,\n    }),\n    getHandler: () => jest.fn(),\n    getClass: () => jest.fn(),\n  } as ExecutionContext;\n}\n```\n\nReference: [NestJS Testing](https://docs.nestjs.com/fundamentals/testing)\n\n---\n\n## 7. Database & ORM\n\n**Section Impact: MEDIUM-HIGH**\n\n### 7.1 Avoid N+1 Query Problems\n\n**Impact: HIGH** — N+1 queries are one of the most common performance killers\n\nN+1 queries occur when you fetch a list of entities, then make an additional query for each entity to load related data. Use eager loading with `relations`, query builder joins, or DataLoader to batch queries efficiently.\n\n**Incorrect (lazy loading in loops causes N+1):**\n\n```typescript\n// Lazy loading in loops causes N+1\n@Injectable()\nexport class OrdersService {\n  async getOrdersWithItems(userId: string): Promise<Order[]> {\n    const orders = await this.orderRepo.find({ where: { userId } });\n    // 1 query for orders\n\n    for (const order of orders) {\n      // N additional queries - one per order!\n      order.items = await this.itemRepo.find({ where: { orderId: order.id } });\n    }\n\n    return orders;\n  }\n}\n\n// Accessing lazy relations without loading\n@Controller('users')\nexport class UsersController {\n  @Get()\n  async findAll(): Promise<User[]> {\n    const users = await this.userRepo.find();\n    // If User.posts is lazy-loaded, serializing triggers N queries\n    return users; // Each user.posts access = 1 query\n  }\n}\n```\n\n**Correct (use relations for eager loading):**\n\n```typescript\n// Use relations option for eager loading\n@Injectable()\nexport class OrdersService {\n  async getOrdersWithItems(userId: string): Promise<Order[]> {\n    // Single query with JOIN\n    return this.orderRepo.find({\n      where: { userId },\n      relations: ['items', 'items.product'],\n    });\n  }\n}\n\n// Use QueryBuilder for complex joins\n@Injectable()\nexport class UsersService {\n  async getUsersWithPostCounts(): Promise<UserWithPostCount[]> {\n    return this.userRepo\n      .createQueryBuilder('user')\n      .leftJoin('user.posts', 'post')\n      .select('user.id', 'id')\n      .addSelect('user.name', 'name')\n      .addSelect('COUNT(post.id)', 'postCount')\n      .groupBy('user.id')\n      .getRawMany();\n  }\n\n  async getActiveUsersWithPosts(): Promise<User[]> {\n    return this.userRepo\n      .createQueryBuilder('user')\n      .leftJoinAndSelect('user.posts', 'post')\n      .leftJoinAndSelect('post.comments', 'comment')\n      .where('user.isActive = :active', { active: true })\n      .andWhere('post.status = :status', { status: 'published' })\n      .getMany();\n  }\n}\n\n// Use find options for specific fields\nasync getOrderSummaries(userId: string): Promise<OrderSummary[]> {\n  return this.orderRepo.find({\n    where: { userId },\n    relations: ['items'],\n    select: {\n      id: true,\n      total: true,\n      status: true,\n      items: {\n        id: true,\n        quantity: true,\n        price: true,\n      },\n    },\n  });\n}\n\n// Use DataLoader for GraphQL to batch and cache queries\nimport DataLoader from 'dataloader';\n\n@Injectable({ scope: Scope.REQUEST })\nexport class PostsLoader {\n  constructor(private postsService: PostsService) {}\n\n  readonly batchPosts = new DataLoader<string, Post[]>(async (userIds) => {\n    // Single query for all users' posts\n    const posts = await this.postsService.findByUserIds([...userIds]);\n\n    // Group by userId\n    const postsMap = new Map<string, Post[]>();\n    for (const post of posts) {\n      const userPosts = postsMap.get(post.userId) || [];\n      userPosts.push(post);\n      postsMap.set(post.userId, userPosts);\n    }\n\n    // Return in same order as input\n    return userIds.map((id) => postsMap.get(id) || []);\n  });\n}\n\n// In resolver\n@ResolveField()\nasync posts(@Parent() user: User): Promise<Post[]> {\n  // DataLoader batches multiple calls into single query\n  return this.postsLoader.batchPosts.load(user.id);\n}\n\n// Enable query logging in development to detect N+1\nTypeOrmModule.forRoot({\n  logging: ['query', 'error'],\n  logger: 'advanced-console',\n});\n```\n\nReference: [TypeORM Relations](https://typeorm.io/relations)\n\n---\n\n### 7.2 Use Database Migrations\n\n**Impact: HIGH** — Enables safe, repeatable database schema changes\n\nNever use `synchronize: true` in production. Use migrations for all schema changes. Migrations provide version control for your database, enable safe rollbacks, and ensure consistency across all environments.\n\n**Incorrect (using synchronize or manual SQL):**\n\n```typescript\n// Use synchronize in production\nTypeOrmModule.forRoot({\n  type: 'postgres',\n  synchronize: true, // DANGEROUS in production!\n  // Can drop columns, tables, or data\n});\n\n// Manual SQL in production\n@Injectable()\nexport class DatabaseService {\n  async addColumn(): Promise<void> {\n    await this.dataSource.query('ALTER TABLE users ADD COLUMN age INT');\n    // No version control, no rollback, inconsistent across envs\n  }\n}\n\n// Modify entities without migration\n@Entity()\nexport class User {\n  @Column()\n  email: string;\n\n  @Column() // Added without migration\n  newField: string; // Will crash in production if synchronize is false\n}\n```\n\n**Correct (use migrations for all schema changes):**\n\n```typescript\n// Configure TypeORM for migrations\n// data-source.ts\nexport const dataSource = new DataSource({\n  type: 'postgres',\n  host: process.env.DB_HOST,\n  port: parseInt(process.env.DB_PORT),\n  username: process.env.DB_USERNAME,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n  entities: ['dist/**/*.entity.js'],\n  migrations: ['dist/migrations/*.js'],\n  synchronize: false, // Always false in production\n  migrationsRun: true, // Run migrations on startup\n});\n\n// app.module.ts\nTypeOrmModule.forRootAsync({\n  inject: [ConfigService],\n  useFactory: (config: ConfigService) => ({\n    type: 'postgres',\n    host: config.get('DB_HOST'),\n    synchronize: config.get('NODE_ENV') === 'development', // Only in dev\n    migrations: ['dist/migrations/*.js'],\n    migrationsRun: true,\n  }),\n});\n\n// migrations/1705312800000-AddUserAge.ts\nimport { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AddUserAge1705312800000 implements MigrationInterface {\n  name = 'AddUserAge1705312800000';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    // Add column with default to handle existing rows\n    await queryRunner.query(`\n      ALTER TABLE \"users\" ADD \"age\" integer DEFAULT 0\n    `);\n\n    // Add index for frequently queried columns\n    await queryRunner.query(`\n      CREATE INDEX \"IDX_users_age\" ON \"users\" (\"age\")\n    `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    // Always implement down for rollback\n    await queryRunner.query(`DROP INDEX \"IDX_users_age\"`);\n    await queryRunner.query(`ALTER TABLE \"users\" DROP COLUMN \"age\"`);\n  }\n}\n\n// Safe column rename (two-step)\nexport class RenameNameToFullName1705312900000 implements MigrationInterface {\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    // Step 1: Add new column\n    await queryRunner.query(`\n      ALTER TABLE \"users\" ADD \"full_name\" varchar(255)\n    `);\n\n    // Step 2: Copy data\n    await queryRunner.query(`\n      UPDATE \"users\" SET \"full_name\" = \"name\"\n    `);\n\n    // Step 3: Add NOT NULL constraint\n    await queryRunner.query(`\n      ALTER TABLE \"users\" ALTER COLUMN \"full_name\" SET NOT NULL\n    `);\n\n    // Step 4: Drop old column (after verifying app works)\n    await queryRunner.query(`\n      ALTER TABLE \"users\" DROP COLUMN \"name\"\n    `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"users\" ADD \"name\" varchar(255)`);\n    await queryRunner.query(`UPDATE \"users\" SET \"name\" = \"full_name\"`);\n    await queryRunner.query(`ALTER TABLE \"users\" DROP COLUMN \"full_name\"`);\n  }\n}\n```\n\nReference: [TypeORM Migrations](https://typeorm.io/migrations)\n\n---\n\n### 7.3 Use Transactions for Multi-Step Operations\n\n**Impact: HIGH** — Ensures data consistency in multi-step operations\n\nWhen multiple database operations must succeed or fail together, wrap them in a transaction. This prevents partial updates that leave your data in an inconsistent state. Use TypeORM's transaction APIs or the DataSource query runner for complex scenarios.\n\n**Incorrect (multiple saves without transaction):**\n\n```typescript\n// Multiple saves without transaction\n@Injectable()\nexport class OrdersService {\n  async createOrder(userId: string, items: OrderItem[]): Promise<Order> {\n    // If any step fails, data is inconsistent\n    const order = await this.orderRepo.save({ userId, status: 'pending' });\n\n    for (const item of items) {\n      await this.orderItemRepo.save({ orderId: order.id, ...item });\n      await this.inventoryRepo.decrement({ productId: item.productId }, 'stock', item.quantity);\n    }\n\n    await this.paymentService.charge(order.id);\n    // If payment fails, order and inventory are already modified!\n\n    return order;\n  }\n}\n```\n\n**Correct (use DataSource.transaction for automatic rollback):**\n\n```typescript\n// Use DataSource.transaction() for automatic rollback\n@Injectable()\nexport class OrdersService {\n  constructor(private dataSource: DataSource) {}\n\n  async createOrder(userId: string, items: OrderItem[]): Promise<Order> {\n    return this.dataSource.transaction(async manager => {\n      // All operations use the same transactional manager\n      const order = await manager.save(Order, { userId, status: 'pending' });\n\n      for (const item of items) {\n        await manager.save(OrderItem, { orderId: order.id, ...item });\n        await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);\n      }\n\n      // If this throws, everything rolls back\n      await this.paymentService.chargeWithManager(manager, order.id);\n\n      return order;\n    });\n  }\n}\n\n// QueryRunner for manual transaction control\n@Injectable()\nexport class TransferService {\n  constructor(private dataSource: DataSource) {}\n\n  async transfer(fromId: string, toId: string, amount: number): Promise<void> {\n    const queryRunner = this.dataSource.createQueryRunner();\n    await queryRunner.connect();\n    await queryRunner.startTransaction();\n\n    try {\n      // Debit source account\n      await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);\n\n      // Verify sufficient funds\n      const source = await queryRunner.manager.findOne(Account, {\n        where: { id: fromId },\n      });\n      if (source.balance < 0) {\n        throw new BadRequestException('Insufficient funds');\n      }\n\n      // Credit destination account\n      await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);\n\n      // Log the transaction\n      await queryRunner.manager.save(TransactionLog, {\n        fromId,\n        toId,\n        amount,\n        timestamp: new Date(),\n      });\n\n      await queryRunner.commitTransaction();\n    } catch (error) {\n      await queryRunner.rollbackTransaction();\n      throw error;\n    } finally {\n      await queryRunner.release();\n    }\n  }\n}\n\n// Repository method with transaction support\n@Injectable()\nexport class UsersRepository {\n  constructor(\n    @InjectRepository(User) private repo: Repository<User>,\n    private dataSource: DataSource,\n  ) {}\n\n  async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {\n    return this.dataSource.transaction(async manager => {\n      const user = await manager.save(User, userData);\n      await manager.save(Profile, { ...profileData, userId: user.id });\n      return user;\n    });\n  }\n}\n```\n\nReference: [TypeORM Transactions](https://typeorm.io/transactions)\n\n---\n\n## 8. API Design\n\n**Section Impact: MEDIUM**\n\n### 8.1 Use DTOs and Serialization for API Responses\n\n**Impact: MEDIUM** — Response DTOs prevent accidental data exposure and ensure consistency\n\nNever return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract.\n\n**Incorrect (returning entities directly or manual spreading):**\n\n```typescript\n// Return entities directly\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findById(id);\n    // Returns: { id, email, passwordHash, ssn, internalNotes, ... }\n    // Exposes sensitive data!\n  }\n}\n\n// Manual object spreading (error-prone)\n@Get(':id')\nasync findOne(@Param('id') id: string) {\n  const user = await this.usersService.findById(id);\n  return {\n    id: user.id,\n    email: user.email,\n    name: user.name,\n    // Easy to forget to exclude sensitive fields\n    // Hard to maintain across endpoints\n  };\n}\n```\n\n**Correct (use class-transformer with @Exclude and response DTOs):**\n\n```typescript\n// Enable class-transformer globally\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));\n  await app.listen(3000);\n}\n\n// Entity with serialization control\n@Entity()\nexport class User {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Column()\n  email: string;\n\n  @Column()\n  name: string;\n\n  @Column()\n  @Exclude() // Never include in responses\n  passwordHash: string;\n\n  @Column({ nullable: true })\n  @Exclude()\n  ssn: string;\n\n  @Column({ default: false })\n  @Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests\n  isAdmin: boolean;\n\n  @CreateDateColumn()\n  createdAt: Date;\n\n  @Column()\n  @Exclude()\n  internalNotes: string;\n}\n\n// Now returning entity is safe\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findById(id);\n    // Returns: { id, email, name, createdAt }\n    // Sensitive fields excluded automatically\n  }\n}\n\n// For different response shapes, use explicit DTOs\nexport class UserResponseDto {\n  @Expose()\n  id: string;\n\n  @Expose()\n  email: string;\n\n  @Expose()\n  name: string;\n\n  @Expose()\n  @Transform(({ obj }) => obj.posts?.length || 0)\n  postCount: number;\n\n  constructor(partial: Partial<User>) {\n    Object.assign(this, partial);\n  }\n}\n\nexport class UserDetailResponseDto extends UserResponseDto {\n  @Expose()\n  createdAt: Date;\n\n  @Expose()\n  @Type(() => PostResponseDto)\n  posts: PostResponseDto[];\n}\n\n// Controller with explicit DTOs\n@Controller('users')\nexport class UsersController {\n  @Get()\n  @SerializeOptions({ type: UserResponseDto })\n  async findAll(): Promise<UserResponseDto[]> {\n    const users = await this.usersService.findAll();\n    return users.map(u => plainToInstance(UserResponseDto, u));\n  }\n\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<UserDetailResponseDto> {\n    const user = await this.usersService.findByIdWithPosts(id);\n    return plainToInstance(UserDetailResponseDto, user, {\n      excludeExtraneousValues: true,\n    });\n  }\n}\n\n// Groups for conditional serialization\nexport class UserDto {\n  @Expose()\n  id: string;\n\n  @Expose()\n  name: string;\n\n  @Expose({ groups: ['admin'] })\n  email: string;\n\n  @Expose({ groups: ['admin'] })\n  createdAt: Date;\n\n  @Expose({ groups: ['admin', 'owner'] })\n  settings: UserSettings;\n}\n\n@Controller('users')\nexport class UsersController {\n  @Get()\n  @SerializeOptions({ groups: ['public'] })\n  async findAllPublic(): Promise<UserDto[]> {\n    // Returns: { id, name }\n  }\n\n  @Get('admin')\n  @UseGuards(AdminGuard)\n  @SerializeOptions({ groups: ['admin'] })\n  async findAllAdmin(): Promise<UserDto[]> {\n    // Returns: { id, name, email, createdAt }\n  }\n\n  @Get('me')\n  @SerializeOptions({ groups: ['owner'] })\n  async getProfile(@CurrentUser() user: User): Promise<UserDto> {\n    // Returns: { id, name, settings }\n  }\n}\n```\n\nReference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization)\n\n---\n\n### 8.2 Use Interceptors for Cross-Cutting Concerns\n\n**Impact: MEDIUM-HIGH** — Interceptors provide clean separation for cross-cutting logic\n\nInterceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams.\n\n**Incorrect (logging and transformation in every method):**\n\n```typescript\n// Logging in every controller method\n@Controller('users')\nexport class UsersController {\n  @Get()\n  async findAll(): Promise<User[]> {\n    const start = Date.now();\n    this.logger.log('findAll called');\n\n    const users = await this.usersService.findAll();\n\n    this.logger.log(`findAll completed in ${Date.now() - start}ms`);\n    return users;\n  }\n\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    const start = Date.now();\n    this.logger.log(`findOne called with id: ${id}`);\n\n    const user = await this.usersService.findOne(id);\n\n    this.logger.log(`findOne completed in ${Date.now() - start}ms`);\n    return user;\n  }\n  // Repeated in every method!\n}\n\n// Manual response wrapping\n@Get()\nasync findAll(): Promise<{ data: User[]; meta: Meta }> {\n  const users = await this.usersService.findAll();\n  return {\n    data: users,\n    meta: { timestamp: new Date(), count: users.length },\n  };\n}\n```\n\n**Correct (use interceptors for cross-cutting concerns):**\n\n```typescript\n// Logging interceptor\n@Injectable()\nexport class LoggingInterceptor implements NestInterceptor {\n  private readonly logger = new Logger('HTTP');\n\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    const request = context.switchToHttp().getRequest();\n    const { method, url, body } = request;\n    const now = Date.now();\n\n    return next.handle().pipe(\n      tap({\n        next: (data) => {\n          const response = context.switchToHttp().getResponse();\n          this.logger.log(\n            `${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`,\n          );\n        },\n        error: (error) => {\n          this.logger.error(\n            `${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`,\n            error.stack,\n          );\n        },\n      }),\n    );\n  }\n}\n\n// Response transformation interceptor\n@Injectable()\nexport class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {\n    return next.handle().pipe(\n      map((data) => ({\n        data,\n        meta: {\n          timestamp: new Date().toISOString(),\n          path: context.switchToHttp().getRequest().url,\n        },\n      })),\n    );\n  }\n}\n\n// Timeout interceptor\n@Injectable()\nexport class TimeoutInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    return next.handle().pipe(\n      timeout(5000),\n      catchError((err) => {\n        if (err instanceof TimeoutError) {\n          throw new RequestTimeoutException('Request timed out');\n        }\n        throw err;\n      }),\n    );\n  }\n}\n\n// Apply globally or per-controller\n@Module({\n  providers: [\n    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },\n    { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },\n  ],\n})\nexport class AppModule {}\n\n// Or per-controller\n@Controller('users')\n@UseInterceptors(LoggingInterceptor)\nexport class UsersController {\n  @Get()\n  async findAll(): Promise<User[]> {\n    // Clean business logic only\n    return this.usersService.findAll();\n  }\n}\n\n// Custom cache interceptor with TTL\n@Injectable()\nexport class HttpCacheInterceptor implements NestInterceptor {\n  constructor(\n    private cacheManager: Cache,\n    private reflector: Reflector,\n  ) {}\n\n  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {\n    const request = context.switchToHttp().getRequest();\n\n    // Only cache GET requests\n    if (request.method !== 'GET') {\n      return next.handle();\n    }\n\n    const cacheKey = this.generateKey(request);\n    const ttl = this.reflector.get<number>('cacheTTL', context.getHandler()) || 300;\n\n    const cached = await this.cacheManager.get(cacheKey);\n    if (cached) {\n      return of(cached);\n    }\n\n    return next.handle().pipe(\n      tap((response) => {\n        this.cacheManager.set(cacheKey, response, ttl);\n      }),\n    );\n  }\n\n  private generateKey(request: Request): string {\n    return `cache:${request.url}:${JSON.stringify(request.query)}`;\n  }\n}\n\n// Usage with custom TTL\n@Get()\n@SetMetadata('cacheTTL', 600)\n@UseInterceptors(HttpCacheInterceptor)\nasync findAll(): Promise<User[]> {\n  return this.usersService.findAll();\n}\n\n// Error mapping interceptor\n@Injectable()\nexport class ErrorMappingInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    return next.handle().pipe(\n      catchError((error) => {\n        if (error instanceof EntityNotFoundError) {\n          throw new NotFoundException(error.message);\n        }\n        if (error instanceof QueryFailedError) {\n          if (error.message.includes('duplicate')) {\n            throw new ConflictException('Resource already exists');\n          }\n        }\n        throw error;\n      }),\n    );\n  }\n}\n```\n\nReference: [NestJS Interceptors](https://docs.nestjs.com/interceptors)\n\n---\n\n### 8.3 Use Pipes for Input Transformation\n\n**Impact: MEDIUM** — Pipes ensure clean, validated data reaches your handlers\n\nUse built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers.\n\n**Incorrect (manual type parsing in handlers):**\n\n```typescript\n// Manual type parsing in handlers\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    // Manual validation in every handler\n    const uuid = id.trim();\n    if (!isUUID(uuid)) {\n      throw new BadRequestException('Invalid UUID');\n    }\n    return this.usersService.findOne(uuid);\n  }\n\n  @Get()\n  async findAll(\n    @Query('page') page: string,\n    @Query('limit') limit: string,\n  ): Promise<User[]> {\n    // Manual parsing and defaults\n    const pageNum = parseInt(page) || 1;\n    const limitNum = parseInt(limit) || 10;\n    return this.usersService.findAll(pageNum, limitNum);\n  }\n}\n\n// Type coercion without validation\n@Get()\nasync search(@Query('price') price: string): Promise<Product[]> {\n  const priceNum = +price; // NaN if invalid, no error\n  return this.productsService.findByPrice(priceNum);\n}\n```\n\n**Correct (use built-in and custom pipes):**\n\n```typescript\n// Use built-in pipes for common transformations\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {\n    // id is guaranteed to be a valid UUID\n    return this.usersService.findOne(id);\n  }\n\n  @Get()\n  async findAll(\n    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,\n    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,\n  ): Promise<User[]> {\n    // Automatic defaults and type conversion\n    return this.usersService.findAll(page, limit);\n  }\n\n  @Get('by-status/:status')\n  async findByStatus(\n    @Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus,\n  ): Promise<User[]> {\n    return this.usersService.findByStatus(status);\n  }\n}\n\n// Custom pipe for business logic\n@Injectable()\nexport class ParseDatePipe implements PipeTransform<string, Date> {\n  transform(value: string): Date {\n    const date = new Date(value);\n    if (isNaN(date.getTime())) {\n      throw new BadRequestException('Invalid date format');\n    }\n    return date;\n  }\n}\n\n@Get('reports')\nasync getReports(\n  @Query('from', ParseDatePipe) from: Date,\n  @Query('to', ParseDatePipe) to: Date,\n): Promise<Report[]> {\n  return this.reportsService.findBetween(from, to);\n}\n\n// Custom transformation pipes\n@Injectable()\nexport class NormalizeEmailPipe implements PipeTransform<string, string> {\n  transform(value: string): string {\n    if (!value) return value;\n    return value.trim().toLowerCase();\n  }\n}\n\n// Parse comma-separated values\n@Injectable()\nexport class ParseArrayPipe implements PipeTransform<string, string[]> {\n  transform(value: string): string[] {\n    if (!value) return [];\n    return value.split(',').map((v) => v.trim()).filter(Boolean);\n  }\n}\n\n@Get('products')\nasync findProducts(\n  @Query('ids', ParseArrayPipe) ids: string[],\n  @Query('email', NormalizeEmailPipe) email: string,\n): Promise<Product[]> {\n  // ids is already an array, email is normalized\n  return this.productsService.findByIds(ids);\n}\n\n// Sanitize HTML input\n@Injectable()\nexport class SanitizeHtmlPipe implements PipeTransform<string, string> {\n  transform(value: string): string {\n    if (!value) return value;\n    return sanitizeHtml(value, { allowedTags: [] });\n  }\n}\n\n// Global validation pipe with transformation\napp.useGlobalPipes(\n  new ValidationPipe({\n    whitelist: true, // Strip non-DTO properties\n    transform: true, // Auto-transform to DTO types\n    transformOptions: {\n      enableImplicitConversion: true, // Convert query strings to numbers\n    },\n    forbidNonWhitelisted: true, // Throw on extra properties\n  }),\n);\n\n// DTO with transformation decorators\nexport class FindProductsDto {\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  page?: number = 1;\n\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  @Max(100)\n  limit?: number = 10;\n\n  @IsOptional()\n  @Transform(({ value }) => value?.toLowerCase())\n  @IsString()\n  search?: string;\n\n  @IsOptional()\n  @Transform(({ value }) => value?.split(','))\n  @IsArray()\n  @IsString({ each: true })\n  categories?: string[];\n}\n\n@Get()\nasync findAll(@Query() dto: FindProductsDto): Promise<Product[]> {\n  // dto is already transformed and validated\n  return this.productsService.findAll(dto);\n}\n\n// Pipe error customization\n@Injectable()\nexport class CustomParseIntPipe extends ParseIntPipe {\n  constructor() {\n    super({\n      exceptionFactory: (error) =>\n        new BadRequestException(`${error} must be a valid integer`),\n    });\n  }\n}\n\n// Or use options on built-in pipes\n@Get(':id')\nasync findOne(\n  @Param(\n    'id',\n    new ParseIntPipe({\n      errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,\n      exceptionFactory: () => new NotAcceptableException('ID must be numeric'),\n    }),\n  )\n  id: number,\n): Promise<Item> {\n  return this.itemsService.findOne(id);\n}\n```\n\nReference: [NestJS Pipes](https://docs.nestjs.com/pipes)\n\n---\n\n### 8.4 Use API Versioning for Breaking Changes\n\n**Impact: MEDIUM** — Versioning allows you to evolve APIs without breaking existing clients\n\nUse NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints.\n\n**Incorrect (breaking changes without versioning):**\n\n```typescript\n// Breaking changes without versioning\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    // Original response: { id, name, email }\n    // Later changed to: { id, firstName, lastName, emailAddress }\n    // Old clients break!\n    return this.usersService.findOne(id);\n  }\n}\n\n// Manual versioning in routes\n@Controller('v1/users')\nexport class UsersV1Controller {}\n\n@Controller('v2/users')\nexport class UsersV2Controller {}\n// Inconsistent, error-prone, hard to maintain\n```\n\n**Correct (use NestJS built-in versioning):**\n\n```typescript\n// Enable versioning in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  // URI versioning: /v1/users, /v2/users\n  app.enableVersioning({\n    type: VersioningType.URI,\n    defaultVersion: '1',\n  });\n\n  // Or header versioning: X-API-Version: 1\n  app.enableVersioning({\n    type: VersioningType.HEADER,\n    header: 'X-API-Version',\n    defaultVersion: '1',\n  });\n\n  // Or media type: Accept: application/json;v=1\n  app.enableVersioning({\n    type: VersioningType.MEDIA_TYPE,\n    key: 'v=',\n    defaultVersion: '1',\n  });\n\n  await app.listen(3000);\n}\n\n// Version-specific controllers\n@Controller('users')\n@Version('1')\nexport class UsersV1Controller {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<UserV1Response> {\n    const user = await this.usersService.findOne(id);\n    // V1 response format\n    return {\n      id: user.id,\n      name: user.name,\n      email: user.email,\n    };\n  }\n}\n\n@Controller('users')\n@Version('2')\nexport class UsersV2Controller {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<UserV2Response> {\n    const user = await this.usersService.findOne(id);\n    // V2 response format with breaking changes\n    return {\n      id: user.id,\n      firstName: user.firstName,\n      lastName: user.lastName,\n      emailAddress: user.email,\n      createdAt: user.createdAt,\n    };\n  }\n}\n\n// Per-route versioning - different versions for different routes\n@Controller('users')\nexport class UsersController {\n  @Get()\n  @Version('1')\n  findAllV1(): Promise<UserV1Response[]> {\n    return this.usersService.findAllV1();\n  }\n\n  @Get()\n  @Version('2')\n  findAllV2(): Promise<UserV2Response[]> {\n    return this.usersService.findAllV2();\n  }\n\n  @Get(':id')\n  @Version(['1', '2']) // Same handler for multiple versions\n  findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findOne(id);\n  }\n\n  @Post()\n  @Version(VERSION_NEUTRAL) // Available in all versions\n  create(@Body() dto: CreateUserDto): Promise<User> {\n    return this.usersService.create(dto);\n  }\n}\n\n// Shared service with version-specific logic\n@Injectable()\nexport class UsersService {\n  async findOne(id: string, version: string): Promise<any> {\n    const user = await this.repo.findOne({ where: { id } });\n\n    if (version === '1') {\n      return this.toV1Response(user);\n    }\n    return this.toV2Response(user);\n  }\n\n  private toV1Response(user: User): UserV1Response {\n    return {\n      id: user.id,\n      name: `${user.firstName} ${user.lastName}`,\n      email: user.email,\n    };\n  }\n\n  private toV2Response(user: User): UserV2Response {\n    return {\n      id: user.id,\n      firstName: user.firstName,\n      lastName: user.lastName,\n      emailAddress: user.email,\n      createdAt: user.createdAt,\n    };\n  }\n}\n\n// Controller extracts version\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {\n    return this.usersService.findOne(id, version);\n  }\n}\n\n// Deprecation strategy - mark old versions as deprecated\n@Controller('users')\n@Version('1')\n@UseInterceptors(DeprecationInterceptor)\nexport class UsersV1Controller {\n  // All V1 routes will include deprecation warning\n}\n\n@Injectable()\nexport class DeprecationInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    const response = context.switchToHttp().getResponse();\n    response.setHeader('Deprecation', 'true');\n    response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT');\n    response.setHeader('Link', '</v2/users>; rel=\"successor-version\"');\n\n    return next.handle();\n  }\n}\n```\n\nReference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning)\n\n---\n\n## 9. Microservices\n\n**Section Impact: MEDIUM**\n\n### 9.1 Implement Health Checks for Microservices\n\n**Impact: MEDIUM-HIGH** — Health checks enable orchestrators to manage service lifecycle\n\nImplement liveness and readiness probes using `@nestjs/terminus`. Liveness checks determine if the service should be restarted. Readiness checks determine if the service can accept traffic. Proper health checks enable Kubernetes and load balancers to route traffic correctly.\n\n**Incorrect (simple ping that doesn't check dependencies):**\n\n```typescript\n// Simple ping that doesn't check dependencies\n@Controller('health')\nexport class HealthController {\n  @Get()\n  check(): string {\n    return 'OK'; // Service might be unhealthy but returns OK\n  }\n}\n\n// Health check that blocks on slow dependencies\n@Controller('health')\nexport class HealthController {\n  @Get()\n  async check(): Promise<string> {\n    // If database is slow, health check times out\n    await this.userRepo.findOne({ where: { id: '1' } });\n    await this.redis.ping();\n    await this.externalApi.healthCheck();\n    return 'OK';\n  }\n}\n```\n\n**Correct (use @nestjs/terminus for comprehensive health checks):**\n\n```typescript\n// Use @nestjs/terminus for comprehensive health checks\nimport {\n  HealthCheckService,\n  HttpHealthIndicator,\n  TypeOrmHealthIndicator,\n  HealthCheck,\n  DiskHealthIndicator,\n  MemoryHealthIndicator,\n} from '@nestjs/terminus';\n\n@Controller('health')\nexport class HealthController {\n  constructor(\n    private health: HealthCheckService,\n    private http: HttpHealthIndicator,\n    private db: TypeOrmHealthIndicator,\n    private disk: DiskHealthIndicator,\n    private memory: MemoryHealthIndicator,\n  ) {}\n\n  // Liveness probe - is the service alive?\n  @Get('live')\n  @HealthCheck()\n  liveness() {\n    return this.health.check([\n      // Basic checks only\n      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB\n    ]);\n  }\n\n  // Readiness probe - can the service handle traffic?\n  @Get('ready')\n  @HealthCheck()\n  readiness() {\n    return this.health.check([\n      () => this.db.pingCheck('database'),\n      () =>\n        this.http.pingCheck('redis', 'http://redis:6379', { timeout: 1000 }),\n      () =>\n        this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),\n    ]);\n  }\n\n  // Deep health check for debugging\n  @Get('deep')\n  @HealthCheck()\n  deepCheck() {\n    return this.health.check([\n      () => this.db.pingCheck('database'),\n      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),\n      () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),\n      () =>\n        this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),\n      () =>\n        this.http.pingCheck('external-api', 'https://api.example.com/health'),\n    ]);\n  }\n}\n\n// Custom indicator for business-specific health\n@Injectable()\nexport class QueueHealthIndicator extends HealthIndicator {\n  constructor(private queueService: QueueService) {\n    super();\n  }\n\n  async isHealthy(key: string): Promise<HealthIndicatorResult> {\n    const queueStats = await this.queueService.getStats();\n\n    const isHealthy = queueStats.failedCount < 100;\n    const result = this.getStatus(key, isHealthy, {\n      waiting: queueStats.waitingCount,\n      active: queueStats.activeCount,\n      failed: queueStats.failedCount,\n    });\n\n    if (!isHealthy) {\n      throw new HealthCheckError('Queue unhealthy', result);\n    }\n\n    return result;\n  }\n}\n\n// Redis health indicator\n@Injectable()\nexport class RedisHealthIndicator extends HealthIndicator {\n  constructor(@InjectRedis() private redis: Redis) {\n    super();\n  }\n\n  async isHealthy(key: string): Promise<HealthIndicatorResult> {\n    try {\n      const pong = await this.redis.ping();\n      return this.getStatus(key, pong === 'PONG');\n    } catch (error) {\n      throw new HealthCheckError('Redis check failed', this.getStatus(key, false));\n    }\n  }\n}\n\n// Use custom indicators\n@Get('ready')\n@HealthCheck()\nreadiness() {\n  return this.health.check([\n    () => this.db.pingCheck('database'),\n    () => this.redis.isHealthy('redis'),\n    () => this.queue.isHealthy('job-queue'),\n  ]);\n}\n\n// Graceful shutdown handling\n@Injectable()\nexport class GracefulShutdownService implements OnApplicationShutdown {\n  private isShuttingDown = false;\n\n  isShutdown(): boolean {\n    return this.isShuttingDown;\n  }\n\n  async onApplicationShutdown(signal: string): Promise<void> {\n    this.isShuttingDown = true;\n    console.log(`Shutting down on ${signal}`);\n\n    // Wait for in-flight requests\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n  }\n}\n\n// Health check respects shutdown state\n@Get('ready')\n@HealthCheck()\nreadiness() {\n  if (this.shutdownService.isShutdown()) {\n    throw new ServiceUnavailableException('Shutting down');\n  }\n\n  return this.health.check([\n    () => this.db.pingCheck('database'),\n  ]);\n}\n```\n\n### Kubernetes Configuration\n\n```yaml\n# Kubernetes deployment with probes\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: api-service\nspec:\n  template:\n    spec:\n      containers:\n        - name: api\n          image: api-service:latest\n          ports:\n            - containerPort: 3000\n          livenessProbe:\n            httpGet:\n              path: /health/live\n              port: 3000\n            initialDelaySeconds: 30\n            periodSeconds: 10\n            timeoutSeconds: 5\n            failureThreshold: 3\n          readinessProbe:\n            httpGet:\n              path: /health/ready\n              port: 3000\n            initialDelaySeconds: 5\n            periodSeconds: 5\n            timeoutSeconds: 3\n            failureThreshold: 3\n          startupProbe:\n            httpGet:\n              path: /health/live\n              port: 3000\n            initialDelaySeconds: 0\n            periodSeconds: 5\n            failureThreshold: 30\n```\n\nReference: [NestJS Terminus](https://docs.nestjs.com/recipes/terminus)\n\n---\n\n### 9.2 Use Message and Event Patterns Correctly\n\n**Impact: MEDIUM** — Proper patterns ensure reliable microservice communication\n\nNestJS microservices support two communication patterns: request-response (MessagePattern) and event-based (EventPattern). Use MessagePattern when you need a response, and EventPattern for fire-and-forget notifications. Understanding the difference prevents communication bugs.\n\n**Incorrect (using wrong pattern for use case):**\n\n```typescript\n// Use @MessagePattern for fire-and-forget\n@Controller()\nexport class NotificationsController {\n  @MessagePattern('user.created')\n  async handleUserCreated(data: UserCreatedEvent) {\n    // This WAITS for response, blocking the sender\n    await this.emailService.sendWelcome(data.email);\n    // If email fails, sender gets an error (coupling!)\n  }\n}\n\n// Use @EventPattern expecting a response\n@Controller()\nexport class OrdersController {\n  @EventPattern('inventory.check')\n  async checkInventory(data: CheckInventoryDto) {\n    const available = await this.inventory.check(data);\n    return available; // This return value is IGNORED with @EventPattern!\n  }\n}\n\n// Tight coupling in client\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Blocks until notification service responds\n    await this.client.send('user.created', user).toPromise();\n    // If notification service is down, user creation fails!\n\n    return user;\n  }\n}\n```\n\n**Correct (use MessagePattern for request-response, EventPattern for fire-and-forget):**\n\n```typescript\n// MessagePattern: Request-Response (when you NEED a response)\n@Controller()\nexport class InventoryController {\n  @MessagePattern({ cmd: 'check_inventory' })\n  async checkInventory(data: CheckInventoryDto): Promise<InventoryResult> {\n    const result = await this.inventoryService.check(data.productId, data.quantity);\n    return result; // Response sent back to caller\n  }\n}\n\n// Client expects response\n@Injectable()\nexport class OrdersService {\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    // Check inventory - we NEED this response to proceed\n    const inventory = await firstValueFrom(\n      this.inventoryClient.send<InventoryResult>(\n        { cmd: 'check_inventory' },\n        { productId: dto.productId, quantity: dto.quantity },\n      ),\n    );\n\n    if (!inventory.available) {\n      throw new BadRequestException('Insufficient inventory');\n    }\n\n    return this.repo.save(dto);\n  }\n}\n\n// EventPattern: Fire-and-Forget (for notifications, side effects)\n@Controller()\nexport class NotificationsController {\n  @EventPattern('user.created')\n  async handleUserCreated(data: UserCreatedEvent): Promise<void> {\n    // No return value needed - just process the event\n    await this.emailService.sendWelcome(data.email);\n    await this.analyticsService.track('user_signup', data);\n    // If this fails, it doesn't affect the sender\n  }\n}\n\n// Client emits event without waiting\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Fire and forget - doesn't block, doesn't wait\n    this.eventClient.emit('user.created', {\n      userId: user.id,\n      email: user.email,\n      timestamp: new Date(),\n    });\n\n    return user; // User creation succeeds regardless of event handling\n  }\n}\n\n// Hybrid pattern for critical events\n@Injectable()\nexport class OrdersService {\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const order = await this.repo.save(dto);\n\n    // Critical: inventory reservation (use MessagePattern)\n    const reserved = await firstValueFrom(\n      this.inventoryClient.send({ cmd: 'reserve_inventory' }, {\n        orderId: order.id,\n        items: dto.items,\n      }),\n    );\n\n    if (!reserved.success) {\n      await this.repo.delete(order.id);\n      throw new BadRequestException('Could not reserve inventory');\n    }\n\n    // Non-critical: notifications (use EventPattern)\n    this.eventClient.emit('order.created', {\n      orderId: order.id,\n      userId: dto.userId,\n      total: dto.total,\n    });\n\n    return order;\n  }\n}\n\n// Error handling patterns\n// MessagePattern errors propagate to caller\n@MessagePattern({ cmd: 'get_user' })\nasync getUser(userId: string): Promise<User> {\n  const user = await this.repo.findOne({ where: { id: userId } });\n  if (!user) {\n    throw new RpcException('User not found'); // Received by caller\n  }\n  return user;\n}\n\n// EventPattern errors should be handled locally\n@EventPattern('order.created')\nasync handleOrderCreated(data: OrderCreatedEvent): Promise<void> {\n  try {\n    await this.processOrder(data);\n  } catch (error) {\n    // Log and potentially retry - don't throw\n    this.logger.error('Failed to process order event', error);\n    await this.deadLetterQueue.add(data);\n  }\n}\n```\n\nReference: [NestJS Microservices](https://docs.nestjs.com/microservices/basics)\n\n---\n\n### 9.3 Use Message Queues for Background Jobs\n\n**Impact: MEDIUM-HIGH** — Queues enable reliable background processing\n\nUse `@nestjs/bullmq` for background job processing. Queues decouple long-running tasks from HTTP requests, enable retry logic, and distribute workload across workers. Use them for emails, file processing, notifications, and any task that shouldn't block user requests.\n\n**Incorrect (long-running tasks in HTTP handlers):**\n\n```typescript\n// Long-running tasks in HTTP handlers\n@Controller('reports')\nexport class ReportsController {\n  @Post()\n  async generate(@Body() dto: GenerateReportDto): Promise<Report> {\n    // This blocks the request for potentially minutes\n    const data = await this.fetchLargeDataset(dto);\n    const report = await this.processData(data); // Slow!\n    await this.sendEmail(dto.email, report); // Can fail!\n    return report; // Client times out\n  }\n}\n\n// Fire-and-forget without retry\n@Injectable()\nexport class EmailService {\n  async sendWelcome(email: string): Promise<void> {\n    // If this fails, email is never sent\n    await this.mailer.send({ to: email, template: 'welcome' });\n    // No retry, no tracking, no visibility\n  }\n}\n\n// Use setInterval for scheduled tasks\nsetInterval(async () => {\n  await cleanupOldRecords();\n}, 60000); // No error handling, memory leaks\n```\n\n**Correct (use BullMQ for background processing):**\n\n```typescript\n// Configure BullMQ\nimport { BullModule } from '@nestjs/bullmq';\n\n@Module({\n  imports: [\n    BullModule.forRoot({\n      connection: {\n        host: 'localhost',\n        port: 6379,\n      },\n      defaultJobOptions: {\n        removeOnComplete: 1000,\n        removeOnFail: 5000,\n        attempts: 3,\n        backoff: {\n          type: 'exponential',\n          delay: 1000,\n        },\n      },\n    }),\n    BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),\n  ],\n})\nexport class QueueModule {}\n\n// Producer: Add jobs to queue\n@Injectable()\nexport class ReportsService {\n  constructor(@InjectQueue('reports') private reportsQueue: Queue) {}\n\n  async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {\n    // Return immediately, process in background\n    const job = await this.reportsQueue.add('generate', dto, {\n      priority: dto.urgent ? 1 : 10,\n      delay: dto.scheduledFor ? Date.parse(dto.scheduledFor) - Date.now() : 0,\n    });\n\n    return { jobId: job.id };\n  }\n\n  async getJobStatus(jobId: string): Promise<JobStatus> {\n    const job = await this.reportsQueue.getJob(jobId);\n    return {\n      status: await job.getState(),\n      progress: job.progress,\n      result: job.returnvalue,\n    };\n  }\n}\n\n// Consumer: Process jobs\n@Processor('reports')\nexport class ReportsProcessor {\n  private readonly logger = new Logger(ReportsProcessor.name);\n\n  @Process('generate')\n  async generateReport(job: Job<GenerateReportDto>): Promise<Report> {\n    this.logger.log(`Processing report job ${job.id}`);\n\n    // Update progress\n    await job.updateProgress(10);\n\n    const data = await this.fetchData(job.data);\n    await job.updateProgress(50);\n\n    const report = await this.processData(data);\n    await job.updateProgress(90);\n\n    await this.saveReport(report);\n    await job.updateProgress(100);\n\n    return report;\n  }\n\n  @OnQueueActive()\n  onActive(job: Job) {\n    this.logger.log(`Processing job ${job.id}`);\n  }\n\n  @OnQueueCompleted()\n  onCompleted(job: Job, result: any) {\n    this.logger.log(`Job ${job.id} completed`);\n  }\n\n  @OnQueueFailed()\n  onFailed(job: Job, error: Error) {\n    this.logger.error(`Job ${job.id} failed: ${error.message}`);\n  }\n}\n\n// Email queue with retry\n@Processor('email')\nexport class EmailProcessor {\n  @Process('send')\n  async sendEmail(job: Job<SendEmailDto>): Promise<void> {\n    const { to, template, data } = job.data;\n\n    try {\n      await this.mailer.send({\n        to,\n        template,\n        context: data,\n      });\n    } catch (error) {\n      // BullMQ will retry based on job options\n      throw error;\n    }\n  }\n}\n\n// Usage\n@Injectable()\nexport class NotificationService {\n  constructor(@InjectQueue('email') private emailQueue: Queue) {}\n\n  async sendWelcome(user: User): Promise<void> {\n    await this.emailQueue.add(\n      'send',\n      {\n        to: user.email,\n        template: 'welcome',\n        data: { name: user.name },\n      },\n      {\n        attempts: 5,\n        backoff: { type: 'exponential', delay: 5000 },\n      },\n    );\n  }\n}\n\n// Scheduled jobs\n@Injectable()\nexport class ScheduledJobsService implements OnModuleInit {\n  constructor(@InjectQueue('maintenance') private queue: Queue) {}\n\n  async onModuleInit(): Promise<void> {\n    // Clean up old reports daily at midnight\n    await this.queue.add(\n      'cleanup',\n      {},\n      {\n        repeat: { cron: '0 0 * * *' },\n        jobId: 'daily-cleanup', // Prevent duplicates\n      },\n    );\n\n    // Send digest every hour\n    await this.queue.add(\n      'digest',\n      {},\n      {\n        repeat: { every: 60 * 60 * 1000 },\n        jobId: 'hourly-digest',\n      },\n    );\n  }\n}\n\n@Processor('maintenance')\nexport class MaintenanceProcessor {\n  @Process('cleanup')\n  async cleanup(): Promise<void> {\n    await this.cleanupOldReports();\n    await this.cleanupExpiredSessions();\n  }\n\n  @Process('digest')\n  async sendDigest(): Promise<void> {\n    const users = await this.getUsersForDigest();\n    for (const user of users) {\n      await this.emailQueue.add('send', { to: user.email, template: 'digest' });\n    }\n  }\n}\n\n// Queue monitoring with Bull Board\nimport { BullBoardModule } from '@bull-board/nestjs';\nimport { BullMQAdapter } from '@bull-board/api/bullMQAdapter';\n\n@Module({\n  imports: [\n    BullBoardModule.forRoot({\n      route: '/admin/queues',\n      adapter: ExpressAdapter,\n    }),\n    BullBoardModule.forFeature({\n      name: 'email',\n      adapter: BullMQAdapter,\n    }),\n    BullBoardModule.forFeature({\n      name: 'reports',\n      adapter: BullMQAdapter,\n    }),\n  ],\n})\nexport class AdminModule {}\n```\n\nReference: [NestJS Queues](https://docs.nestjs.com/techniques/queues)\n\n---\n\n## 10. DevOps & Deployment\n\n**Section Impact: LOW-MEDIUM**\n\n### 10.1 Implement Graceful Shutdown\n\n**Impact: MEDIUM-HIGH** — Proper shutdown handling ensures zero-downtime deployments\n\nHandle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments.\n\n**Incorrect (ignoring shutdown signals):**\n\n```typescript\n// Ignore shutdown signals\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  await app.listen(3000);\n  // App crashes immediately on SIGTERM\n  // In-flight requests fail\n  // Database connections are abruptly closed\n}\n\n// Long-running tasks without cancellation\n@Injectable()\nexport class ProcessingService {\n  async processLargeFile(file: File): Promise<void> {\n    // No way to interrupt this during shutdown\n    for (let i = 0; i < file.chunks.length; i++) {\n      await this.processChunk(file.chunks[i]);\n      // May run for minutes, blocking shutdown\n    }\n  }\n}\n```\n\n**Correct (enable shutdown hooks and handle cleanup):**\n\n```typescript\n// Enable shutdown hooks in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  // Enable shutdown hooks\n  app.enableShutdownHooks();\n\n  // Optional: Add timeout for forced shutdown\n  const server = await app.listen(3000);\n  server.setTimeout(30000); // 30 second timeout\n\n  // Handle graceful shutdown\n  const signals = ['SIGTERM', 'SIGINT'];\n  signals.forEach(signal => {\n    process.on(signal, async () => {\n      console.log(`Received ${signal}, starting graceful shutdown...`);\n\n      // Stop accepting new connections\n      server.close(async () => {\n        console.log('HTTP server closed');\n        await app.close();\n        process.exit(0);\n      });\n\n      // Force exit after timeout\n      setTimeout(() => {\n        console.error('Forced shutdown after timeout');\n        process.exit(1);\n      }, 30000);\n    });\n  });\n}\n\n// Lifecycle hooks for cleanup\n@Injectable()\nexport class DatabaseService implements OnApplicationShutdown {\n  private readonly connections: Connection[] = [];\n\n  async onApplicationShutdown(signal?: string): Promise<void> {\n    console.log(`Database service shutting down on ${signal}`);\n\n    // Close all connections gracefully\n    await Promise.all(this.connections.map(conn => conn.close()));\n\n    console.log('All database connections closed');\n  }\n}\n\n// Queue processor with graceful shutdown\n@Injectable()\nexport class QueueService implements OnApplicationShutdown, OnModuleDestroy {\n  private isShuttingDown = false;\n\n  onModuleDestroy(): void {\n    this.isShuttingDown = true;\n  }\n\n  async onApplicationShutdown(): Promise<void> {\n    // Wait for current jobs to complete\n    await this.queue.close();\n  }\n\n  async processJob(job: Job): Promise<void> {\n    if (this.isShuttingDown) {\n      throw new Error('Service is shutting down');\n    }\n    await this.doWork(job);\n  }\n}\n\n// WebSocket gateway cleanup\n@WebSocketGateway()\nexport class EventsGateway implements OnApplicationShutdown {\n  @WebSocketServer()\n  server: Server;\n\n  async onApplicationShutdown(): Promise<void> {\n    // Notify all connected clients\n    this.server.emit('shutdown', { message: 'Server is shutting down' });\n\n    // Close all connections\n    this.server.disconnectSockets();\n  }\n}\n\n// Health check integration\n@Injectable()\nexport class ShutdownService {\n  private isShuttingDown = false;\n\n  startShutdown(): void {\n    this.isShuttingDown = true;\n  }\n\n  isShutdown(): boolean {\n    return this.isShuttingDown;\n  }\n}\n\n@Controller('health')\nexport class HealthController {\n  constructor(private shutdownService: ShutdownService) {}\n\n  @Get('ready')\n  @HealthCheck()\n  readiness(): Promise<HealthCheckResult> {\n    // Return 503 during shutdown - k8s stops sending traffic\n    if (this.shutdownService.isShutdown()) {\n      throw new ServiceUnavailableException('Shutting down');\n    }\n\n    return this.health.check([() => this.db.pingCheck('database')]);\n  }\n}\n\n// Integrate with shutdown\n@Injectable()\nexport class AppShutdownService implements OnApplicationShutdown {\n  constructor(private shutdownService: ShutdownService) {}\n\n  async onApplicationShutdown(): Promise<void> {\n    // Mark as unhealthy first\n    this.shutdownService.startShutdown();\n\n    // Wait for k8s to update endpoints\n    await this.sleep(5000);\n\n    // Then proceed with cleanup\n  }\n}\n\n// Request tracking for in-flight requests\n@Injectable()\nexport class RequestTracker implements NestMiddleware, OnApplicationShutdown {\n  private activeRequests = 0;\n  private isShuttingDown = false;\n  private shutdownPromise: Promise<void> | null = null;\n  private resolveShutdown: (() => void) | null = null;\n\n  use(req: Request, res: Response, next: NextFunction): void {\n    if (this.isShuttingDown) {\n      res.status(503).send('Service Unavailable');\n      return;\n    }\n\n    this.activeRequests++;\n\n    res.on('finish', () => {\n      this.activeRequests--;\n      if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) {\n        this.resolveShutdown();\n      }\n    });\n\n    next();\n  }\n\n  async onApplicationShutdown(): Promise<void> {\n    this.isShuttingDown = true;\n\n    if (this.activeRequests > 0) {\n      console.log(`Waiting for ${this.activeRequests} requests to complete`);\n      this.shutdownPromise = new Promise(resolve => {\n        this.resolveShutdown = resolve;\n      });\n\n      // Wait with timeout\n      await Promise.race([this.shutdownPromise, new Promise(resolve => setTimeout(resolve, 30000))]);\n    }\n\n    console.log('All requests completed');\n  }\n}\n```\n\nReference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)\n\n---\n\n### 10.2 Use ConfigModule for Environment Configuration\n\n**Impact: LOW-MEDIUM** — Proper configuration prevents deployment failures\n\nUse `@nestjs/config` for environment-based configuration. Validate configuration at startup to fail fast on misconfigurations. Use namespaced configuration for organization and type safety.\n\n**Incorrect (accessing process.env directly):**\n\n```typescript\n// Access process.env directly\n@Injectable()\nexport class DatabaseService {\n  constructor() {\n    // No validation, can fail at runtime\n    this.connection = new Pool({\n      host: process.env.DB_HOST,\n      port: parseInt(process.env.DB_PORT), // NaN if missing\n      password: process.env.DB_PASSWORD, // undefined if missing\n    });\n  }\n}\n\n// Scattered env access\n@Injectable()\nexport class EmailService {\n  sendEmail() {\n    // Different services access env differently\n    const apiKey = process.env.SENDGRID_API_KEY || 'default';\n    // Typos go unnoticed: process.env.SENDGRID_API_KY\n  }\n}\n```\n\n**Correct (use @nestjs/config with validation):**\n\n```typescript\n// Setup validated configuration\nimport { ConfigModule, ConfigService, registerAs } from '@nestjs/config';\nimport * as Joi from 'joi';\n\n// config/database.config.ts\nexport const databaseConfig = registerAs('database', () => ({\n  host: process.env.DB_HOST,\n  port: parseInt(process.env.DB_PORT, 10),\n  username: process.env.DB_USERNAME,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n}));\n\n// config/app.config.ts\nexport const appConfig = registerAs('app', () => ({\n  port: parseInt(process.env.PORT, 10) || 3000,\n  environment: process.env.NODE_ENV || 'development',\n  apiPrefix: process.env.API_PREFIX || 'api',\n}));\n\n// config/validation.schema.ts\nexport const validationSchema = Joi.object({\n  NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),\n  PORT: Joi.number().default(3000),\n  DB_HOST: Joi.string().required(),\n  DB_PORT: Joi.number().default(5432),\n  DB_USERNAME: Joi.string().required(),\n  DB_PASSWORD: Joi.string().required(),\n  DB_NAME: Joi.string().required(),\n  JWT_SECRET: Joi.string().min(32).required(),\n  REDIS_URL: Joi.string().uri().required(),\n});\n\n// app.module.ts\n@Module({\n  imports: [\n    ConfigModule.forRoot({\n      isGlobal: true, // Available everywhere without importing\n      load: [databaseConfig, appConfig],\n      validationSchema,\n      validationOptions: {\n        abortEarly: true, // Stop on first error\n        allowUnknown: true, // Allow other env vars\n      },\n    }),\n    TypeOrmModule.forRootAsync({\n      inject: [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        type: 'postgres',\n        host: config.get('database.host'),\n        port: config.get('database.port'),\n        username: config.get('database.username'),\n        password: config.get('database.password'),\n        database: config.get('database.database'),\n        autoLoadEntities: true,\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Type-safe configuration access\nexport interface AppConfig {\n  port: number;\n  environment: 'development' | 'production' | 'test';\n  apiPrefix: string;\n}\n\nexport interface DatabaseConfig {\n  host: string;\n  port: number;\n  username: string;\n  password: string;\n  database: string;\n}\n\n// Type-safe access\n@Injectable()\nexport class AppService {\n  constructor(private config: ConfigService) {}\n\n  getPort(): number {\n    // Type-safe with generic\n    return this.config.get<number>('app.port');\n  }\n\n  getDatabaseConfig(): DatabaseConfig {\n    return this.config.get<DatabaseConfig>('database');\n  }\n}\n\n// Inject namespaced config directly\n@Injectable()\nexport class DatabaseService {\n  constructor(\n    @Inject(databaseConfig.KEY)\n    private dbConfig: ConfigType<typeof databaseConfig>,\n  ) {\n    // Full type inference!\n    const host = this.dbConfig.host; // string\n    const port = this.dbConfig.port; // number\n  }\n}\n\n// Environment files support\nConfigModule.forRoot({\n  envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],\n});\n\n// .env.development\n// DB_HOST=localhost\n// DB_PORT=5432\n\n// .env.production\n// DB_HOST=prod-db.example.com\n// DB_PORT=5432\n```\n\nReference: [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)\n\n---\n\n### 10.3 Use Structured Logging\n\n**Impact: MEDIUM-HIGH** — Structured logging enables effective debugging and monitoring\n\nUse NestJS Logger with structured JSON output in production. Include contextual information (request ID, user ID, operation) to trace requests across services. Avoid console.log and implement proper log levels.\n\n**Incorrect (using console.log in production):**\n\n```typescript\n// Use console.log in production\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    console.log('Creating user:', dto);\n    // Not structured, no levels, lost in production logs\n\n    try {\n      const user = await this.repo.save(dto);\n      console.log('User created:', user.id);\n      return user;\n    } catch (error) {\n      console.log('Error:', error); // Using log for errors\n      throw error;\n    }\n  }\n}\n\n// Log sensitive data\nconsole.log('Login attempt:', { email, password }); // SECURITY RISK!\n\n// Inconsistent log format\nlogger.log('User ' + userId + ' created at ' + new Date());\n// Hard to parse, no structure\n```\n\n**Correct (use structured logging with context):**\n\n```typescript\n// Configure logger in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule, {\n    logger:\n      process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],\n  });\n}\n\n// Use NestJS Logger with context\n@Injectable()\nexport class UsersService {\n  private readonly logger = new Logger(UsersService.name);\n\n  async createUser(dto: CreateUserDto): Promise<User> {\n    this.logger.log('Creating user', { email: dto.email });\n\n    try {\n      const user = await this.repo.save(dto);\n      this.logger.log('User created', { userId: user.id });\n      return user;\n    } catch (error) {\n      this.logger.error('Failed to create user', error.stack, {\n        email: dto.email,\n      });\n      throw error;\n    }\n  }\n}\n\n// Custom logger for JSON output\n@Injectable()\nexport class JsonLogger implements LoggerService {\n  log(message: string, context?: object): void {\n    console.log(\n      JSON.stringify({\n        level: 'info',\n        timestamp: new Date().toISOString(),\n        message,\n        ...context,\n      }),\n    );\n  }\n\n  error(message: string, trace?: string, context?: object): void {\n    console.error(\n      JSON.stringify({\n        level: 'error',\n        timestamp: new Date().toISOString(),\n        message,\n        trace,\n        ...context,\n      }),\n    );\n  }\n\n  warn(message: string, context?: object): void {\n    console.warn(\n      JSON.stringify({\n        level: 'warn',\n        timestamp: new Date().toISOString(),\n        message,\n        ...context,\n      }),\n    );\n  }\n\n  debug(message: string, context?: object): void {\n    console.debug(\n      JSON.stringify({\n        level: 'debug',\n        timestamp: new Date().toISOString(),\n        message,\n        ...context,\n      }),\n    );\n  }\n}\n\n// Request context logging with ClsModule\nimport { ClsModule, ClsService } from 'nestjs-cls';\n\n@Module({\n  imports: [\n    ClsModule.forRoot({\n      global: true,\n      middleware: {\n        mount: true,\n        generateId: true,\n      },\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Middleware to set request context\n@Injectable()\nexport class RequestContextMiddleware implements NestMiddleware {\n  constructor(private cls: ClsService) {}\n\n  use(req: Request, res: Response, next: NextFunction): void {\n    const requestId = req.headers['x-request-id'] || randomUUID();\n    this.cls.set('requestId', requestId);\n    this.cls.set('userId', req.user?.id);\n\n    res.setHeader('x-request-id', requestId);\n    next();\n  }\n}\n\n// Logger that includes request context\n@Injectable()\nexport class ContextLogger {\n  constructor(private cls: ClsService) {}\n\n  log(message: string, data?: object): void {\n    console.log(\n      JSON.stringify({\n        level: 'info',\n        timestamp: new Date().toISOString(),\n        requestId: this.cls.get('requestId'),\n        userId: this.cls.get('userId'),\n        message,\n        ...data,\n      }),\n    );\n  }\n\n  error(message: string, error: Error, data?: object): void {\n    console.error(\n      JSON.stringify({\n        level: 'error',\n        timestamp: new Date().toISOString(),\n        requestId: this.cls.get('requestId'),\n        userId: this.cls.get('userId'),\n        message,\n        error: error.message,\n        stack: error.stack,\n        ...data,\n      }),\n    );\n  }\n}\n\n// Pino integration for high-performance logging\nimport { LoggerModule } from 'nestjs-pino';\n\n@Module({\n  imports: [\n    LoggerModule.forRoot({\n      pinoHttp: {\n        level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',\n        transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,\n        redact: ['req.headers.authorization', 'req.body.password'],\n        serializers: {\n          req: req => ({\n            method: req.method,\n            url: req.url,\n            query: req.query,\n          }),\n          res: res => ({\n            statusCode: res.statusCode,\n          }),\n        },\n      },\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Usage with Pino\n@Injectable()\nexport class UsersService {\n  constructor(private logger: PinoLogger) {\n    this.logger.setContext(UsersService.name);\n  }\n\n  async findOne(id: string): Promise<User> {\n    this.logger.info({ userId: id }, 'Finding user');\n    // Pino uses first arg for data, second for message\n  }\n}\n```\n\nReference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)\n\n---\n\n## References\n\n- https://docs.nestjs.com\n- https://github.com/nestjs/nest\n- https://typeorm.io\n- https://github.com/typestack/class-validator\n- https://github.com/goldbergyoni/nodebestpractices\n\n---\n\n_Generated by build-agents.ts on 2026-01-16_"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/README.md",
    "content": "# NestJS Best Practices\n\n📖 [For Humans <3](https://kadajett.github.io/agent-nestjs-skills/)\n\nA structured repository for creating and maintaining NestJS Best Practices optimized for agents and LLMs.\n\n## Installation\n\nInstall this skill using [skills](https://github.com/vercel-labs/skills):\n\n```bash\n# GitHub shorthand\nnpx skills add Kadajett/agent-nestjs-skills\n\n# Install globally (available across all projects)\nnpx skills add Kadajett/agent-nestjs-skills --global\n\n# Install for specific agents\nnpx skills add Kadajett/agent-nestjs-skills -a claude-code -a cursor\n```\n\n### Supported Agents\n\n- Claude Code\n- OpenCode\n- Codex\n- Cursor\n- Antigravity\n- Roo Code\n\n## Structure\n\n- `rules/` - Individual rule files (one per rule)\n  - `_sections.md` - Section metadata (titles, impacts, descriptions)\n  - `_template.md` - Template for creating new rules\n  - `area-description.md` - Individual rule files\n- `scripts/` - Build scripts and utilities\n- `metadata.json` - Document metadata (version, organization, abstract)\n- **`AGENTS.md`** - Compiled output (generated)\n\n## Getting Started\n\n1. Install dependencies:\n\n   ```bash\n   cd scripts && npm install\n   ```\n\n2. Build AGENTS.md from rules:\n   ```bash\n   npm run build\n   # or\n   ./scripts/build.sh\n   ```\n\n## Creating a New Rule\n\n1. Copy `rules/_template.md` to `rules/area-description.md`\n2. Choose the appropriate area prefix:\n   - `arch-` for Architecture (Section 1)\n   - `di-` for Dependency Injection (Section 2)\n   - `error-` for Error Handling (Section 3)\n   - `security-` for Security (Section 4)\n   - `perf-` for Performance (Section 5)\n   - `test-` for Testing (Section 6)\n   - `db-` for Database & ORM (Section 7)\n   - `api-` for API Design (Section 8)\n   - `micro-` for Microservices (Section 9)\n   - `devops-` for DevOps & Deployment (Section 10)\n3. Fill in the frontmatter and content\n4. Ensure you have clear examples with explanations\n5. Run the build script to regenerate AGENTS.md\n\n## Rule File Structure\n\nEach rule file should follow this structure:\n\n````markdown\n---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: Optional description\ntags: tag1, tag2, tag3\n---\n\n## Rule Title Here\n\nBrief explanation of the rule and why it matters.\n\n**Incorrect (description of what's wrong):**\n\n```typescript\n// Bad code example\n```\n````\n\n**Correct (description of what's right):**\n\n```typescript\n// Good code example\n```\n\nOptional explanatory text after examples.\n\nReference: [NestJS Documentation](https://docs.nestjs.com)\n\n## File Naming Convention\n\n- Files starting with `_` are special (excluded from build)\n- Rule files: `area-description.md` (e.g., `arch-avoid-circular-deps.md`)\n- Section is automatically inferred from filename prefix\n- Rules are sorted alphabetically by title within each section\n- IDs (e.g., 1.1, 1.2) are auto-generated during build\n\n## Impact Levels\n\n| Level       | Description                                                                           |\n| ----------- | ------------------------------------------------------------------------------------- |\n| CRITICAL    | Violations cause runtime errors, security vulnerabilities, or architectural breakdown |\n| HIGH        | Significant impact on reliability, security, or maintainability                       |\n| MEDIUM-HIGH | Notable impact on quality and developer experience                                    |\n| MEDIUM      | Moderate impact on code quality and best practices                                    |\n| LOW-MEDIUM  | Minor improvements for consistency and maintainability                                |\n\n## Scripts\n\n- `npm run build` (in scripts/) - Compile rules into AGENTS.md\n\n## Contributing\n\nWhen adding or modifying rules:\n\n1. Use the correct filename prefix for your section\n2. Follow the `_template.md` structure\n3. Include clear bad/good examples with explanations\n4. Add appropriate tags\n5. Run the build script to regenerate AGENTS.md\n6. Rules are automatically sorted by title - no need to manage numbers!\n\n## Documentation Website\n\nThe documentation website source code lives on the [`docs` branch](https://github.com/Kadajett/agent-nestjs-skills/tree/docs/website). This separation keeps the skill installation lightweight while maintaining the full documentation site.\n\nTo contribute to the website:\n\n```bash\ngit checkout docs\ncd website\nnpm install\nnpm run dev\n```\n\n## Acknowledgments\n\n- Inspired by the [Vercel React Best Practices](https://github.com/vercel-labs/agent-skills) skill structure\n- Compatible with [skills](https://github.com/vercel-labs/skills) for easy installation across coding agents\n\n## Compatible Agents\n\nThese NestJS skills work with:\n\n- [Claude Code](https://claude.ai/code) - Anthropic's official CLI\n- [AdaL](https://sylph.ai/adal) - Self-evolving AI coding agent with MCP support"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/SKILL.md",
    "content": "---\nname: nestjs-best-practices\ndescription: NestJS best practices and architecture patterns for building production-ready applications. This skill should be used when writing, reviewing, or refactoring NestJS code to ensure proper patterns for modules, dependency injection, security, and performance.\nlicense: MIT\nmetadata:\n  author: Kadajett\n  version: '1.1.0'\n---\n\n# NestJS Best Practices\n\nComprehensive best practices guide for NestJS applications. Contains 40 rules across 10 categories, prioritized by impact to guide automated refactoring and code generation.\n\n## When to Apply\n\nReference these guidelines when:\n\n- Writing new NestJS modules, controllers, or services\n- Implementing authentication and authorization\n- Reviewing code for architecture and security issues\n- Refactoring existing NestJS codebases\n- Optimizing performance or database queries\n- Building microservices architectures\n\n## Rule Categories by Priority\n\n| Priority | Category             | Impact      | Prefix      |\n| -------- | -------------------- | ----------- | ----------- |\n| 1        | Architecture         | CRITICAL    | `arch-`     |\n| 2        | Dependency Injection | CRITICAL    | `di-`       |\n| 3        | Error Handling       | HIGH        | `error-`    |\n| 4        | Security             | HIGH        | `security-` |\n| 5        | Performance          | HIGH        | `perf-`     |\n| 6        | Testing              | MEDIUM-HIGH | `test-`     |\n| 7        | Database & ORM       | MEDIUM-HIGH | `db-`       |\n| 8        | API Design           | MEDIUM      | `api-`      |\n| 9        | Microservices        | MEDIUM      | `micro-`    |\n| 10       | DevOps & Deployment  | LOW-MEDIUM  | `devops-`   |\n\n## Quick Reference\n\n### 1. Architecture (CRITICAL)\n\n- `arch-avoid-circular-deps` - Avoid circular module dependencies\n- `arch-feature-modules` - Organize by feature, not technical layer\n- `arch-module-sharing` - Proper module exports/imports, avoid duplicate providers\n- `arch-single-responsibility` - Focused services over \"god services\"\n- `arch-use-repository-pattern` - Abstract database logic for testability\n- `arch-use-events` - Event-driven architecture for decoupling\n\n### 2. Dependency Injection (CRITICAL)\n\n- `di-avoid-service-locator` - Avoid service locator anti-pattern\n- `di-interface-segregation` - Interface Segregation Principle (ISP)\n- `di-liskov-substitution` - Liskov Substitution Principle (LSP)\n- `di-prefer-constructor-injection` - Constructor over property injection\n- `di-scope-awareness` - Understand singleton/request/transient scopes\n- `di-use-interfaces-tokens` - Use injection tokens for interfaces\n\n### 3. Error Handling (HIGH)\n\n- `error-use-exception-filters` - Centralized exception handling\n- `error-throw-http-exceptions` - Use NestJS HTTP exceptions\n- `error-handle-async-errors` - Handle async errors properly\n\n### 4. Security (HIGH)\n\n- `security-auth-jwt` - Secure JWT authentication\n- `security-validate-all-input` - Validate with class-validator\n- `security-use-guards` - Authentication and authorization guards\n- `security-sanitize-output` - Prevent XSS attacks\n- `security-rate-limiting` - Implement rate limiting\n\n### 5. Performance (HIGH)\n\n- `perf-async-hooks` - Proper async lifecycle hooks\n- `perf-use-caching` - Implement caching strategies\n- `perf-optimize-database` - Optimize database queries\n- `perf-lazy-loading` - Lazy load modules for faster startup\n\n### 6. Testing (MEDIUM-HIGH)\n\n- `test-use-testing-module` - Use NestJS testing utilities\n- `test-e2e-supertest` - E2E testing with Supertest\n- `test-mock-external-services` - Mock external dependencies\n\n### 7. Database & ORM (MEDIUM-HIGH)\n\n- `db-use-transactions` - Transaction management\n- `db-avoid-n-plus-one` - Avoid N+1 query problems\n- `db-use-migrations` - Use migrations for schema changes\n\n### 8. API Design (MEDIUM)\n\n- `api-use-dto-serialization` - DTO and response serialization\n- `api-use-interceptors` - Cross-cutting concerns\n- `api-versioning` - API versioning strategies\n- `api-use-pipes` - Input transformation with pipes\n\n### 9. Microservices (MEDIUM)\n\n- `micro-use-patterns` - Message and event patterns\n- `micro-use-health-checks` - Health checks for orchestration\n- `micro-use-queues` - Background job processing\n\n### 10. DevOps & Deployment (LOW-MEDIUM)\n\n- `devops-use-config-module` - Environment configuration\n- `devops-use-logging` - Structured logging\n- `devops-graceful-shutdown` - Zero-downtime deployments\n\n## How to Use\n\nRead individual rule files for detailed explanations and code examples:\n\n```\nrules/arch-avoid-circular-deps.md\nrules/security-validate-all-input.md\nrules/_sections.md\n```\n\nEach rule file contains:\n\n- Brief explanation of why it matters\n- Incorrect code example with explanation\n- Correct code example with explanation\n- Additional context and references\n\n## Full Compiled Document\n\nFor the complete guide with all rules expanded: `AGENTS.md`"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/api-use-dto-serialization.md",
    "content": "---\ntitle: Use DTOs and Serialization for API Responses\nimpact: MEDIUM\nimpactDescription: Response DTOs prevent accidental data exposure and ensure consistency\ntags: api, dto, serialization, class-transformer\n---\n\n## Use DTOs and Serialization for API Responses\n\nNever return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract.\n\n**Incorrect (returning entities directly or manual spreading):**\n\n```typescript\n// Return entities directly\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findById(id);\n    // Returns: { id, email, passwordHash, ssn, internalNotes, ... }\n    // Exposes sensitive data!\n  }\n}\n\n// Manual object spreading (error-prone)\n@Get(':id')\nasync findOne(@Param('id') id: string) {\n  const user = await this.usersService.findById(id);\n  return {\n    id: user.id,\n    email: user.email,\n    name: user.name,\n    // Easy to forget to exclude sensitive fields\n    // Hard to maintain across endpoints\n  };\n}\n```\n\n**Correct (use class-transformer with @Exclude and response DTOs):**\n\n```typescript\n// Enable class-transformer globally\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));\n  await app.listen(3000);\n}\n\n// Entity with serialization control\n@Entity()\nexport class User {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Column()\n  email: string;\n\n  @Column()\n  name: string;\n\n  @Column()\n  @Exclude() // Never include in responses\n  passwordHash: string;\n\n  @Column({ nullable: true })\n  @Exclude()\n  ssn: string;\n\n  @Column({ default: false })\n  @Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests\n  isAdmin: boolean;\n\n  @CreateDateColumn()\n  createdAt: Date;\n\n  @Column()\n  @Exclude()\n  internalNotes: string;\n}\n\n// Now returning entity is safe\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findById(id);\n    // Returns: { id, email, name, createdAt }\n    // Sensitive fields excluded automatically\n  }\n}\n\n// For different response shapes, use explicit DTOs\nexport class UserResponseDto {\n  @Expose()\n  id: string;\n\n  @Expose()\n  email: string;\n\n  @Expose()\n  name: string;\n\n  @Expose()\n  @Transform(({ obj }) => obj.posts?.length || 0)\n  postCount: number;\n\n  constructor(partial: Partial<User>) {\n    Object.assign(this, partial);\n  }\n}\n\nexport class UserDetailResponseDto extends UserResponseDto {\n  @Expose()\n  createdAt: Date;\n\n  @Expose()\n  @Type(() => PostResponseDto)\n  posts: PostResponseDto[];\n}\n\n// Controller with explicit DTOs\n@Controller('users')\nexport class UsersController {\n  @Get()\n  @SerializeOptions({ type: UserResponseDto })\n  async findAll(): Promise<UserResponseDto[]> {\n    const users = await this.usersService.findAll();\n    return users.map(u => plainToInstance(UserResponseDto, u));\n  }\n\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<UserDetailResponseDto> {\n    const user = await this.usersService.findByIdWithPosts(id);\n    return plainToInstance(UserDetailResponseDto, user, {\n      excludeExtraneousValues: true,\n    });\n  }\n}\n\n// Groups for conditional serialization\nexport class UserDto {\n  @Expose()\n  id: string;\n\n  @Expose()\n  name: string;\n\n  @Expose({ groups: ['admin'] })\n  email: string;\n\n  @Expose({ groups: ['admin'] })\n  createdAt: Date;\n\n  @Expose({ groups: ['admin', 'owner'] })\n  settings: UserSettings;\n}\n\n@Controller('users')\nexport class UsersController {\n  @Get()\n  @SerializeOptions({ groups: ['public'] })\n  async findAllPublic(): Promise<UserDto[]> {\n    // Returns: { id, name }\n  }\n\n  @Get('admin')\n  @UseGuards(AdminGuard)\n  @SerializeOptions({ groups: ['admin'] })\n  async findAllAdmin(): Promise<UserDto[]> {\n    // Returns: { id, name, email, createdAt }\n  }\n\n  @Get('me')\n  @SerializeOptions({ groups: ['owner'] })\n  async getProfile(@CurrentUser() user: User): Promise<UserDto> {\n    // Returns: { id, name, settings }\n  }\n}\n```\n\nReference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/api-use-interceptors.md",
    "content": "---\ntitle: Use Interceptors for Cross-Cutting Concerns\nimpact: MEDIUM-HIGH\nimpactDescription: Interceptors provide clean separation for cross-cutting logic\ntags: api, interceptors, logging, caching\n---\n\n## Use Interceptors for Cross-Cutting Concerns\n\nInterceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams.\n\n**Incorrect (logging and transformation in every method):**\n\n```typescript\n// Logging in every controller method\n@Controller('users')\nexport class UsersController {\n  @Get()\n  async findAll(): Promise<User[]> {\n    const start = Date.now();\n    this.logger.log('findAll called');\n\n    const users = await this.usersService.findAll();\n\n    this.logger.log(`findAll completed in ${Date.now() - start}ms`);\n    return users;\n  }\n\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    const start = Date.now();\n    this.logger.log(`findOne called with id: ${id}`);\n\n    const user = await this.usersService.findOne(id);\n\n    this.logger.log(`findOne completed in ${Date.now() - start}ms`);\n    return user;\n  }\n  // Repeated in every method!\n}\n\n// Manual response wrapping\n@Get()\nasync findAll(): Promise<{ data: User[]; meta: Meta }> {\n  const users = await this.usersService.findAll();\n  return {\n    data: users,\n    meta: { timestamp: new Date(), count: users.length },\n  };\n}\n```\n\n**Correct (use interceptors for cross-cutting concerns):**\n\n```typescript\n// Logging interceptor\n@Injectable()\nexport class LoggingInterceptor implements NestInterceptor {\n  private readonly logger = new Logger('HTTP');\n\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    const request = context.switchToHttp().getRequest();\n    const { method, url, body } = request;\n    const now = Date.now();\n\n    return next.handle().pipe(\n      tap({\n        next: (data) => {\n          const response = context.switchToHttp().getResponse();\n          this.logger.log(\n            `${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`,\n          );\n        },\n        error: (error) => {\n          this.logger.error(\n            `${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`,\n            error.stack,\n          );\n        },\n      }),\n    );\n  }\n}\n\n// Response transformation interceptor\n@Injectable()\nexport class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {\n    return next.handle().pipe(\n      map((data) => ({\n        data,\n        meta: {\n          timestamp: new Date().toISOString(),\n          path: context.switchToHttp().getRequest().url,\n        },\n      })),\n    );\n  }\n}\n\n// Timeout interceptor\n@Injectable()\nexport class TimeoutInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    return next.handle().pipe(\n      timeout(5000),\n      catchError((err) => {\n        if (err instanceof TimeoutError) {\n          throw new RequestTimeoutException('Request timed out');\n        }\n        throw err;\n      }),\n    );\n  }\n}\n\n// Apply globally or per-controller\n@Module({\n  providers: [\n    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },\n    { provide: APP_INTERCEPTOR, useClass: TransformInterceptor },\n  ],\n})\nexport class AppModule {}\n\n// Or per-controller\n@Controller('users')\n@UseInterceptors(LoggingInterceptor)\nexport class UsersController {\n  @Get()\n  async findAll(): Promise<User[]> {\n    // Clean business logic only\n    return this.usersService.findAll();\n  }\n}\n\n// Custom cache interceptor with TTL\n@Injectable()\nexport class HttpCacheInterceptor implements NestInterceptor {\n  constructor(\n    private cacheManager: Cache,\n    private reflector: Reflector,\n  ) {}\n\n  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {\n    const request = context.switchToHttp().getRequest();\n\n    // Only cache GET requests\n    if (request.method !== 'GET') {\n      return next.handle();\n    }\n\n    const cacheKey = this.generateKey(request);\n    const ttl = this.reflector.get<number>('cacheTTL', context.getHandler()) || 300;\n\n    const cached = await this.cacheManager.get(cacheKey);\n    if (cached) {\n      return of(cached);\n    }\n\n    return next.handle().pipe(\n      tap((response) => {\n        this.cacheManager.set(cacheKey, response, ttl);\n      }),\n    );\n  }\n\n  private generateKey(request: Request): string {\n    return `cache:${request.url}:${JSON.stringify(request.query)}`;\n  }\n}\n\n// Usage with custom TTL\n@Get()\n@SetMetadata('cacheTTL', 600)\n@UseInterceptors(HttpCacheInterceptor)\nasync findAll(): Promise<User[]> {\n  return this.usersService.findAll();\n}\n\n// Error mapping interceptor\n@Injectable()\nexport class ErrorMappingInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    return next.handle().pipe(\n      catchError((error) => {\n        if (error instanceof EntityNotFoundError) {\n          throw new NotFoundException(error.message);\n        }\n        if (error instanceof QueryFailedError) {\n          if (error.message.includes('duplicate')) {\n            throw new ConflictException('Resource already exists');\n          }\n        }\n        throw error;\n      }),\n    );\n  }\n}\n```\n\nReference: [NestJS Interceptors](https://docs.nestjs.com/interceptors)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/api-use-pipes.md",
    "content": "---\ntitle: Use Pipes for Input Transformation\nimpact: MEDIUM\nimpactDescription: Pipes ensure clean, validated data reaches your handlers\ntags: api, pipes, validation, transformation\n---\n\n## Use Pipes for Input Transformation\n\nUse built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers.\n\n**Incorrect (manual type parsing in handlers):**\n\n```typescript\n// Manual type parsing in handlers\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    // Manual validation in every handler\n    const uuid = id.trim();\n    if (!isUUID(uuid)) {\n      throw new BadRequestException('Invalid UUID');\n    }\n    return this.usersService.findOne(uuid);\n  }\n\n  @Get()\n  async findAll(\n    @Query('page') page: string,\n    @Query('limit') limit: string,\n  ): Promise<User[]> {\n    // Manual parsing and defaults\n    const pageNum = parseInt(page) || 1;\n    const limitNum = parseInt(limit) || 10;\n    return this.usersService.findAll(pageNum, limitNum);\n  }\n}\n\n// Type coercion without validation\n@Get()\nasync search(@Query('price') price: string): Promise<Product[]> {\n  const priceNum = +price; // NaN if invalid, no error\n  return this.productsService.findByPrice(priceNum);\n}\n```\n\n**Correct (use built-in and custom pipes):**\n\n```typescript\n// Use built-in pipes for common transformations\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {\n    // id is guaranteed to be a valid UUID\n    return this.usersService.findOne(id);\n  }\n\n  @Get()\n  async findAll(\n    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,\n    @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,\n  ): Promise<User[]> {\n    // Automatic defaults and type conversion\n    return this.usersService.findAll(page, limit);\n  }\n\n  @Get('by-status/:status')\n  async findByStatus(\n    @Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus,\n  ): Promise<User[]> {\n    return this.usersService.findByStatus(status);\n  }\n}\n\n// Custom pipe for business logic\n@Injectable()\nexport class ParseDatePipe implements PipeTransform<string, Date> {\n  transform(value: string): Date {\n    const date = new Date(value);\n    if (isNaN(date.getTime())) {\n      throw new BadRequestException('Invalid date format');\n    }\n    return date;\n  }\n}\n\n@Get('reports')\nasync getReports(\n  @Query('from', ParseDatePipe) from: Date,\n  @Query('to', ParseDatePipe) to: Date,\n): Promise<Report[]> {\n  return this.reportsService.findBetween(from, to);\n}\n\n// Custom transformation pipes\n@Injectable()\nexport class NormalizeEmailPipe implements PipeTransform<string, string> {\n  transform(value: string): string {\n    if (!value) return value;\n    return value.trim().toLowerCase();\n  }\n}\n\n// Parse comma-separated values\n@Injectable()\nexport class ParseArrayPipe implements PipeTransform<string, string[]> {\n  transform(value: string): string[] {\n    if (!value) return [];\n    return value.split(',').map((v) => v.trim()).filter(Boolean);\n  }\n}\n\n@Get('products')\nasync findProducts(\n  @Query('ids', ParseArrayPipe) ids: string[],\n  @Query('email', NormalizeEmailPipe) email: string,\n): Promise<Product[]> {\n  // ids is already an array, email is normalized\n  return this.productsService.findByIds(ids);\n}\n\n// Sanitize HTML input\n@Injectable()\nexport class SanitizeHtmlPipe implements PipeTransform<string, string> {\n  transform(value: string): string {\n    if (!value) return value;\n    return sanitizeHtml(value, { allowedTags: [] });\n  }\n}\n\n// Global validation pipe with transformation\napp.useGlobalPipes(\n  new ValidationPipe({\n    whitelist: true, // Strip non-DTO properties\n    transform: true, // Auto-transform to DTO types\n    transformOptions: {\n      enableImplicitConversion: true, // Convert query strings to numbers\n    },\n    forbidNonWhitelisted: true, // Throw on extra properties\n  }),\n);\n\n// DTO with transformation decorators\nexport class FindProductsDto {\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  page?: number = 1;\n\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  @Max(100)\n  limit?: number = 10;\n\n  @IsOptional()\n  @Transform(({ value }) => value?.toLowerCase())\n  @IsString()\n  search?: string;\n\n  @IsOptional()\n  @Transform(({ value }) => value?.split(','))\n  @IsArray()\n  @IsString({ each: true })\n  categories?: string[];\n}\n\n@Get()\nasync findAll(@Query() dto: FindProductsDto): Promise<Product[]> {\n  // dto is already transformed and validated\n  return this.productsService.findAll(dto);\n}\n\n// Pipe error customization\n@Injectable()\nexport class CustomParseIntPipe extends ParseIntPipe {\n  constructor() {\n    super({\n      exceptionFactory: (error) =>\n        new BadRequestException(`${error} must be a valid integer`),\n    });\n  }\n}\n\n// Or use options on built-in pipes\n@Get(':id')\nasync findOne(\n  @Param(\n    'id',\n    new ParseIntPipe({\n      errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,\n      exceptionFactory: () => new NotAcceptableException('ID must be numeric'),\n    }),\n  )\n  id: number,\n): Promise<Item> {\n  return this.itemsService.findOne(id);\n}\n```\n\nReference: [NestJS Pipes](https://docs.nestjs.com/pipes)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/api-versioning.md",
    "content": "---\ntitle: Use API Versioning for Breaking Changes\nimpact: MEDIUM\nimpactDescription: Versioning allows you to evolve APIs without breaking existing clients\ntags: api, versioning, breaking-changes, compatibility\n---\n\n## Use API Versioning for Breaking Changes\n\nUse NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints.\n\n**Incorrect (breaking changes without versioning):**\n\n```typescript\n// Breaking changes without versioning\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    // Original response: { id, name, email }\n    // Later changed to: { id, firstName, lastName, emailAddress }\n    // Old clients break!\n    return this.usersService.findOne(id);\n  }\n}\n\n// Manual versioning in routes\n@Controller('v1/users')\nexport class UsersV1Controller {}\n\n@Controller('v2/users')\nexport class UsersV2Controller {}\n// Inconsistent, error-prone, hard to maintain\n```\n\n**Correct (use NestJS built-in versioning):**\n\n```typescript\n// Enable versioning in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  // URI versioning: /v1/users, /v2/users\n  app.enableVersioning({\n    type: VersioningType.URI,\n    defaultVersion: '1',\n  });\n\n  // Or header versioning: X-API-Version: 1\n  app.enableVersioning({\n    type: VersioningType.HEADER,\n    header: 'X-API-Version',\n    defaultVersion: '1',\n  });\n\n  // Or media type: Accept: application/json;v=1\n  app.enableVersioning({\n    type: VersioningType.MEDIA_TYPE,\n    key: 'v=',\n    defaultVersion: '1',\n  });\n\n  await app.listen(3000);\n}\n\n// Version-specific controllers\n@Controller('users')\n@Version('1')\nexport class UsersV1Controller {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<UserV1Response> {\n    const user = await this.usersService.findOne(id);\n    // V1 response format\n    return {\n      id: user.id,\n      name: user.name,\n      email: user.email,\n    };\n  }\n}\n\n@Controller('users')\n@Version('2')\nexport class UsersV2Controller {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<UserV2Response> {\n    const user = await this.usersService.findOne(id);\n    // V2 response format with breaking changes\n    return {\n      id: user.id,\n      firstName: user.firstName,\n      lastName: user.lastName,\n      emailAddress: user.email,\n      createdAt: user.createdAt,\n    };\n  }\n}\n\n// Per-route versioning - different versions for different routes\n@Controller('users')\nexport class UsersController {\n  @Get()\n  @Version('1')\n  findAllV1(): Promise<UserV1Response[]> {\n    return this.usersService.findAllV1();\n  }\n\n  @Get()\n  @Version('2')\n  findAllV2(): Promise<UserV2Response[]> {\n    return this.usersService.findAllV2();\n  }\n\n  @Get(':id')\n  @Version(['1', '2']) // Same handler for multiple versions\n  findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findOne(id);\n  }\n\n  @Post()\n  @Version(VERSION_NEUTRAL) // Available in all versions\n  create(@Body() dto: CreateUserDto): Promise<User> {\n    return this.usersService.create(dto);\n  }\n}\n\n// Shared service with version-specific logic\n@Injectable()\nexport class UsersService {\n  async findOne(id: string, version: string): Promise<any> {\n    const user = await this.repo.findOne({ where: { id } });\n\n    if (version === '1') {\n      return this.toV1Response(user);\n    }\n    return this.toV2Response(user);\n  }\n\n  private toV1Response(user: User): UserV1Response {\n    return {\n      id: user.id,\n      name: `${user.firstName} ${user.lastName}`,\n      email: user.email,\n    };\n  }\n\n  private toV2Response(user: User): UserV2Response {\n    return {\n      id: user.id,\n      firstName: user.firstName,\n      lastName: user.lastName,\n      emailAddress: user.email,\n      createdAt: user.createdAt,\n    };\n  }\n}\n\n// Controller extracts version\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string, @Headers('X-API-Version') version: string = '1'): Promise<any> {\n    return this.usersService.findOne(id, version);\n  }\n}\n\n// Deprecation strategy - mark old versions as deprecated\n@Controller('users')\n@Version('1')\n@UseInterceptors(DeprecationInterceptor)\nexport class UsersV1Controller {\n  // All V1 routes will include deprecation warning\n}\n\n@Injectable()\nexport class DeprecationInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {\n    const response = context.switchToHttp().getResponse();\n    response.setHeader('Deprecation', 'true');\n    response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT');\n    response.setHeader('Link', '</v2/users>; rel=\"successor-version\"');\n\n    return next.handle();\n  }\n}\n```\n\nReference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/arch-avoid-circular-deps.md",
    "content": "---\ntitle: Avoid Circular Dependencies\nimpact: CRITICAL\nimpactDescription: '#1 cause of runtime crashes'\ntags: architecture, modules, dependencies\n---\n\n## Avoid Circular Dependencies\n\nCircular dependencies occur when Module A imports Module B, and Module B imports Module A (directly or transitively). NestJS can sometimes resolve these through forward references, but they indicate architectural problems and should be avoided. This is the #1 cause of runtime crashes in NestJS applications.\n\n**Incorrect (circular module imports):**\n\n```typescript\n// users.module.ts\n@Module({\n  imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular\n  providers: [UsersService],\n  exports: [UsersService],\n})\nexport class UsersModule {}\n\n// orders.module.ts\n@Module({\n  imports: [UsersModule], // Circular dependency!\n  providers: [OrdersService],\n  exports: [OrdersService],\n})\nexport class OrdersModule {}\n```\n\n**Correct (extract shared logic or use events):**\n\n```typescript\n// Option 1: Extract shared logic to a third module\n// shared.module.ts\n@Module({\n  providers: [SharedService],\n  exports: [SharedService],\n})\nexport class SharedModule {}\n\n// users.module.ts\n@Module({\n  imports: [SharedModule],\n  providers: [UsersService],\n})\nexport class UsersModule {}\n\n// orders.module.ts\n@Module({\n  imports: [SharedModule],\n  providers: [OrdersService],\n})\nexport class OrdersModule {}\n\n// Option 2: Use events for decoupled communication\n// users.service.ts\n@Injectable()\nexport class UsersService {\n  constructor(private eventEmitter: EventEmitter2) {}\n\n  async createUser(data: CreateUserDto) {\n    const user = await this.userRepo.save(data);\n    this.eventEmitter.emit('user.created', user);\n    return user;\n  }\n}\n\n// orders.service.ts\n@Injectable()\nexport class OrdersService {\n  @OnEvent('user.created')\n  handleUserCreated(user: User) {\n    // React to user creation without direct dependency\n  }\n}\n```\n\nReference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/arch-feature-modules.md",
    "content": "---\ntitle: Organize by Feature Modules\nimpact: CRITICAL\nimpactDescription: '3-5x faster onboarding and development'\ntags: architecture, modules, organization\n---\n\n## Organize by Feature Modules\n\nOrganize your application into feature modules that encapsulate related functionality. Each feature module should be self-contained with its own controllers, services, entities, and DTOs. Avoid organizing by technical layer (all controllers together, all services together). This enables 3-5x faster onboarding and feature development.\n\n**Incorrect (technical layer organization):**\n\n```typescript\n// Technical layer organization (anti-pattern)\nsrc/\n├── controllers/\n│   ├── users.controller.ts\n│   ├── orders.controller.ts\n│   └── products.controller.ts\n├── services/\n│   ├── users.service.ts\n│   ├── orders.service.ts\n│   └── products.service.ts\n├── entities/\n│   ├── user.entity.ts\n│   ├── order.entity.ts\n│   └── product.entity.ts\n└── app.module.ts  // Imports everything directly\n```\n\n**Correct (feature module organization):**\n\n```typescript\n// Feature module organization\nsrc/\n├── users/\n│   ├── dto/\n│   │   ├── create-user.dto.ts\n│   │   └── update-user.dto.ts\n│   ├── entities/\n│   │   └── user.entity.ts\n│   ├── users.controller.ts\n│   ├── users.service.ts\n│   ├── users.repository.ts\n│   └── users.module.ts\n├── orders/\n│   ├── dto/\n│   ├── entities/\n│   ├── orders.controller.ts\n│   ├── orders.service.ts\n│   └── orders.module.ts\n├── shared/\n│   ├── guards/\n│   ├── interceptors/\n│   ├── filters/\n│   └── shared.module.ts\n└── app.module.ts\n\n// users.module.ts\n@Module({\n  imports: [TypeOrmModule.forFeature([User])],\n  controllers: [UsersController],\n  providers: [UsersService, UsersRepository],\n  exports: [UsersService], // Only export what others need\n})\nexport class UsersModule {}\n\n// app.module.ts\n@Module({\n  imports: [\n    ConfigModule.forRoot(),\n    TypeOrmModule.forRoot(),\n    UsersModule,\n    OrdersModule,\n    SharedModule,\n  ],\n})\nexport class AppModule {}\n```\n\nReference: [NestJS Modules](https://docs.nestjs.com/modules)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/arch-module-sharing.md",
    "content": "---\ntitle: Use Proper Module Sharing Patterns\nimpact: CRITICAL\nimpactDescription: Prevents duplicate instances, memory leaks, and state inconsistency\ntags: architecture, modules, sharing, exports\n---\n\n## Use Proper Module Sharing Patterns\n\nNestJS modules are singletons by default. When a service is properly exported from a module and that module is imported elsewhere, the same instance is shared. However, providing a service in multiple modules creates separate instances, leading to memory waste, state inconsistency, and confusing behavior. Always encapsulate services in dedicated modules, export them explicitly, and import the module where needed.\n\n**Incorrect (service provided in multiple modules):**\n\n```typescript\n// StorageService provided directly in multiple modules - WRONG\n// storage.service.ts\n@Injectable()\nexport class StorageService {\n  private cache = new Map(); // Each instance has separate state!\n\n  store(key: string, value: any) {\n    this.cache.set(key, value);\n  }\n}\n\n// app.module.ts\n@Module({\n  providers: [StorageService], // Instance #1\n  controllers: [AppController],\n})\nexport class AppModule {}\n\n// videos.module.ts\n@Module({\n  providers: [StorageService], // Instance #2 - different from AppModule!\n  controllers: [VideosController],\n})\nexport class VideosModule {}\n\n// Problems:\n// 1. Two separate StorageService instances exist\n// 2. cache.set() in VideosModule doesn't affect AppModule's cache\n// 3. Memory wasted on duplicate instances\n// 4. Debugging nightmares when state doesn't sync\n```\n\n**Correct (dedicated module with exports):**\n\n```typescript\n// storage/storage.module.ts\n@Module({\n  providers: [StorageService],\n  exports: [StorageService], // Make available to importers\n})\nexport class StorageModule {}\n\n// videos/videos.module.ts\n@Module({\n  imports: [StorageModule], // Import the module, not the service\n  controllers: [VideosController],\n  providers: [VideosService],\n})\nexport class VideosModule {}\n\n// channels/channels.module.ts\n@Module({\n  imports: [StorageModule], // Same instance shared\n  controllers: [ChannelsController],\n  providers: [ChannelsService],\n})\nexport class ChannelsModule {}\n\n// app.module.ts\n@Module({\n  imports: [\n    StorageModule, // Only if AppModule itself needs StorageService\n    VideosModule,\n    ChannelsModule,\n  ],\n})\nexport class AppModule {}\n\n// Now all modules share the SAME StorageService instance\n```\n\n**When to use @Global() (sparingly):**\n\n```typescript\n// ONLY for truly cross-cutting concerns\n@Global()\n@Module({\n  providers: [ConfigService, LoggerService],\n  exports: [ConfigService, LoggerService],\n})\nexport class CoreModule {}\n\n// Import once in AppModule\n@Module({\n  imports: [CoreModule], // Registered globally, available everywhere\n})\nexport class AppModule {}\n\n// Other modules don't need to import CoreModule\n@Module({\n  controllers: [UsersController],\n  providers: [UsersService], // Can inject ConfigService without importing\n})\nexport class UsersModule {}\n\n// WARNING: Don't make everything global!\n// - Hides dependencies (can't see what a module needs from imports)\n// - Makes testing harder\n// - Reserve for: config, logging, database connections\n```\n\n**Module re-exporting pattern:**\n\n```typescript\n// common.module.ts - shared utilities\n@Module({\n  providers: [DateService, ValidationService],\n  exports: [DateService, ValidationService],\n})\nexport class CommonModule {}\n\n// core.module.ts - re-exports common for convenience\n@Module({\n  imports: [CommonModule, DatabaseModule],\n  exports: [CommonModule, DatabaseModule], // Re-export for consumers\n})\nexport class CoreModule {}\n\n// feature.module.ts - imports CoreModule, gets both\n@Module({\n  imports: [CoreModule], // Gets CommonModule + DatabaseModule\n  controllers: [FeatureController],\n})\nexport class FeatureModule {}\n```\n\nReference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/arch-single-responsibility.md",
    "content": "---\ntitle: Single Responsibility for Services\nimpact: CRITICAL\nimpactDescription: '40%+ improvement in testability'\ntags: architecture, services, single-responsibility\n---\n\n## Single Responsibility for Services\n\nEach service should have a single, well-defined responsibility. Avoid \"god services\" that handle multiple unrelated concerns. If a service name includes \"And\" or handles more than one domain concept, it likely violates single responsibility. This reduces complexity and improves testability by 40%+.\n\n**Incorrect (god service anti-pattern):**\n\n```typescript\n// God service anti-pattern\n@Injectable()\nexport class UserAndOrderService {\n  constructor(\n    private userRepo: UserRepository,\n    private orderRepo: OrderRepository,\n    private mailer: MailService,\n    private payment: PaymentService,\n  ) {}\n\n  async createUser(dto: CreateUserDto) {\n    const user = await this.userRepo.save(dto);\n    await this.mailer.sendWelcome(user);\n    return user;\n  }\n\n  async createOrder(userId: string, dto: CreateOrderDto) {\n    const order = await this.orderRepo.save({ userId, ...dto });\n    await this.payment.charge(order);\n    await this.mailer.sendOrderConfirmation(order);\n    return order;\n  }\n\n  async calculateOrderStats(userId: string) {\n    // Stats logic mixed in\n  }\n\n  async validatePayment(orderId: string) {\n    // Payment logic mixed in\n  }\n}\n```\n\n**Correct (focused services with single responsibility):**\n\n```typescript\n// Focused services with single responsibility\n@Injectable()\nexport class UsersService {\n  constructor(private userRepo: UserRepository) {}\n\n  async create(dto: CreateUserDto): Promise<User> {\n    return this.userRepo.save(dto);\n  }\n\n  async findById(id: string): Promise<User> {\n    return this.userRepo.findOneOrFail({ where: { id } });\n  }\n}\n\n@Injectable()\nexport class OrdersService {\n  constructor(private orderRepo: OrderRepository) {}\n\n  async create(userId: string, dto: CreateOrderDto): Promise<Order> {\n    return this.orderRepo.save({ userId, ...dto });\n  }\n\n  async findByUser(userId: string): Promise<Order[]> {\n    return this.orderRepo.find({ where: { userId } });\n  }\n}\n\n@Injectable()\nexport class OrderStatsService {\n  constructor(private orderRepo: OrderRepository) {}\n\n  async calculateForUser(userId: string): Promise<OrderStats> {\n    // Focused stats calculation\n  }\n}\n\n// Orchestration in controller or dedicated orchestrator\n@Controller('orders')\nexport class OrdersController {\n  constructor(\n    private orders: OrdersService,\n    private payment: PaymentService,\n    private notifications: NotificationService,\n  ) {}\n\n  @Post()\n  async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) {\n    const order = await this.orders.create(user.id, dto);\n    await this.payment.charge(order);\n    await this.notifications.sendOrderConfirmation(order);\n    return order;\n  }\n}\n```\n\nReference: [NestJS Providers](https://docs.nestjs.com/providers)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/arch-use-events.md",
    "content": "---\ntitle: Use Event-Driven Architecture for Decoupling\nimpact: MEDIUM-HIGH\nimpactDescription: Enables async processing and modularity\ntags: architecture, events, decoupling\n---\n\n## Use Event-Driven Architecture for Decoupling\n\nUse `@nestjs/event-emitter` for intra-service events and message brokers for inter-service communication. Events allow modules to react to changes without direct dependencies, improving modularity and enabling async processing.\n\n**Incorrect (direct service coupling):**\n\n```typescript\n// Direct service coupling\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private inventoryService: InventoryService,\n    private emailService: EmailService,\n    private analyticsService: AnalyticsService,\n    private notificationService: NotificationService,\n    private loyaltyService: LoyaltyService,\n  ) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const order = await this.repo.save(dto);\n\n    // Tight coupling - OrdersService knows about all consumers\n    await this.inventoryService.reserve(order.items);\n    await this.emailService.sendConfirmation(order);\n    await this.analyticsService.track('order_created', order);\n    await this.notificationService.push(order.userId, 'Order placed');\n    await this.loyaltyService.addPoints(order.userId, order.total);\n\n    // Adding new behavior requires modifying this service\n    return order;\n  }\n}\n```\n\n**Correct (event-driven decoupling):**\n\n```typescript\n// Use EventEmitter for decoupling\nimport { EventEmitter2 } from '@nestjs/event-emitter';\n\n// Define event\nexport class OrderCreatedEvent {\n  constructor(\n    public readonly orderId: string,\n    public readonly userId: string,\n    public readonly items: OrderItem[],\n    public readonly total: number,\n  ) {}\n}\n\n// Service emits events\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private eventEmitter: EventEmitter2,\n    private repo: Repository<Order>,\n  ) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const order = await this.repo.save(dto);\n\n    // Emit event - no knowledge of consumers\n    this.eventEmitter.emit('order.created', new OrderCreatedEvent(order.id, order.userId, order.items, order.total));\n\n    return order;\n  }\n}\n\n// Listeners in separate modules\n@Injectable()\nexport class InventoryListener {\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    await this.inventoryService.reserve(event.items);\n  }\n}\n\n@Injectable()\nexport class EmailListener {\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    await this.emailService.sendConfirmation(event.orderId);\n  }\n}\n\n@Injectable()\nexport class AnalyticsListener {\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    await this.analyticsService.track('order_created', {\n      orderId: event.orderId,\n      total: event.total,\n    });\n  }\n}\n```\n\nReference: [NestJS Events](https://docs.nestjs.com/techniques/events)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/arch-use-repository-pattern.md",
    "content": "---\ntitle: Use Repository Pattern for Data Access\nimpact: HIGH\nimpactDescription: Decouples business logic from database\ntags: architecture, repository, data-access\n---\n\n## Use Repository Pattern for Data Access\n\nCreate custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code.\n\n**Incorrect (complex queries in services):**\n\n```typescript\n// Complex queries in services\n@Injectable()\nexport class UsersService {\n  constructor(@InjectRepository(User) private repo: Repository<User>) {}\n\n  async findActiveWithOrders(minOrders: number): Promise<User[]> {\n    // Complex query logic mixed with business logic\n    return this.repo\n      .createQueryBuilder('user')\n      .leftJoinAndSelect('user.orders', 'order')\n      .where('user.isActive = :active', { active: true })\n      .andWhere('user.deletedAt IS NULL')\n      .groupBy('user.id')\n      .having('COUNT(order.id) >= :min', { min: minOrders })\n      .orderBy('user.createdAt', 'DESC')\n      .getMany();\n  }\n\n  // Service becomes bloated with query logic\n}\n```\n\n**Correct (custom repository with encapsulated queries):**\n\n```typescript\n// Custom repository with encapsulated queries\n@Injectable()\nexport class UsersRepository {\n  constructor(@InjectRepository(User) private repo: Repository<User>) {}\n\n  async findById(id: string): Promise<User | null> {\n    return this.repo.findOne({ where: { id } });\n  }\n\n  async findByEmail(email: string): Promise<User | null> {\n    return this.repo.findOne({ where: { email } });\n  }\n\n  async findActiveWithMinOrders(minOrders: number): Promise<User[]> {\n    return this.repo\n      .createQueryBuilder('user')\n      .leftJoinAndSelect('user.orders', 'order')\n      .where('user.isActive = :active', { active: true })\n      .andWhere('user.deletedAt IS NULL')\n      .groupBy('user.id')\n      .having('COUNT(order.id) >= :min', { min: minOrders })\n      .orderBy('user.createdAt', 'DESC')\n      .getMany();\n  }\n\n  async save(user: User): Promise<User> {\n    return this.repo.save(user);\n  }\n}\n\n// Clean service with business logic only\n@Injectable()\nexport class UsersService {\n  constructor(private usersRepo: UsersRepository) {}\n\n  async getActiveUsersWithOrders(): Promise<User[]> {\n    return this.usersRepo.findActiveWithMinOrders(1);\n  }\n\n  async create(dto: CreateUserDto): Promise<User> {\n    const existing = await this.usersRepo.findByEmail(dto.email);\n    if (existing) {\n      throw new ConflictException('Email already registered');\n    }\n\n    const user = new User();\n    user.email = dto.email;\n    user.name = dto.name;\n    return this.usersRepo.save(user);\n  }\n}\n```\n\nReference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/db-avoid-n-plus-one.md",
    "content": "---\ntitle: Avoid N+1 Query Problems\nimpact: HIGH\nimpactDescription: N+1 queries are one of the most common performance killers\ntags: database, n-plus-one, queries, performance\n---\n\n## Avoid N+1 Query Problems\n\nN+1 queries occur when you fetch a list of entities, then make an additional query for each entity to load related data. Use eager loading with `relations`, query builder joins, or DataLoader to batch queries efficiently.\n\n**Incorrect (lazy loading in loops causes N+1):**\n\n```typescript\n// Lazy loading in loops causes N+1\n@Injectable()\nexport class OrdersService {\n  async getOrdersWithItems(userId: string): Promise<Order[]> {\n    const orders = await this.orderRepo.find({ where: { userId } });\n    // 1 query for orders\n\n    for (const order of orders) {\n      // N additional queries - one per order!\n      order.items = await this.itemRepo.find({ where: { orderId: order.id } });\n    }\n\n    return orders;\n  }\n}\n\n// Accessing lazy relations without loading\n@Controller('users')\nexport class UsersController {\n  @Get()\n  async findAll(): Promise<User[]> {\n    const users = await this.userRepo.find();\n    // If User.posts is lazy-loaded, serializing triggers N queries\n    return users; // Each user.posts access = 1 query\n  }\n}\n```\n\n**Correct (use relations for eager loading):**\n\n```typescript\n// Use relations option for eager loading\n@Injectable()\nexport class OrdersService {\n  async getOrdersWithItems(userId: string): Promise<Order[]> {\n    // Single query with JOIN\n    return this.orderRepo.find({\n      where: { userId },\n      relations: ['items', 'items.product'],\n    });\n  }\n}\n\n// Use QueryBuilder for complex joins\n@Injectable()\nexport class UsersService {\n  async getUsersWithPostCounts(): Promise<UserWithPostCount[]> {\n    return this.userRepo\n      .createQueryBuilder('user')\n      .leftJoin('user.posts', 'post')\n      .select('user.id', 'id')\n      .addSelect('user.name', 'name')\n      .addSelect('COUNT(post.id)', 'postCount')\n      .groupBy('user.id')\n      .getRawMany();\n  }\n\n  async getActiveUsersWithPosts(): Promise<User[]> {\n    return this.userRepo\n      .createQueryBuilder('user')\n      .leftJoinAndSelect('user.posts', 'post')\n      .leftJoinAndSelect('post.comments', 'comment')\n      .where('user.isActive = :active', { active: true })\n      .andWhere('post.status = :status', { status: 'published' })\n      .getMany();\n  }\n}\n\n// Use find options for specific fields\nasync getOrderSummaries(userId: string): Promise<OrderSummary[]> {\n  return this.orderRepo.find({\n    where: { userId },\n    relations: ['items'],\n    select: {\n      id: true,\n      total: true,\n      status: true,\n      items: {\n        id: true,\n        quantity: true,\n        price: true,\n      },\n    },\n  });\n}\n\n// Use DataLoader for GraphQL to batch and cache queries\nimport DataLoader from 'dataloader';\n\n@Injectable({ scope: Scope.REQUEST })\nexport class PostsLoader {\n  constructor(private postsService: PostsService) {}\n\n  readonly batchPosts = new DataLoader<string, Post[]>(async (userIds) => {\n    // Single query for all users' posts\n    const posts = await this.postsService.findByUserIds([...userIds]);\n\n    // Group by userId\n    const postsMap = new Map<string, Post[]>();\n    for (const post of posts) {\n      const userPosts = postsMap.get(post.userId) || [];\n      userPosts.push(post);\n      postsMap.set(post.userId, userPosts);\n    }\n\n    // Return in same order as input\n    return userIds.map((id) => postsMap.get(id) || []);\n  });\n}\n\n// In resolver\n@ResolveField()\nasync posts(@Parent() user: User): Promise<Post[]> {\n  // DataLoader batches multiple calls into single query\n  return this.postsLoader.batchPosts.load(user.id);\n}\n\n// Enable query logging in development to detect N+1\nTypeOrmModule.forRoot({\n  logging: ['query', 'error'],\n  logger: 'advanced-console',\n});\n```\n\nReference: [TypeORM Relations](https://typeorm.io/relations)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/db-use-migrations.md",
    "content": "---\ntitle: Use Database Migrations\nimpact: HIGH\nimpactDescription: Enables safe, repeatable database schema changes\ntags: database, migrations, typeorm, schema\n---\n\n## Use Database Migrations\n\nNever use `synchronize: true` in production. Use migrations for all schema changes. Migrations provide version control for your database, enable safe rollbacks, and ensure consistency across all environments.\n\n**Incorrect (using synchronize or manual SQL):**\n\n```typescript\n// Use synchronize in production\nTypeOrmModule.forRoot({\n  type: 'postgres',\n  synchronize: true, // DANGEROUS in production!\n  // Can drop columns, tables, or data\n});\n\n// Manual SQL in production\n@Injectable()\nexport class DatabaseService {\n  async addColumn(): Promise<void> {\n    await this.dataSource.query('ALTER TABLE users ADD COLUMN age INT');\n    // No version control, no rollback, inconsistent across envs\n  }\n}\n\n// Modify entities without migration\n@Entity()\nexport class User {\n  @Column()\n  email: string;\n\n  @Column() // Added without migration\n  newField: string; // Will crash in production if synchronize is false\n}\n```\n\n**Correct (use migrations for all schema changes):**\n\n```typescript\n// Configure TypeORM for migrations\n// data-source.ts\nexport const dataSource = new DataSource({\n  type: 'postgres',\n  host: process.env.DB_HOST,\n  port: parseInt(process.env.DB_PORT),\n  username: process.env.DB_USERNAME,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n  entities: ['dist/**/*.entity.js'],\n  migrations: ['dist/migrations/*.js'],\n  synchronize: false, // Always false in production\n  migrationsRun: true, // Run migrations on startup\n});\n\n// app.module.ts\nTypeOrmModule.forRootAsync({\n  inject: [ConfigService],\n  useFactory: (config: ConfigService) => ({\n    type: 'postgres',\n    host: config.get('DB_HOST'),\n    synchronize: config.get('NODE_ENV') === 'development', // Only in dev\n    migrations: ['dist/migrations/*.js'],\n    migrationsRun: true,\n  }),\n});\n\n// migrations/1705312800000-AddUserAge.ts\nimport { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AddUserAge1705312800000 implements MigrationInterface {\n  name = 'AddUserAge1705312800000';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    // Add column with default to handle existing rows\n    await queryRunner.query(`\n      ALTER TABLE \"users\" ADD \"age\" integer DEFAULT 0\n    `);\n\n    // Add index for frequently queried columns\n    await queryRunner.query(`\n      CREATE INDEX \"IDX_users_age\" ON \"users\" (\"age\")\n    `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    // Always implement down for rollback\n    await queryRunner.query(`DROP INDEX \"IDX_users_age\"`);\n    await queryRunner.query(`ALTER TABLE \"users\" DROP COLUMN \"age\"`);\n  }\n}\n\n// Safe column rename (two-step)\nexport class RenameNameToFullName1705312900000 implements MigrationInterface {\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    // Step 1: Add new column\n    await queryRunner.query(`\n      ALTER TABLE \"users\" ADD \"full_name\" varchar(255)\n    `);\n\n    // Step 2: Copy data\n    await queryRunner.query(`\n      UPDATE \"users\" SET \"full_name\" = \"name\"\n    `);\n\n    // Step 3: Add NOT NULL constraint\n    await queryRunner.query(`\n      ALTER TABLE \"users\" ALTER COLUMN \"full_name\" SET NOT NULL\n    `);\n\n    // Step 4: Drop old column (after verifying app works)\n    await queryRunner.query(`\n      ALTER TABLE \"users\" DROP COLUMN \"name\"\n    `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"users\" ADD \"name\" varchar(255)`);\n    await queryRunner.query(`UPDATE \"users\" SET \"name\" = \"full_name\"`);\n    await queryRunner.query(`ALTER TABLE \"users\" DROP COLUMN \"full_name\"`);\n  }\n}\n```\n\nReference: [TypeORM Migrations](https://typeorm.io/migrations)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/db-use-transactions.md",
    "content": "---\ntitle: Use Transactions for Multi-Step Operations\nimpact: HIGH\nimpactDescription: Ensures data consistency in multi-step operations\ntags: database, transactions, typeorm, consistency\n---\n\n## Use Transactions for Multi-Step Operations\n\nWhen multiple database operations must succeed or fail together, wrap them in a transaction. This prevents partial updates that leave your data in an inconsistent state. Use TypeORM's transaction APIs or the DataSource query runner for complex scenarios.\n\n**Incorrect (multiple saves without transaction):**\n\n```typescript\n// Multiple saves without transaction\n@Injectable()\nexport class OrdersService {\n  async createOrder(userId: string, items: OrderItem[]): Promise<Order> {\n    // If any step fails, data is inconsistent\n    const order = await this.orderRepo.save({ userId, status: 'pending' });\n\n    for (const item of items) {\n      await this.orderItemRepo.save({ orderId: order.id, ...item });\n      await this.inventoryRepo.decrement({ productId: item.productId }, 'stock', item.quantity);\n    }\n\n    await this.paymentService.charge(order.id);\n    // If payment fails, order and inventory are already modified!\n\n    return order;\n  }\n}\n```\n\n**Correct (use DataSource.transaction for automatic rollback):**\n\n```typescript\n// Use DataSource.transaction() for automatic rollback\n@Injectable()\nexport class OrdersService {\n  constructor(private dataSource: DataSource) {}\n\n  async createOrder(userId: string, items: OrderItem[]): Promise<Order> {\n    return this.dataSource.transaction(async manager => {\n      // All operations use the same transactional manager\n      const order = await manager.save(Order, { userId, status: 'pending' });\n\n      for (const item of items) {\n        await manager.save(OrderItem, { orderId: order.id, ...item });\n        await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);\n      }\n\n      // If this throws, everything rolls back\n      await this.paymentService.chargeWithManager(manager, order.id);\n\n      return order;\n    });\n  }\n}\n\n// QueryRunner for manual transaction control\n@Injectable()\nexport class TransferService {\n  constructor(private dataSource: DataSource) {}\n\n  async transfer(fromId: string, toId: string, amount: number): Promise<void> {\n    const queryRunner = this.dataSource.createQueryRunner();\n    await queryRunner.connect();\n    await queryRunner.startTransaction();\n\n    try {\n      // Debit source account\n      await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);\n\n      // Verify sufficient funds\n      const source = await queryRunner.manager.findOne(Account, {\n        where: { id: fromId },\n      });\n      if (source.balance < 0) {\n        throw new BadRequestException('Insufficient funds');\n      }\n\n      // Credit destination account\n      await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);\n\n      // Log the transaction\n      await queryRunner.manager.save(TransactionLog, {\n        fromId,\n        toId,\n        amount,\n        timestamp: new Date(),\n      });\n\n      await queryRunner.commitTransaction();\n    } catch (error) {\n      await queryRunner.rollbackTransaction();\n      throw error;\n    } finally {\n      await queryRunner.release();\n    }\n  }\n}\n\n// Repository method with transaction support\n@Injectable()\nexport class UsersRepository {\n  constructor(\n    @InjectRepository(User) private repo: Repository<User>,\n    private dataSource: DataSource,\n  ) {}\n\n  async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {\n    return this.dataSource.transaction(async manager => {\n      const user = await manager.save(User, userData);\n      await manager.save(Profile, { ...profileData, userId: user.id });\n      return user;\n    });\n  }\n}\n```\n\nReference: [TypeORM Transactions](https://typeorm.io/transactions)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/devops-graceful-shutdown.md",
    "content": "---\ntitle: Implement Graceful Shutdown\nimpact: MEDIUM-HIGH\nimpactDescription: Proper shutdown handling ensures zero-downtime deployments\ntags: devops, graceful-shutdown, lifecycle, kubernetes\n---\n\n## Implement Graceful Shutdown\n\nHandle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments.\n\n**Incorrect (ignoring shutdown signals):**\n\n```typescript\n// Ignore shutdown signals\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  await app.listen(3000);\n  // App crashes immediately on SIGTERM\n  // In-flight requests fail\n  // Database connections are abruptly closed\n}\n\n// Long-running tasks without cancellation\n@Injectable()\nexport class ProcessingService {\n  async processLargeFile(file: File): Promise<void> {\n    // No way to interrupt this during shutdown\n    for (let i = 0; i < file.chunks.length; i++) {\n      await this.processChunk(file.chunks[i]);\n      // May run for minutes, blocking shutdown\n    }\n  }\n}\n```\n\n**Correct (enable shutdown hooks and handle cleanup):**\n\n```typescript\n// Enable shutdown hooks in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  // Enable shutdown hooks\n  app.enableShutdownHooks();\n\n  // Optional: Add timeout for forced shutdown\n  const server = await app.listen(3000);\n  server.setTimeout(30000); // 30 second timeout\n\n  // Handle graceful shutdown\n  const signals = ['SIGTERM', 'SIGINT'];\n  signals.forEach(signal => {\n    process.on(signal, async () => {\n      console.log(`Received ${signal}, starting graceful shutdown...`);\n\n      // Stop accepting new connections\n      server.close(async () => {\n        console.log('HTTP server closed');\n        await app.close();\n        process.exit(0);\n      });\n\n      // Force exit after timeout\n      setTimeout(() => {\n        console.error('Forced shutdown after timeout');\n        process.exit(1);\n      }, 30000);\n    });\n  });\n}\n\n// Lifecycle hooks for cleanup\n@Injectable()\nexport class DatabaseService implements OnApplicationShutdown {\n  private readonly connections: Connection[] = [];\n\n  async onApplicationShutdown(signal?: string): Promise<void> {\n    console.log(`Database service shutting down on ${signal}`);\n\n    // Close all connections gracefully\n    await Promise.all(this.connections.map(conn => conn.close()));\n\n    console.log('All database connections closed');\n  }\n}\n\n// Queue processor with graceful shutdown\n@Injectable()\nexport class QueueService implements OnApplicationShutdown, OnModuleDestroy {\n  private isShuttingDown = false;\n\n  onModuleDestroy(): void {\n    this.isShuttingDown = true;\n  }\n\n  async onApplicationShutdown(): Promise<void> {\n    // Wait for current jobs to complete\n    await this.queue.close();\n  }\n\n  async processJob(job: Job): Promise<void> {\n    if (this.isShuttingDown) {\n      throw new Error('Service is shutting down');\n    }\n    await this.doWork(job);\n  }\n}\n\n// WebSocket gateway cleanup\n@WebSocketGateway()\nexport class EventsGateway implements OnApplicationShutdown {\n  @WebSocketServer()\n  server: Server;\n\n  async onApplicationShutdown(): Promise<void> {\n    // Notify all connected clients\n    this.server.emit('shutdown', { message: 'Server is shutting down' });\n\n    // Close all connections\n    this.server.disconnectSockets();\n  }\n}\n\n// Health check integration\n@Injectable()\nexport class ShutdownService {\n  private isShuttingDown = false;\n\n  startShutdown(): void {\n    this.isShuttingDown = true;\n  }\n\n  isShutdown(): boolean {\n    return this.isShuttingDown;\n  }\n}\n\n@Controller('health')\nexport class HealthController {\n  constructor(private shutdownService: ShutdownService) {}\n\n  @Get('ready')\n  @HealthCheck()\n  readiness(): Promise<HealthCheckResult> {\n    // Return 503 during shutdown - k8s stops sending traffic\n    if (this.shutdownService.isShutdown()) {\n      throw new ServiceUnavailableException('Shutting down');\n    }\n\n    return this.health.check([() => this.db.pingCheck('database')]);\n  }\n}\n\n// Integrate with shutdown\n@Injectable()\nexport class AppShutdownService implements OnApplicationShutdown {\n  constructor(private shutdownService: ShutdownService) {}\n\n  async onApplicationShutdown(): Promise<void> {\n    // Mark as unhealthy first\n    this.shutdownService.startShutdown();\n\n    // Wait for k8s to update endpoints\n    await this.sleep(5000);\n\n    // Then proceed with cleanup\n  }\n}\n\n// Request tracking for in-flight requests\n@Injectable()\nexport class RequestTracker implements NestMiddleware, OnApplicationShutdown {\n  private activeRequests = 0;\n  private isShuttingDown = false;\n  private shutdownPromise: Promise<void> | null = null;\n  private resolveShutdown: (() => void) | null = null;\n\n  use(req: Request, res: Response, next: NextFunction): void {\n    if (this.isShuttingDown) {\n      res.status(503).send('Service Unavailable');\n      return;\n    }\n\n    this.activeRequests++;\n\n    res.on('finish', () => {\n      this.activeRequests--;\n      if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) {\n        this.resolveShutdown();\n      }\n    });\n\n    next();\n  }\n\n  async onApplicationShutdown(): Promise<void> {\n    this.isShuttingDown = true;\n\n    if (this.activeRequests > 0) {\n      console.log(`Waiting for ${this.activeRequests} requests to complete`);\n      this.shutdownPromise = new Promise(resolve => {\n        this.resolveShutdown = resolve;\n      });\n\n      // Wait with timeout\n      await Promise.race([this.shutdownPromise, new Promise(resolve => setTimeout(resolve, 30000))]);\n    }\n\n    console.log('All requests completed');\n  }\n}\n```\n\nReference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/devops-use-config-module.md",
    "content": "---\ntitle: Use ConfigModule for Environment Configuration\nimpact: LOW-MEDIUM\nimpactDescription: Proper configuration prevents deployment failures\ntags: devops, configuration, environment, validation\n---\n\n## Use ConfigModule for Environment Configuration\n\nUse `@nestjs/config` for environment-based configuration. Validate configuration at startup to fail fast on misconfigurations. Use namespaced configuration for organization and type safety.\n\n**Incorrect (accessing process.env directly):**\n\n```typescript\n// Access process.env directly\n@Injectable()\nexport class DatabaseService {\n  constructor() {\n    // No validation, can fail at runtime\n    this.connection = new Pool({\n      host: process.env.DB_HOST,\n      port: parseInt(process.env.DB_PORT), // NaN if missing\n      password: process.env.DB_PASSWORD, // undefined if missing\n    });\n  }\n}\n\n// Scattered env access\n@Injectable()\nexport class EmailService {\n  sendEmail() {\n    // Different services access env differently\n    const apiKey = process.env.SENDGRID_API_KEY || 'default';\n    // Typos go unnoticed: process.env.SENDGRID_API_KY\n  }\n}\n```\n\n**Correct (use @nestjs/config with validation):**\n\n```typescript\n// Setup validated configuration\nimport { ConfigModule, ConfigService, registerAs } from '@nestjs/config';\nimport * as Joi from 'joi';\n\n// config/database.config.ts\nexport const databaseConfig = registerAs('database', () => ({\n  host: process.env.DB_HOST,\n  port: parseInt(process.env.DB_PORT, 10),\n  username: process.env.DB_USERNAME,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n}));\n\n// config/app.config.ts\nexport const appConfig = registerAs('app', () => ({\n  port: parseInt(process.env.PORT, 10) || 3000,\n  environment: process.env.NODE_ENV || 'development',\n  apiPrefix: process.env.API_PREFIX || 'api',\n}));\n\n// config/validation.schema.ts\nexport const validationSchema = Joi.object({\n  NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),\n  PORT: Joi.number().default(3000),\n  DB_HOST: Joi.string().required(),\n  DB_PORT: Joi.number().default(5432),\n  DB_USERNAME: Joi.string().required(),\n  DB_PASSWORD: Joi.string().required(),\n  DB_NAME: Joi.string().required(),\n  JWT_SECRET: Joi.string().min(32).required(),\n  REDIS_URL: Joi.string().uri().required(),\n});\n\n// app.module.ts\n@Module({\n  imports: [\n    ConfigModule.forRoot({\n      isGlobal: true, // Available everywhere without importing\n      load: [databaseConfig, appConfig],\n      validationSchema,\n      validationOptions: {\n        abortEarly: true, // Stop on first error\n        allowUnknown: true, // Allow other env vars\n      },\n    }),\n    TypeOrmModule.forRootAsync({\n      inject: [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        type: 'postgres',\n        host: config.get('database.host'),\n        port: config.get('database.port'),\n        username: config.get('database.username'),\n        password: config.get('database.password'),\n        database: config.get('database.database'),\n        autoLoadEntities: true,\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Type-safe configuration access\nexport interface AppConfig {\n  port: number;\n  environment: 'development' | 'production' | 'test';\n  apiPrefix: string;\n}\n\nexport interface DatabaseConfig {\n  host: string;\n  port: number;\n  username: string;\n  password: string;\n  database: string;\n}\n\n// Type-safe access\n@Injectable()\nexport class AppService {\n  constructor(private config: ConfigService) {}\n\n  getPort(): number {\n    // Type-safe with generic\n    return this.config.get<number>('app.port');\n  }\n\n  getDatabaseConfig(): DatabaseConfig {\n    return this.config.get<DatabaseConfig>('database');\n  }\n}\n\n// Inject namespaced config directly\n@Injectable()\nexport class DatabaseService {\n  constructor(\n    @Inject(databaseConfig.KEY)\n    private dbConfig: ConfigType<typeof databaseConfig>,\n  ) {\n    // Full type inference!\n    const host = this.dbConfig.host; // string\n    const port = this.dbConfig.port; // number\n  }\n}\n\n// Environment files support\nConfigModule.forRoot({\n  envFilePath: [`.env.${process.env.NODE_ENV}.local`, `.env.${process.env.NODE_ENV}`, '.env.local', '.env'],\n});\n\n// .env.development\n// DB_HOST=localhost\n// DB_PORT=5432\n\n// .env.production\n// DB_HOST=prod-db.example.com\n// DB_PORT=5432\n```\n\nReference: [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/devops-use-logging.md",
    "content": "---\ntitle: Use Structured Logging\nimpact: MEDIUM-HIGH\nimpactDescription: Structured logging enables effective debugging and monitoring\ntags: devops, logging, structured-logs, pino\n---\n\n## Use Structured Logging\n\nUse NestJS Logger with structured JSON output in production. Include contextual information (request ID, user ID, operation) to trace requests across services. Avoid console.log and implement proper log levels.\n\n**Incorrect (using console.log in production):**\n\n```typescript\n// Use console.log in production\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    console.log('Creating user:', dto);\n    // Not structured, no levels, lost in production logs\n\n    try {\n      const user = await this.repo.save(dto);\n      console.log('User created:', user.id);\n      return user;\n    } catch (error) {\n      console.log('Error:', error); // Using log for errors\n      throw error;\n    }\n  }\n}\n\n// Log sensitive data\nconsole.log('Login attempt:', { email, password }); // SECURITY RISK!\n\n// Inconsistent log format\nlogger.log('User ' + userId + ' created at ' + new Date());\n// Hard to parse, no structure\n```\n\n**Correct (use structured logging with context):**\n\n```typescript\n// Configure logger in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule, {\n    logger:\n      process.env.NODE_ENV === 'production' ? ['error', 'warn', 'log'] : ['error', 'warn', 'log', 'debug', 'verbose'],\n  });\n}\n\n// Use NestJS Logger with context\n@Injectable()\nexport class UsersService {\n  private readonly logger = new Logger(UsersService.name);\n\n  async createUser(dto: CreateUserDto): Promise<User> {\n    this.logger.log('Creating user', { email: dto.email });\n\n    try {\n      const user = await this.repo.save(dto);\n      this.logger.log('User created', { userId: user.id });\n      return user;\n    } catch (error) {\n      this.logger.error('Failed to create user', error.stack, {\n        email: dto.email,\n      });\n      throw error;\n    }\n  }\n}\n\n// Custom logger for JSON output\n@Injectable()\nexport class JsonLogger implements LoggerService {\n  log(message: string, context?: object): void {\n    console.log(\n      JSON.stringify({\n        level: 'info',\n        timestamp: new Date().toISOString(),\n        message,\n        ...context,\n      }),\n    );\n  }\n\n  error(message: string, trace?: string, context?: object): void {\n    console.error(\n      JSON.stringify({\n        level: 'error',\n        timestamp: new Date().toISOString(),\n        message,\n        trace,\n        ...context,\n      }),\n    );\n  }\n\n  warn(message: string, context?: object): void {\n    console.warn(\n      JSON.stringify({\n        level: 'warn',\n        timestamp: new Date().toISOString(),\n        message,\n        ...context,\n      }),\n    );\n  }\n\n  debug(message: string, context?: object): void {\n    console.debug(\n      JSON.stringify({\n        level: 'debug',\n        timestamp: new Date().toISOString(),\n        message,\n        ...context,\n      }),\n    );\n  }\n}\n\n// Request context logging with ClsModule\nimport { ClsModule, ClsService } from 'nestjs-cls';\n\n@Module({\n  imports: [\n    ClsModule.forRoot({\n      global: true,\n      middleware: {\n        mount: true,\n        generateId: true,\n      },\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Middleware to set request context\n@Injectable()\nexport class RequestContextMiddleware implements NestMiddleware {\n  constructor(private cls: ClsService) {}\n\n  use(req: Request, res: Response, next: NextFunction): void {\n    const requestId = req.headers['x-request-id'] || randomUUID();\n    this.cls.set('requestId', requestId);\n    this.cls.set('userId', req.user?.id);\n\n    res.setHeader('x-request-id', requestId);\n    next();\n  }\n}\n\n// Logger that includes request context\n@Injectable()\nexport class ContextLogger {\n  constructor(private cls: ClsService) {}\n\n  log(message: string, data?: object): void {\n    console.log(\n      JSON.stringify({\n        level: 'info',\n        timestamp: new Date().toISOString(),\n        requestId: this.cls.get('requestId'),\n        userId: this.cls.get('userId'),\n        message,\n        ...data,\n      }),\n    );\n  }\n\n  error(message: string, error: Error, data?: object): void {\n    console.error(\n      JSON.stringify({\n        level: 'error',\n        timestamp: new Date().toISOString(),\n        requestId: this.cls.get('requestId'),\n        userId: this.cls.get('userId'),\n        message,\n        error: error.message,\n        stack: error.stack,\n        ...data,\n      }),\n    );\n  }\n}\n\n// Pino integration for high-performance logging\nimport { LoggerModule } from 'nestjs-pino';\n\n@Module({\n  imports: [\n    LoggerModule.forRoot({\n      pinoHttp: {\n        level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',\n        transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,\n        redact: ['req.headers.authorization', 'req.body.password'],\n        serializers: {\n          req: req => ({\n            method: req.method,\n            url: req.url,\n            query: req.query,\n          }),\n          res: res => ({\n            statusCode: res.statusCode,\n          }),\n        },\n      },\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Usage with Pino\n@Injectable()\nexport class UsersService {\n  constructor(private logger: PinoLogger) {\n    this.logger.setContext(UsersService.name);\n  }\n\n  async findOne(id: string): Promise<User> {\n    this.logger.info({ userId: id }, 'Finding user');\n    // Pino uses first arg for data, second for message\n  }\n}\n```\n\nReference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/di-avoid-service-locator.md",
    "content": "---\ntitle: Avoid Service Locator Anti-Pattern\nimpact: HIGH\nimpactDescription: Hides dependencies and breaks testability\ntags: dependency-injection, anti-patterns, testing\n---\n\n## Avoid Service Locator Anti-Pattern\n\nAvoid using `ModuleRef.get()` or global containers to resolve dependencies at runtime. This hides dependencies, makes code harder to test, and breaks the benefits of dependency injection. Use constructor injection instead.\n\n**Incorrect (service locator anti-pattern):**\n\n```typescript\n// Use ModuleRef to get dependencies dynamically\n@Injectable()\nexport class OrdersService {\n  constructor(private moduleRef: ModuleRef) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    // Dependencies are hidden - not visible in constructor\n    const usersService = this.moduleRef.get(UsersService);\n    const inventoryService = this.moduleRef.get(InventoryService);\n    const paymentService = this.moduleRef.get(PaymentService);\n\n    const user = await usersService.findOne(dto.userId);\n    // ... rest of logic\n  }\n}\n\n// Global singleton container\nclass ServiceContainer {\n  private static instance: ServiceContainer;\n  private services = new Map<string, any>();\n\n  static getInstance(): ServiceContainer {\n    if (!this.instance) {\n      this.instance = new ServiceContainer();\n    }\n    return this.instance;\n  }\n\n  get<T>(key: string): T {\n    return this.services.get(key);\n  }\n}\n```\n\n**Correct (constructor injection with explicit dependencies):**\n\n```typescript\n// Use constructor injection - dependencies are explicit\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private usersService: UsersService,\n    private inventoryService: InventoryService,\n    private paymentService: PaymentService,\n  ) {}\n\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const user = await this.usersService.findOne(dto.userId);\n    const inventory = await this.inventoryService.check(dto.items);\n    // Dependencies are clear and testable\n  }\n}\n\n// Easy to test with mocks\ndescribe('OrdersService', () => {\n  let service: OrdersService;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        OrdersService,\n        { provide: UsersService, useValue: mockUsersService },\n        { provide: InventoryService, useValue: mockInventoryService },\n        { provide: PaymentService, useValue: mockPaymentService },\n      ],\n    }).compile();\n\n    service = module.get(OrdersService);\n  });\n});\n\n// VALID: Factory pattern for dynamic instantiation\n@Injectable()\nexport class HandlerFactory {\n  constructor(private moduleRef: ModuleRef) {}\n\n  getHandler(type: string): Handler {\n    switch (type) {\n      case 'email':\n        return this.moduleRef.get(EmailHandler);\n      case 'sms':\n        return this.moduleRef.get(SmsHandler);\n      default:\n        return this.moduleRef.get(DefaultHandler);\n    }\n  }\n}\n```\n\nReference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/di-interface-segregation.md",
    "content": "---\ntitle: Apply Interface Segregation Principle\nimpact: HIGH\nimpactDescription: Reduces coupling and improves testability by 30-50%\ntags: dependency-injection, interfaces, solid, isp\n---\n\n## Apply Interface Segregation Principle\n\nClients should not be forced to depend on interfaces they don't use. In NestJS, this means keeping interfaces small and focused on specific capabilities rather than creating \"fat\" interfaces that bundle unrelated methods. When a service only needs to send emails, it shouldn't depend on an interface that also includes SMS, push notifications, and logging. Split large interfaces into role-based ones.\n\n**Incorrect (fat interface forcing unused dependencies):**\n\n```typescript\n// Fat interface - forces all consumers to depend on everything\ninterface NotificationService {\n  sendEmail(to: string, subject: string, body: string): Promise<void>;\n  sendSms(phone: string, message: string): Promise<void>;\n  sendPush(userId: string, notification: PushPayload): Promise<void>;\n  sendSlack(channel: string, message: string): Promise<void>;\n  logNotification(type: string, payload: any): Promise<void>;\n  getDeliveryStatus(id: string): Promise<DeliveryStatus>;\n  retryFailed(id: string): Promise<void>;\n  scheduleNotification(dto: ScheduleDto): Promise<string>;\n}\n\n// Consumer only needs email, but must mock everything for tests\n@Injectable()\nexport class OrdersService {\n  constructor(\n    private notifications: NotificationService, // Depends on 8 methods, uses 1\n  ) {}\n\n  async confirmOrder(order: Order): Promise<void> {\n    await this.notifications.sendEmail(\n      order.customer.email,\n      'Order Confirmed',\n      `Your order ${order.id} has been confirmed.`,\n    );\n  }\n}\n\n// Testing is painful - must mock unused methods\nconst mockNotificationService = {\n  sendEmail: jest.fn(),\n  sendSms: jest.fn(), // Never used, but required\n  sendPush: jest.fn(), // Never used, but required\n  sendSlack: jest.fn(), // Never used, but required\n  logNotification: jest.fn(), // Never used, but required\n  getDeliveryStatus: jest.fn(), // Never used, but required\n  retryFailed: jest.fn(), // Never used, but required\n  scheduleNotification: jest.fn(), // Never used, but required\n};\n```\n\n**Correct (segregated interfaces by capability):**\n\n```typescript\n// Segregated interfaces - each focused on one capability\ninterface EmailSender {\n  sendEmail(to: string, subject: string, body: string): Promise<void>;\n}\n\ninterface SmsSender {\n  sendSms(phone: string, message: string): Promise<void>;\n}\n\ninterface PushSender {\n  sendPush(userId: string, notification: PushPayload): Promise<void>;\n}\n\ninterface NotificationLogger {\n  logNotification(type: string, payload: any): Promise<void>;\n}\n\ninterface NotificationScheduler {\n  scheduleNotification(dto: ScheduleDto): Promise<string>;\n}\n\n// Implementation can implement multiple interfaces\n@Injectable()\nexport class NotificationService implements EmailSender, SmsSender, PushSender {\n  async sendEmail(to: string, subject: string, body: string): Promise<void> {\n    // Email implementation\n  }\n\n  async sendSms(phone: string, message: string): Promise<void> {\n    // SMS implementation\n  }\n\n  async sendPush(userId: string, notification: PushPayload): Promise<void> {\n    // Push implementation\n  }\n}\n\n// Or separate implementations\n@Injectable()\nexport class SendGridEmailService implements EmailSender {\n  async sendEmail(to: string, subject: string, body: string): Promise<void> {\n    // SendGrid-specific implementation\n  }\n}\n\n// Consumer depends only on what it needs\n@Injectable()\nexport class OrdersService {\n  constructor(\n    @Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency\n  ) {}\n\n  async confirmOrder(order: Order): Promise<void> {\n    await this.emailSender.sendEmail(\n      order.customer.email,\n      'Order Confirmed',\n      `Your order ${order.id} has been confirmed.`,\n    );\n  }\n}\n\n// Testing is simple - only mock what's used\nconst mockEmailSender: EmailSender = {\n  sendEmail: jest.fn(),\n};\n\n// Module registration with tokens\nexport const EMAIL_SENDER = Symbol('EMAIL_SENDER');\nexport const SMS_SENDER = Symbol('SMS_SENDER');\n\n@Module({\n  providers: [\n    { provide: EMAIL_SENDER, useClass: SendGridEmailService },\n    { provide: SMS_SENDER, useClass: TwilioSmsService },\n  ],\n  exports: [EMAIL_SENDER, SMS_SENDER],\n})\nexport class NotificationModule {}\n```\n\n**Combining interfaces when needed:**\n\n```typescript\n// Sometimes a consumer legitimately needs multiple capabilities\ninterface EmailAndSmsSender extends EmailSender, SmsSender {}\n\n// Or use intersection types\ntype MultiChannelSender = EmailSender & SmsSender & PushSender;\n\n// Consumer that genuinely needs multiple channels\n@Injectable()\nexport class AlertService {\n  constructor(\n    @Inject(MULTI_CHANNEL_SENDER)\n    private sender: EmailSender & SmsSender,\n  ) {}\n\n  async sendCriticalAlert(user: User, message: string): Promise<void> {\n    await Promise.all([\n      this.sender.sendEmail(user.email, 'Critical Alert', message),\n      this.sender.sendSms(user.phone, message),\n    ]);\n  }\n}\n```\n\nReference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/di-liskov-substitution.md",
    "content": "---\ntitle: Honor Liskov Substitution Principle\nimpact: HIGH\nimpactDescription: Ensures implementations are truly interchangeable without breaking callers\ntags: dependency-injection, inheritance, solid, lsp\n---\n\n## Honor Liskov Substitution Principle\n\nSubtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations.\n\n**Incorrect (implementation violates the contract):**\n\n```typescript\n// Base interface with clear contract\ninterface PaymentGateway {\n  /**\n   * Charges the specified amount.\n   * @returns PaymentResult on success\n   * @throws PaymentFailedException on payment failure\n   */\n  charge(amount: number, currency: string): Promise<PaymentResult>;\n}\n\n// Production implementation - follows the contract\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    const response = await this.stripe.charges.create({ amount, currency });\n    return { success: true, transactionId: response.id, amount };\n  }\n}\n\n// Mock that violates LSP - different behavior!\n@Injectable()\nexport class MockPaymentService implements PaymentGateway {\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    // VIOLATION 1: Throws for valid input (contract says return PaymentResult)\n    if (amount > 1000) {\n      throw new Error('Mock does not support large amounts');\n    }\n\n    // VIOLATION 2: Returns null instead of PaymentResult\n    if (currency !== 'USD') {\n      return null as any; // Real service would convert or reject properly\n    }\n\n    // VIOLATION 3: Missing required field\n    return { success: true } as PaymentResult; // Missing transactionId!\n  }\n}\n\n// Consumer trusts the contract\n@Injectable()\nexport class OrdersService {\n  constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}\n\n  async checkout(order: Order): Promise<void> {\n    const result = await this.payment.charge(order.total, order.currency);\n    // These fail with MockPaymentService:\n    await this.saveTransaction(result.transactionId); // undefined!\n    await this.sendReceipt(result); // might be null!\n  }\n}\n```\n\n**Correct (implementations honor the contract):**\n\n```typescript\n// Well-defined interface with documented behavior\ninterface PaymentGateway {\n  /**\n   * Charges the specified amount.\n   * @param amount - Amount in smallest currency unit (cents)\n   * @param currency - ISO 4217 currency code\n   * @returns PaymentResult with transactionId, success status, and amount\n   * @throws PaymentFailedException if charge is declined\n   * @throws InvalidCurrencyException if currency is not supported\n   */\n  charge(amount: number, currency: string): Promise<PaymentResult>;\n\n  /**\n   * Refunds a previous charge.\n   * @throws TransactionNotFoundException if transactionId is invalid\n   */\n  refund(transactionId: string, amount?: number): Promise<RefundResult>;\n}\n\n// Production implementation\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    try {\n      const response = await this.stripe.charges.create({ amount, currency });\n      return {\n        success: true,\n        transactionId: response.id,\n        amount: response.amount,\n      };\n    } catch (error) {\n      if (error.type === 'card_error') {\n        throw new PaymentFailedException(error.message);\n      }\n      throw error;\n    }\n  }\n\n  async refund(transactionId: string, amount?: number): Promise<RefundResult> {\n    // Implementation...\n  }\n}\n\n// Mock that honors LSP - same contract, same behavior shape\n@Injectable()\nexport class MockPaymentService implements PaymentGateway {\n  private transactions = new Map<string, PaymentResult>();\n\n  async charge(amount: number, currency: string): Promise<PaymentResult> {\n    // Honor the contract: validate currency like real service would\n    if (!['USD', 'EUR', 'GBP'].includes(currency)) {\n      throw new InvalidCurrencyException(`Unsupported currency: ${currency}`);\n    }\n\n    // Simulate decline for specific test scenarios\n    if (amount === 99999) {\n      throw new PaymentFailedException('Card declined (test scenario)');\n    }\n\n    // Return same shape as production\n    const result: PaymentResult = {\n      success: true,\n      transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`,\n      amount,\n    };\n\n    this.transactions.set(result.transactionId, result);\n    return result;\n  }\n\n  async refund(transactionId: string, amount?: number): Promise<RefundResult> {\n    // Honor the contract: throw if transaction not found\n    if (!this.transactions.has(transactionId)) {\n      throw new TransactionNotFoundException(transactionId);\n    }\n\n    return {\n      success: true,\n      refundId: `refund_${transactionId}`,\n      amount: amount ?? this.transactions.get(transactionId)!.amount,\n    };\n  }\n}\n\n// Consumer can swap implementations safely\n@Injectable()\nexport class OrdersService {\n  constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}\n\n  async checkout(order: Order): Promise<Order> {\n    try {\n      const result = await this.payment.charge(order.total, order.currency);\n      // Works with both StripeService and MockPaymentService\n      order.transactionId = result.transactionId;\n      order.status = 'paid';\n      return order;\n    } catch (error) {\n      if (error instanceof PaymentFailedException) {\n        order.status = 'payment_failed';\n        return order;\n      }\n      throw error;\n    }\n  }\n}\n```\n\n**Testing LSP compliance:**\n\n```typescript\n// Shared test suite that any implementation must pass\nfunction testPaymentGatewayContract(createGateway: () => PaymentGateway) {\n  describe('PaymentGateway contract', () => {\n    let gateway: PaymentGateway;\n\n    beforeEach(() => {\n      gateway = createGateway();\n    });\n\n    it('returns PaymentResult with all required fields', async () => {\n      const result = await gateway.charge(1000, 'USD');\n      expect(result).toHaveProperty('success');\n      expect(result).toHaveProperty('transactionId');\n      expect(result).toHaveProperty('amount');\n      expect(typeof result.transactionId).toBe('string');\n    });\n\n    it('throws InvalidCurrencyException for unsupported currency', async () => {\n      await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);\n    });\n\n    it('throws TransactionNotFoundException for invalid refund', async () => {\n      await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);\n    });\n  });\n}\n\n// Run against all implementations\ndescribe('StripeService', () => {\n  testPaymentGatewayContract(() => new StripeService(mockStripeClient));\n});\n\ndescribe('MockPaymentService', () => {\n  testPaymentGatewayContract(() => new MockPaymentService());\n});\n```\n\nReference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/di-prefer-constructor-injection.md",
    "content": "---\ntitle: Prefer Constructor Injection\nimpact: CRITICAL\nimpactDescription: Required for proper DI and testing\ntags: dependency-injection, constructor, testing\n---\n\n## Prefer Constructor Injection\n\nAlways use constructor injection over property injection. Constructor injection makes dependencies explicit, enables TypeScript type checking, ensures dependencies are available when the class is instantiated, and improves testability. This is required for proper DI, testing, and TypeScript support.\n\n**Incorrect (property injection with hidden dependencies):**\n\n```typescript\n// Property injection - avoid unless necessary\n@Injectable()\nexport class UsersService {\n  @Inject()\n  private userRepo: UserRepository; // Hidden dependency\n\n  @Inject('CONFIG')\n  private config: ConfigType; // Also hidden\n\n  async findAll() {\n    return this.userRepo.find();\n  }\n}\n\n// Problems:\n// 1. Dependencies not visible in constructor\n// 2. Service can be instantiated without dependencies in tests\n// 3. TypeScript can't enforce dependency types at instantiation\n```\n\n**Correct (constructor injection with explicit dependencies):**\n\n```typescript\n// Constructor injection - explicit and testable\n@Injectable()\nexport class UsersService {\n  constructor(\n    private readonly userRepo: UserRepository,\n    @Inject('CONFIG') private readonly config: ConfigType,\n  ) {}\n\n  async findAll(): Promise<User[]> {\n    return this.userRepo.find();\n  }\n}\n\n// Testing is straightforward\ndescribe('UsersService', () => {\n  let service: UsersService;\n  let mockRepo: jest.Mocked<UserRepository>;\n\n  beforeEach(() => {\n    mockRepo = {\n      find: jest.fn(),\n      save: jest.fn(),\n    } as any;\n\n    service = new UsersService(mockRepo, { dbUrl: 'test' });\n  });\n\n  it('should find all users', async () => {\n    mockRepo.find.mockResolvedValue([{ id: '1', name: 'Test' }]);\n    const result = await service.findAll();\n    expect(result).toHaveLength(1);\n  });\n});\n\n// Only use property injection for optional dependencies\n@Injectable()\nexport class LoggingService {\n  @Optional()\n  @Inject('ANALYTICS')\n  private analytics?: AnalyticsService;\n\n  log(message: string) {\n    console.log(message);\n    this.analytics?.track('log', message); // Optional enhancement\n  }\n}\n```\n\nReference: [NestJS Providers](https://docs.nestjs.com/providers)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/di-scope-awareness.md",
    "content": "---\ntitle: Understand Provider Scopes\nimpact: CRITICAL\nimpactDescription: Prevents data leaks and performance issues\ntags: dependency-injection, scopes, request-context\n---\n\n## Understand Provider Scopes\n\nNestJS has three provider scopes: DEFAULT (singleton), REQUEST (per-request instance), and TRANSIENT (new instance for each injection). Most providers should be singletons. Request-scoped providers have performance implications as they bubble up through the dependency tree. Understanding scopes prevents memory leaks and incorrect data sharing.\n\n**Incorrect (wrong scope usage):**\n\n```typescript\n// Request-scoped when not needed (performance hit)\n@Injectable({ scope: Scope.REQUEST })\nexport class UsersService {\n  // This creates a new instance for EVERY request\n  // All dependencies also become request-scoped\n  async findAll() {\n    return this.userRepo.find();\n  }\n}\n\n// Singleton with mutable request state\n@Injectable() // Default: singleton\nexport class RequestContextService {\n  private userId: string; // DANGER: Shared across all requests!\n\n  setUser(userId: string) {\n    this.userId = userId; // Overwrites for all concurrent requests\n  }\n\n  getUser() {\n    return this.userId; // Returns wrong user!\n  }\n}\n```\n\n**Correct (appropriate scope for each use case):**\n\n```typescript\n// Singleton for stateless services (default, most common)\n@Injectable()\nexport class UsersService {\n  constructor(private readonly userRepo: UserRepository) {}\n\n  async findById(id: string): Promise<User> {\n    return this.userRepo.findOne({ where: { id } });\n  }\n}\n\n// Request-scoped ONLY when you need request context\n@Injectable({ scope: Scope.REQUEST })\nexport class RequestContextService {\n  private userId: string;\n\n  setUser(userId: string) {\n    this.userId = userId;\n  }\n\n  getUser(): string {\n    return this.userId;\n  }\n}\n\n// Better: Use NestJS built-in request context\nimport { REQUEST } from '@nestjs/core';\nimport { Request } from 'express';\n\n@Injectable({ scope: Scope.REQUEST })\nexport class AuditService {\n  constructor(@Inject(REQUEST) private request: Request) {}\n\n  log(action: string) {\n    console.log(`User ${this.request.user?.id} performed ${action}`);\n  }\n}\n\n// Best: Use ClsModule for async context (no scope bubble-up)\nimport { ClsService } from 'nestjs-cls';\n\n@Injectable() // Stays singleton!\nexport class AuditService {\n  constructor(private cls: ClsService) {}\n\n  log(action: string) {\n    const userId = this.cls.get('userId');\n    console.log(`User ${userId} performed ${action}`);\n  }\n}\n```\n\nReference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/di-use-interfaces-tokens.md",
    "content": "---\ntitle: Use Injection Tokens for Interfaces\nimpact: HIGH\nimpactDescription: Enables interface-based DI at runtime\ntags: dependency-injection, tokens, interfaces\n---\n\n## Use Injection Tokens for Interfaces\n\nTypeScript interfaces are erased at compile time and can't be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces. This enables swapping implementations for testing or different environments.\n\n**Incorrect (interface can't be used as token):**\n\n```typescript\n// Interface can't be used as injection token\ninterface PaymentGateway {\n  charge(amount: number): Promise<PaymentResult>;\n}\n\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  charge(amount: number) {\n    /* ... */\n  }\n}\n\n@Injectable()\nexport class OrdersService {\n  // This WON'T work - PaymentGateway doesn't exist at runtime\n  constructor(private payment: PaymentGateway) {}\n}\n```\n\n**Correct (symbol tokens or abstract classes):**\n\n```typescript\n// Option 1: String/Symbol tokens (most flexible)\nexport const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY');\n\nexport interface PaymentGateway {\n  charge(amount: number): Promise<PaymentResult>;\n}\n\n@Injectable()\nexport class StripeService implements PaymentGateway {\n  async charge(amount: number): Promise<PaymentResult> {\n    // Stripe implementation\n  }\n}\n\n@Injectable()\nexport class MockPaymentService implements PaymentGateway {\n  async charge(amount: number): Promise<PaymentResult> {\n    return { success: true, id: 'mock-id' };\n  }\n}\n\n// Module registration\n@Module({\n  providers: [\n    {\n      provide: PAYMENT_GATEWAY,\n      useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,\n    },\n  ],\n  exports: [PAYMENT_GATEWAY],\n})\nexport class PaymentModule {}\n\n// Injection\n@Injectable()\nexport class OrdersService {\n  constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}\n\n  async createOrder(dto: CreateOrderDto) {\n    await this.payment.charge(dto.amount);\n  }\n}\n\n// Option 2: Abstract class (carries runtime type info)\nexport abstract class PaymentGateway {\n  abstract charge(amount: number): Promise<PaymentResult>;\n}\n\n@Injectable()\nexport class StripeService extends PaymentGateway {\n  async charge(amount: number): Promise<PaymentResult> {\n    // Implementation\n  }\n}\n\n// No @Inject needed with abstract class\n@Injectable()\nexport class OrdersService {\n  constructor(private payment: PaymentGateway) {}\n}\n```\n\nReference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/error-handle-async-errors.md",
    "content": "---\ntitle: Handle Async Errors Properly\nimpact: HIGH\nimpactDescription: Prevents process crashes from unhandled rejections\ntags: error-handling, async, promises\n---\n\n## Handle Async Errors Properly\n\nNestJS automatically catches errors from async route handlers, but errors from background tasks, event handlers, and manually created promises can crash your application. Always handle async errors explicitly and use global handlers as a safety net.\n\n**Incorrect (fire-and-forget without error handling):**\n\n```typescript\n// Fire-and-forget without error handling\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Fire and forget - if this fails, error is unhandled!\n    this.emailService.sendWelcome(user.email);\n\n    return user;\n  }\n}\n\n// Unhandled promise in event handler\n@Injectable()\nexport class OrdersService {\n  @OnEvent('order.created')\n  handleOrderCreated(event: OrderCreatedEvent) {\n    // This returns a promise but it's not awaited!\n    this.processOrder(event);\n    // Errors will crash the process\n  }\n\n  private async processOrder(event: OrderCreatedEvent): Promise<void> {\n    await this.inventoryService.reserve(event.items);\n    await this.notificationService.send(event.userId);\n  }\n}\n\n// Missing try-catch in scheduled tasks\n@Cron('0 0 * * *')\nasync dailyCleanup(): Promise<void> {\n  await this.cleanupService.run();\n  // If this throws, no error handling\n}\n```\n\n**Correct (explicit async error handling):**\n\n```typescript\n// Handle fire-and-forget with explicit catch\n@Injectable()\nexport class UsersService {\n  private readonly logger = new Logger(UsersService.name);\n\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Explicitly catch and log errors\n    this.emailService.sendWelcome(user.email).catch(error => {\n      this.logger.error('Failed to send welcome email', error.stack);\n      // Optionally queue for retry\n    });\n\n    return user;\n  }\n}\n\n// Properly handle async event handlers\n@Injectable()\nexport class OrdersService {\n  private readonly logger = new Logger(OrdersService.name);\n\n  @OnEvent('order.created')\n  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {\n    try {\n      await this.processOrder(event);\n    } catch (error) {\n      this.logger.error('Failed to process order', { event, error });\n      // Don't rethrow - would crash the process\n      await this.deadLetterQueue.add('order.created', event);\n    }\n  }\n}\n\n// Safe scheduled tasks\n@Injectable()\nexport class CleanupService {\n  private readonly logger = new Logger(CleanupService.name);\n\n  @Cron('0 0 * * *')\n  async dailyCleanup(): Promise<void> {\n    try {\n      await this.cleanupService.run();\n      this.logger.log('Daily cleanup completed');\n    } catch (error) {\n      this.logger.error('Daily cleanup failed', error.stack);\n      // Alert or retry logic\n    }\n  }\n}\n\n// Global unhandled rejection handler in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  const logger = new Logger('Bootstrap');\n\n  process.on('unhandledRejection', (reason, promise) => {\n    logger.error('Unhandled Rejection at:', promise, 'reason:', reason);\n  });\n\n  process.on('uncaughtException', error => {\n    logger.error('Uncaught Exception:', error);\n    process.exit(1);\n  });\n\n  await app.listen(3000);\n}\n```\n\nReference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/error-throw-http-exceptions.md",
    "content": "---\ntitle: Throw HTTP Exceptions from Services\nimpact: HIGH\nimpactDescription: Keeps controllers thin and simplifies error handling\ntags: error-handling, exceptions, services\n---\n\n## Throw HTTP Exceptions from Services\n\nIt's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes.\n\n**Incorrect (return error objects instead of throwing):**\n\n```typescript\n// Return error objects instead of throwing\n@Injectable()\nexport class UsersService {\n  async findById(id: string): Promise<{ user?: User; error?: string }> {\n    const user = await this.repo.findOne({ where: { id } });\n    if (!user) {\n      return { error: 'User not found' }; // Controller must check this\n    }\n    return { user };\n  }\n}\n\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string) {\n    const result = await this.usersService.findById(id);\n    if (result.error) {\n      throw new NotFoundException(result.error);\n    }\n    return result.user;\n  }\n}\n```\n\n**Correct (throw exceptions directly from service):**\n\n```typescript\n// Throw exceptions directly from service\n@Injectable()\nexport class UsersService {\n  constructor(private readonly repo: UserRepository) {}\n\n  async findById(id: string): Promise<User> {\n    const user = await this.repo.findOne({ where: { id } });\n    if (!user) {\n      throw new NotFoundException(`User #${id} not found`);\n    }\n    return user;\n  }\n\n  async create(dto: CreateUserDto): Promise<User> {\n    const existing = await this.repo.findOne({\n      where: { email: dto.email },\n    });\n    if (existing) {\n      throw new ConflictException('Email already registered');\n    }\n    return this.repo.save(dto);\n  }\n\n  async update(id: string, dto: UpdateUserDto): Promise<User> {\n    const user = await this.findById(id); // Throws if not found\n    Object.assign(user, dto);\n    return this.repo.save(user);\n  }\n}\n\n// Controller stays thin\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  findOne(@Param('id') id: string): Promise<User> {\n    return this.usersService.findById(id);\n  }\n\n  @Post()\n  create(@Body() dto: CreateUserDto): Promise<User> {\n    return this.usersService.create(dto);\n  }\n}\n\n// For layer-agnostic services, use domain exceptions\nexport class EntityNotFoundException extends Error {\n  constructor(\n    public readonly entity: string,\n    public readonly id: string,\n  ) {\n    super(`${entity} with ID \"${id}\" not found`);\n  }\n}\n\n// Map to HTTP in exception filter\n@Catch(EntityNotFoundException)\nexport class EntityNotFoundFilter implements ExceptionFilter {\n  catch(exception: EntityNotFoundException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse<Response>();\n\n    response.status(404).json({\n      statusCode: 404,\n      message: exception.message,\n      entity: exception.entity,\n      id: exception.id,\n    });\n  }\n}\n```\n\nReference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/error-use-exception-filters.md",
    "content": "---\ntitle: Use Exception Filters for Error Handling\nimpact: HIGH\nimpactDescription: Consistent, centralized error handling\ntags: error-handling, exception-filters, consistency\n---\n\n## Use Exception Filters for Error Handling\n\nNever catch exceptions and manually format error responses in controllers. Use NestJS exception filters to handle errors consistently across your application. Create custom exception filters for specific error types and a global filter for unhandled exceptions.\n\n**Incorrect (manual error handling in controllers):**\n\n```typescript\n// Manual error handling in controllers\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string, @Res() res: Response) {\n    try {\n      const user = await this.usersService.findById(id);\n      if (!user) {\n        return res.status(404).json({\n          statusCode: 404,\n          message: 'User not found',\n        });\n      }\n      return res.json(user);\n    } catch (error) {\n      console.error(error);\n      return res.status(500).json({\n        statusCode: 500,\n        message: 'Internal server error',\n      });\n    }\n  }\n}\n```\n\n**Correct (exception filters with consistent handling):**\n\n```typescript\n// Use built-in and custom exceptions\n@Controller('users')\nexport class UsersController {\n  @Get(':id')\n  async findOne(@Param('id') id: string): Promise<User> {\n    const user = await this.usersService.findById(id);\n    if (!user) {\n      throw new NotFoundException(`User #${id} not found`);\n    }\n    return user;\n  }\n}\n\n// Custom domain exception\nexport class UserNotFoundException extends NotFoundException {\n  constructor(userId: string) {\n    super({\n      statusCode: 404,\n      error: 'Not Found',\n      message: `User with ID \"${userId}\" not found`,\n      code: 'USER_NOT_FOUND',\n    });\n  }\n}\n\n// Custom exception filter for domain errors\n@Catch(DomainException)\nexport class DomainExceptionFilter implements ExceptionFilter {\n  catch(exception: DomainException, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse<Response>();\n    const request = ctx.getRequest<Request>();\n\n    const status = exception.getStatus?.() || 400;\n\n    response.status(status).json({\n      statusCode: status,\n      code: exception.code,\n      message: exception.message,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n\n// Global exception filter for unhandled errors\n@Catch()\nexport class AllExceptionsFilter implements ExceptionFilter {\n  constructor(private readonly logger: Logger) {}\n\n  catch(exception: unknown, host: ArgumentsHost) {\n    const ctx = host.switchToHttp();\n    const response = ctx.getResponse<Response>();\n    const request = ctx.getRequest<Request>();\n\n    const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;\n\n    const message = exception instanceof HttpException ? exception.message : 'Internal server error';\n\n    this.logger.error(`${request.method} ${request.url}`, exception instanceof Error ? exception.stack : exception);\n\n    response.status(status).json({\n      statusCode: status,\n      message,\n      timestamp: new Date().toISOString(),\n      path: request.url,\n    });\n  }\n}\n\n// Register globally in main.ts\napp.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());\n\n// Or via module\n@Module({\n  providers: [\n    {\n      provide: APP_FILTER,\n      useClass: AllExceptionsFilter,\n    },\n  ],\n})\nexport class AppModule {}\n```\n\nReference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/micro-use-health-checks.md",
    "content": "---\ntitle: Implement Health Checks for Microservices\nimpact: MEDIUM-HIGH\nimpactDescription: Health checks enable orchestrators to manage service lifecycle\ntags: microservices, health-checks, terminus, kubernetes\n---\n\n## Implement Health Checks for Microservices\n\nImplement liveness and readiness probes using `@nestjs/terminus`. Liveness checks determine if the service should be restarted. Readiness checks determine if the service can accept traffic. Proper health checks enable Kubernetes and load balancers to route traffic correctly.\n\n**Incorrect (simple ping that doesn't check dependencies):**\n\n```typescript\n// Simple ping that doesn't check dependencies\n@Controller('health')\nexport class HealthController {\n  @Get()\n  check(): string {\n    return 'OK'; // Service might be unhealthy but returns OK\n  }\n}\n\n// Health check that blocks on slow dependencies\n@Controller('health')\nexport class HealthController {\n  @Get()\n  async check(): Promise<string> {\n    // If database is slow, health check times out\n    await this.userRepo.findOne({ where: { id: '1' } });\n    await this.redis.ping();\n    await this.externalApi.healthCheck();\n    return 'OK';\n  }\n}\n```\n\n**Correct (use @nestjs/terminus for comprehensive health checks):**\n\n```typescript\n// Use @nestjs/terminus for comprehensive health checks\nimport {\n  HealthCheckService,\n  HttpHealthIndicator,\n  TypeOrmHealthIndicator,\n  HealthCheck,\n  DiskHealthIndicator,\n  MemoryHealthIndicator,\n} from '@nestjs/terminus';\n\n@Controller('health')\nexport class HealthController {\n  constructor(\n    private health: HealthCheckService,\n    private http: HttpHealthIndicator,\n    private db: TypeOrmHealthIndicator,\n    private disk: DiskHealthIndicator,\n    private memory: MemoryHealthIndicator,\n  ) {}\n\n  // Liveness probe - is the service alive?\n  @Get('live')\n  @HealthCheck()\n  liveness() {\n    return this.health.check([\n      // Basic checks only\n      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB\n    ]);\n  }\n\n  // Readiness probe - can the service handle traffic?\n  @Get('ready')\n  @HealthCheck()\n  readiness() {\n    return this.health.check([\n      () => this.db.pingCheck('database'),\n      () =>\n        this.http.pingCheck('redis', 'http://redis:6379', { timeout: 1000 }),\n      () =>\n        this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),\n    ]);\n  }\n\n  // Deep health check for debugging\n  @Get('deep')\n  @HealthCheck()\n  deepCheck() {\n    return this.health.check([\n      () => this.db.pingCheck('database'),\n      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),\n      () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),\n      () =>\n        this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),\n      () =>\n        this.http.pingCheck('external-api', 'https://api.example.com/health'),\n    ]);\n  }\n}\n\n// Custom indicator for business-specific health\n@Injectable()\nexport class QueueHealthIndicator extends HealthIndicator {\n  constructor(private queueService: QueueService) {\n    super();\n  }\n\n  async isHealthy(key: string): Promise<HealthIndicatorResult> {\n    const queueStats = await this.queueService.getStats();\n\n    const isHealthy = queueStats.failedCount < 100;\n    const result = this.getStatus(key, isHealthy, {\n      waiting: queueStats.waitingCount,\n      active: queueStats.activeCount,\n      failed: queueStats.failedCount,\n    });\n\n    if (!isHealthy) {\n      throw new HealthCheckError('Queue unhealthy', result);\n    }\n\n    return result;\n  }\n}\n\n// Redis health indicator\n@Injectable()\nexport class RedisHealthIndicator extends HealthIndicator {\n  constructor(@InjectRedis() private redis: Redis) {\n    super();\n  }\n\n  async isHealthy(key: string): Promise<HealthIndicatorResult> {\n    try {\n      const pong = await this.redis.ping();\n      return this.getStatus(key, pong === 'PONG');\n    } catch (error) {\n      throw new HealthCheckError('Redis check failed', this.getStatus(key, false));\n    }\n  }\n}\n\n// Use custom indicators\n@Get('ready')\n@HealthCheck()\nreadiness() {\n  return this.health.check([\n    () => this.db.pingCheck('database'),\n    () => this.redis.isHealthy('redis'),\n    () => this.queue.isHealthy('job-queue'),\n  ]);\n}\n\n// Graceful shutdown handling\n@Injectable()\nexport class GracefulShutdownService implements OnApplicationShutdown {\n  private isShuttingDown = false;\n\n  isShutdown(): boolean {\n    return this.isShuttingDown;\n  }\n\n  async onApplicationShutdown(signal: string): Promise<void> {\n    this.isShuttingDown = true;\n    console.log(`Shutting down on ${signal}`);\n\n    // Wait for in-flight requests\n    await new Promise((resolve) => setTimeout(resolve, 5000));\n  }\n}\n\n// Health check respects shutdown state\n@Get('ready')\n@HealthCheck()\nreadiness() {\n  if (this.shutdownService.isShutdown()) {\n    throw new ServiceUnavailableException('Shutting down');\n  }\n\n  return this.health.check([\n    () => this.db.pingCheck('database'),\n  ]);\n}\n```\n\n### Kubernetes Configuration\n\n```yaml\n# Kubernetes deployment with probes\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: api-service\nspec:\n  template:\n    spec:\n      containers:\n        - name: api\n          image: api-service:latest\n          ports:\n            - containerPort: 3000\n          livenessProbe:\n            httpGet:\n              path: /health/live\n              port: 3000\n            initialDelaySeconds: 30\n            periodSeconds: 10\n            timeoutSeconds: 5\n            failureThreshold: 3\n          readinessProbe:\n            httpGet:\n              path: /health/ready\n              port: 3000\n            initialDelaySeconds: 5\n            periodSeconds: 5\n            timeoutSeconds: 3\n            failureThreshold: 3\n          startupProbe:\n            httpGet:\n              path: /health/live\n              port: 3000\n            initialDelaySeconds: 0\n            periodSeconds: 5\n            failureThreshold: 30\n```\n\nReference: [NestJS Terminus](https://docs.nestjs.com/recipes/terminus)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/micro-use-patterns.md",
    "content": "---\ntitle: Use Message and Event Patterns Correctly\nimpact: MEDIUM\nimpactDescription: Proper patterns ensure reliable microservice communication\ntags: microservices, message-pattern, event-pattern, communication\n---\n\n## Use Message and Event Patterns Correctly\n\nNestJS microservices support two communication patterns: request-response (MessagePattern) and event-based (EventPattern). Use MessagePattern when you need a response, and EventPattern for fire-and-forget notifications. Understanding the difference prevents communication bugs.\n\n**Incorrect (using wrong pattern for use case):**\n\n```typescript\n// Use @MessagePattern for fire-and-forget\n@Controller()\nexport class NotificationsController {\n  @MessagePattern('user.created')\n  async handleUserCreated(data: UserCreatedEvent) {\n    // This WAITS for response, blocking the sender\n    await this.emailService.sendWelcome(data.email);\n    // If email fails, sender gets an error (coupling!)\n  }\n}\n\n// Use @EventPattern expecting a response\n@Controller()\nexport class OrdersController {\n  @EventPattern('inventory.check')\n  async checkInventory(data: CheckInventoryDto) {\n    const available = await this.inventory.check(data);\n    return available; // This return value is IGNORED with @EventPattern!\n  }\n}\n\n// Tight coupling in client\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Blocks until notification service responds\n    await this.client.send('user.created', user).toPromise();\n    // If notification service is down, user creation fails!\n\n    return user;\n  }\n}\n```\n\n**Correct (use MessagePattern for request-response, EventPattern for fire-and-forget):**\n\n```typescript\n// MessagePattern: Request-Response (when you NEED a response)\n@Controller()\nexport class InventoryController {\n  @MessagePattern({ cmd: 'check_inventory' })\n  async checkInventory(data: CheckInventoryDto): Promise<InventoryResult> {\n    const result = await this.inventoryService.check(data.productId, data.quantity);\n    return result; // Response sent back to caller\n  }\n}\n\n// Client expects response\n@Injectable()\nexport class OrdersService {\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    // Check inventory - we NEED this response to proceed\n    const inventory = await firstValueFrom(\n      this.inventoryClient.send<InventoryResult>(\n        { cmd: 'check_inventory' },\n        { productId: dto.productId, quantity: dto.quantity },\n      ),\n    );\n\n    if (!inventory.available) {\n      throw new BadRequestException('Insufficient inventory');\n    }\n\n    return this.repo.save(dto);\n  }\n}\n\n// EventPattern: Fire-and-Forget (for notifications, side effects)\n@Controller()\nexport class NotificationsController {\n  @EventPattern('user.created')\n  async handleUserCreated(data: UserCreatedEvent): Promise<void> {\n    // No return value needed - just process the event\n    await this.emailService.sendWelcome(data.email);\n    await this.analyticsService.track('user_signup', data);\n    // If this fails, it doesn't affect the sender\n  }\n}\n\n// Client emits event without waiting\n@Injectable()\nexport class UsersService {\n  async createUser(dto: CreateUserDto): Promise<User> {\n    const user = await this.repo.save(dto);\n\n    // Fire and forget - doesn't block, doesn't wait\n    this.eventClient.emit('user.created', {\n      userId: user.id,\n      email: user.email,\n      timestamp: new Date(),\n    });\n\n    return user; // User creation succeeds regardless of event handling\n  }\n}\n\n// Hybrid pattern for critical events\n@Injectable()\nexport class OrdersService {\n  async createOrder(dto: CreateOrderDto): Promise<Order> {\n    const order = await this.repo.save(dto);\n\n    // Critical: inventory reservation (use MessagePattern)\n    const reserved = await firstValueFrom(\n      this.inventoryClient.send({ cmd: 'reserve_inventory' }, {\n        orderId: order.id,\n        items: dto.items,\n      }),\n    );\n\n    if (!reserved.success) {\n      await this.repo.delete(order.id);\n      throw new BadRequestException('Could not reserve inventory');\n    }\n\n    // Non-critical: notifications (use EventPattern)\n    this.eventClient.emit('order.created', {\n      orderId: order.id,\n      userId: dto.userId,\n      total: dto.total,\n    });\n\n    return order;\n  }\n}\n\n// Error handling patterns\n// MessagePattern errors propagate to caller\n@MessagePattern({ cmd: 'get_user' })\nasync getUser(userId: string): Promise<User> {\n  const user = await this.repo.findOne({ where: { id: userId } });\n  if (!user) {\n    throw new RpcException('User not found'); // Received by caller\n  }\n  return user;\n}\n\n// EventPattern errors should be handled locally\n@EventPattern('order.created')\nasync handleOrderCreated(data: OrderCreatedEvent): Promise<void> {\n  try {\n    await this.processOrder(data);\n  } catch (error) {\n    // Log and potentially retry - don't throw\n    this.logger.error('Failed to process order event', error);\n    await this.deadLetterQueue.add(data);\n  }\n}\n```\n\nReference: [NestJS Microservices](https://docs.nestjs.com/microservices/basics)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/micro-use-queues.md",
    "content": "---\ntitle: Use Message Queues for Background Jobs\nimpact: MEDIUM-HIGH\nimpactDescription: Queues enable reliable background processing\ntags: microservices, queues, bullmq, background-jobs\n---\n\n## Use Message Queues for Background Jobs\n\nUse `@nestjs/bullmq` for background job processing. Queues decouple long-running tasks from HTTP requests, enable retry logic, and distribute workload across workers. Use them for emails, file processing, notifications, and any task that shouldn't block user requests.\n\n**Incorrect (long-running tasks in HTTP handlers):**\n\n```typescript\n// Long-running tasks in HTTP handlers\n@Controller('reports')\nexport class ReportsController {\n  @Post()\n  async generate(@Body() dto: GenerateReportDto): Promise<Report> {\n    // This blocks the request for potentially minutes\n    const data = await this.fetchLargeDataset(dto);\n    const report = await this.processData(data); // Slow!\n    await this.sendEmail(dto.email, report); // Can fail!\n    return report; // Client times out\n  }\n}\n\n// Fire-and-forget without retry\n@Injectable()\nexport class EmailService {\n  async sendWelcome(email: string): Promise<void> {\n    // If this fails, email is never sent\n    await this.mailer.send({ to: email, template: 'welcome' });\n    // No retry, no tracking, no visibility\n  }\n}\n\n// Use setInterval for scheduled tasks\nsetInterval(async () => {\n  await cleanupOldRecords();\n}, 60000); // No error handling, memory leaks\n```\n\n**Correct (use BullMQ for background processing):**\n\n```typescript\n// Configure BullMQ\nimport { BullModule } from '@nestjs/bullmq';\n\n@Module({\n  imports: [\n    BullModule.forRoot({\n      connection: {\n        host: 'localhost',\n        port: 6379,\n      },\n      defaultJobOptions: {\n        removeOnComplete: 1000,\n        removeOnFail: 5000,\n        attempts: 3,\n        backoff: {\n          type: 'exponential',\n          delay: 1000,\n        },\n      },\n    }),\n    BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),\n  ],\n})\nexport class QueueModule {}\n\n// Producer: Add jobs to queue\n@Injectable()\nexport class ReportsService {\n  constructor(@InjectQueue('reports') private reportsQueue: Queue) {}\n\n  async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {\n    // Return immediately, process in background\n    const job = await this.reportsQueue.add('generate', dto, {\n      priority: dto.urgent ? 1 : 10,\n      delay: dto.scheduledFor ? Date.parse(dto.scheduledFor) - Date.now() : 0,\n    });\n\n    return { jobId: job.id };\n  }\n\n  async getJobStatus(jobId: string): Promise<JobStatus> {\n    const job = await this.reportsQueue.getJob(jobId);\n    return {\n      status: await job.getState(),\n      progress: job.progress,\n      result: job.returnvalue,\n    };\n  }\n}\n\n// Consumer: Process jobs\n@Processor('reports')\nexport class ReportsProcessor {\n  private readonly logger = new Logger(ReportsProcessor.name);\n\n  @Process('generate')\n  async generateReport(job: Job<GenerateReportDto>): Promise<Report> {\n    this.logger.log(`Processing report job ${job.id}`);\n\n    // Update progress\n    await job.updateProgress(10);\n\n    const data = await this.fetchData(job.data);\n    await job.updateProgress(50);\n\n    const report = await this.processData(data);\n    await job.updateProgress(90);\n\n    await this.saveReport(report);\n    await job.updateProgress(100);\n\n    return report;\n  }\n\n  @OnQueueActive()\n  onActive(job: Job) {\n    this.logger.log(`Processing job ${job.id}`);\n  }\n\n  @OnQueueCompleted()\n  onCompleted(job: Job, result: any) {\n    this.logger.log(`Job ${job.id} completed`);\n  }\n\n  @OnQueueFailed()\n  onFailed(job: Job, error: Error) {\n    this.logger.error(`Job ${job.id} failed: ${error.message}`);\n  }\n}\n\n// Email queue with retry\n@Processor('email')\nexport class EmailProcessor {\n  @Process('send')\n  async sendEmail(job: Job<SendEmailDto>): Promise<void> {\n    const { to, template, data } = job.data;\n\n    try {\n      await this.mailer.send({\n        to,\n        template,\n        context: data,\n      });\n    } catch (error) {\n      // BullMQ will retry based on job options\n      throw error;\n    }\n  }\n}\n\n// Usage\n@Injectable()\nexport class NotificationService {\n  constructor(@InjectQueue('email') private emailQueue: Queue) {}\n\n  async sendWelcome(user: User): Promise<void> {\n    await this.emailQueue.add(\n      'send',\n      {\n        to: user.email,\n        template: 'welcome',\n        data: { name: user.name },\n      },\n      {\n        attempts: 5,\n        backoff: { type: 'exponential', delay: 5000 },\n      },\n    );\n  }\n}\n\n// Scheduled jobs\n@Injectable()\nexport class ScheduledJobsService implements OnModuleInit {\n  constructor(@InjectQueue('maintenance') private queue: Queue) {}\n\n  async onModuleInit(): Promise<void> {\n    // Clean up old reports daily at midnight\n    await this.queue.add(\n      'cleanup',\n      {},\n      {\n        repeat: { cron: '0 0 * * *' },\n        jobId: 'daily-cleanup', // Prevent duplicates\n      },\n    );\n\n    // Send digest every hour\n    await this.queue.add(\n      'digest',\n      {},\n      {\n        repeat: { every: 60 * 60 * 1000 },\n        jobId: 'hourly-digest',\n      },\n    );\n  }\n}\n\n@Processor('maintenance')\nexport class MaintenanceProcessor {\n  @Process('cleanup')\n  async cleanup(): Promise<void> {\n    await this.cleanupOldReports();\n    await this.cleanupExpiredSessions();\n  }\n\n  @Process('digest')\n  async sendDigest(): Promise<void> {\n    const users = await this.getUsersForDigest();\n    for (const user of users) {\n      await this.emailQueue.add('send', { to: user.email, template: 'digest' });\n    }\n  }\n}\n\n// Queue monitoring with Bull Board\nimport { BullBoardModule } from '@bull-board/nestjs';\nimport { BullMQAdapter } from '@bull-board/api/bullMQAdapter';\n\n@Module({\n  imports: [\n    BullBoardModule.forRoot({\n      route: '/admin/queues',\n      adapter: ExpressAdapter,\n    }),\n    BullBoardModule.forFeature({\n      name: 'email',\n      adapter: BullMQAdapter,\n    }),\n    BullBoardModule.forFeature({\n      name: 'reports',\n      adapter: BullMQAdapter,\n    }),\n  ],\n})\nexport class AdminModule {}\n```\n\nReference: [NestJS Queues](https://docs.nestjs.com/techniques/queues)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/perf-async-hooks.md",
    "content": "---\ntitle: Use Async Lifecycle Hooks Correctly\nimpact: HIGH\nimpactDescription: Improper async handling blocks application startup\ntags: performance, lifecycle, async, hooks\n---\n\n## Use Async Lifecycle Hooks Correctly\n\nNestJS lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.) support async operations. However, misusing them can block application startup or cause race conditions. Understand the lifecycle order and use hooks appropriately.\n\n**Incorrect (fire-and-forget async without await):**\n\n```typescript\n// Fire-and-forget async without await\n@Injectable()\nexport class DatabaseService implements OnModuleInit {\n  onModuleInit() {\n    // This runs but doesn't block - app starts before DB is ready!\n    this.connect();\n  }\n\n  private async connect() {\n    await this.pool.connect();\n    console.log('Database connected');\n  }\n}\n\n// Heavy blocking operations in constructor\n@Injectable()\nexport class ConfigService {\n  private config: Config;\n\n  constructor() {\n    // BLOCKS entire module instantiation synchronously\n    this.config = fs.readFileSync('config.json');\n  }\n}\n```\n\n**Correct (return promises from async hooks):**\n\n```typescript\n// Return promise from async hooks\n@Injectable()\nexport class DatabaseService implements OnModuleInit {\n  private pool: Pool;\n\n  async onModuleInit(): Promise<void> {\n    // NestJS waits for this to complete before continuing\n    await this.pool.connect();\n    console.log('Database connected');\n  }\n\n  async onModuleDestroy(): Promise<void> {\n    // Clean up resources on shutdown\n    await this.pool.end();\n    console.log('Database disconnected');\n  }\n}\n\n// Use onApplicationBootstrap for cross-module dependencies\n@Injectable()\nexport class CacheWarmerService implements OnApplicationBootstrap {\n  constructor(\n    private cache: CacheService,\n    private products: ProductsService,\n  ) {}\n\n  async onApplicationBootstrap(): Promise<void> {\n    // All modules are initialized, safe to warm cache\n    const products = await this.products.findPopular();\n    await this.cache.warmup(products);\n  }\n}\n\n// Heavy init in async hooks, not constructor\n@Injectable()\nexport class ConfigService implements OnModuleInit {\n  private config: Config;\n\n  constructor() {\n    // Keep constructor synchronous and fast\n  }\n\n  async onModuleInit(): Promise<void> {\n    // Async loading in lifecycle hook\n    this.config = await this.loadConfig();\n  }\n\n  private async loadConfig(): Promise<Config> {\n    const file = await fs.promises.readFile('config.json');\n    return JSON.parse(file.toString());\n  }\n\n  get<T>(key: string): T {\n    return this.config[key];\n  }\n}\n\n// Enable shutdown hooks in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n  app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling\n  await app.listen(3000);\n}\n```\n\nReference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/perf-lazy-loading.md",
    "content": "---\ntitle: Use Lazy Loading for Large Modules\nimpact: MEDIUM\nimpactDescription: Improves startup time for large applications\ntags: performance, lazy-loading, modules, optimization\n---\n\n## Use Lazy Loading for Large Modules\n\nNestJS supports lazy-loading modules, which defers initialization until first use. This is valuable for large applications where some features are rarely used, serverless deployments where cold start time matters, or when certain modules have heavy initialization costs.\n\n**Incorrect (loading everything eagerly):**\n\n```typescript\n// Load everything eagerly in a large app\n@Module({\n  imports: [\n    UsersModule,\n    OrdersModule,\n    PaymentsModule,\n    ReportsModule, // Heavy, rarely used\n    AnalyticsModule, // Heavy, rarely used\n    AdminModule, // Only admins use this\n    LegacyModule, // Migration module, rarely used\n    BulkImportModule, // Used once a month\n  ],\n})\nexport class AppModule {}\n\n// All modules initialize at startup, even if never used\n// Slow cold starts in serverless\n// Memory wasted on unused modules\n```\n\n**Correct (lazy load rarely-used modules):**\n\n```typescript\n// Use LazyModuleLoader for optional modules\nimport { LazyModuleLoader } from '@nestjs/core';\n\n@Injectable()\nexport class ReportsService {\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  async generateReport(type: string): Promise<Report> {\n    // Load module only when needed\n    const { ReportsModule } = await import('./reports/reports.module');\n    const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule);\n\n    const reportsService = moduleRef.get(ReportsGeneratorService);\n    return reportsService.generate(type);\n  }\n}\n\n// Lazy load admin features with caching\n@Injectable()\nexport class AdminService {\n  private adminModule: ModuleRef | null = null;\n\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  private async getAdminModule(): Promise<ModuleRef> {\n    if (!this.adminModule) {\n      const { AdminModule } = await import('./admin/admin.module');\n      this.adminModule = await this.lazyModuleLoader.load(() => AdminModule);\n    }\n    return this.adminModule;\n  }\n\n  async runAdminTask(task: string): Promise<void> {\n    const moduleRef = await this.getAdminModule();\n    const taskRunner = moduleRef.get(AdminTaskRunner);\n    await taskRunner.run(task);\n  }\n}\n\n// Reusable lazy loader service\n@Injectable()\nexport class ModuleLoaderService {\n  private loadedModules = new Map<string, ModuleRef>();\n\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  async load<T>(key: string, importFn: () => Promise<{ default: Type<T> } | Type<T>>): Promise<ModuleRef> {\n    if (!this.loadedModules.has(key)) {\n      const module = await importFn();\n      const moduleType = 'default' in module ? module.default : module;\n      const moduleRef = await this.lazyModuleLoader.load(() => moduleType);\n      this.loadedModules.set(key, moduleRef);\n    }\n    return this.loadedModules.get(key)!;\n  }\n}\n\n// Preload modules in background after startup\n@Injectable()\nexport class ModulePreloader implements OnApplicationBootstrap {\n  constructor(private lazyModuleLoader: LazyModuleLoader) {}\n\n  async onApplicationBootstrap(): Promise<void> {\n    setTimeout(async () => {\n      await this.preloadModule(() => import('./reports/reports.module'));\n    }, 5000); // 5 seconds after startup\n  }\n\n  private async preloadModule(importFn: () => Promise<any>): Promise<void> {\n    try {\n      const module = await importFn();\n      const moduleType = module.default || Object.values(module)[0];\n      await this.lazyModuleLoader.load(() => moduleType);\n    } catch (error) {\n      console.warn('Failed to preload module', error);\n    }\n  }\n}\n```\n\nReference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/perf-optimize-database.md",
    "content": "---\ntitle: Optimize Database Queries\nimpact: HIGH\nimpactDescription: Database queries are typically the largest source of latency\ntags: performance, database, queries, optimization\n---\n\n## Optimize Database Queries\n\nSelect only needed columns, use proper indexes, avoid over-fetching relations, and consider query performance when designing your data access. Most API slowness traces back to inefficient database queries.\n\n**Incorrect (over-fetching data and missing indexes):**\n\n```typescript\n// Select everything when you need few fields\n@Injectable()\nexport class UsersService {\n  async findAllEmails(): Promise<string[]> {\n    const users = await this.repo.find();\n    // Fetches ALL columns for ALL users\n    return users.map(u => u.email);\n  }\n\n  async getUserSummary(id: string): Promise<UserSummary> {\n    const user = await this.repo.findOne({\n      where: { id },\n      relations: ['posts', 'posts.comments', 'posts.comments.author', 'followers'],\n    });\n    // Over-fetches massive relation tree\n    return { name: user.name, postCount: user.posts.length };\n  }\n}\n\n// No indexes on frequently queried columns\n@Entity()\nexport class Order {\n  @Column()\n  userId: string; // No index - full table scan on every lookup\n\n  @Column()\n  status: string; // No index - slow status filtering\n}\n```\n\n**Correct (select only needed data with proper indexes):**\n\n```typescript\n// Select only needed columns\n@Injectable()\nexport class UsersService {\n  async findAllEmails(): Promise<string[]> {\n    const users = await this.repo.find({\n      select: ['email'], // Only fetch email column\n    });\n    return users.map(u => u.email);\n  }\n\n  // Use QueryBuilder for complex selections\n  async getUserSummary(id: string): Promise<UserSummary> {\n    return this.repo\n      .createQueryBuilder('user')\n      .select('user.name', 'name')\n      .addSelect('COUNT(post.id)', 'postCount')\n      .leftJoin('user.posts', 'post')\n      .where('user.id = :id', { id })\n      .groupBy('user.id')\n      .getRawOne();\n  }\n\n  // Fetch relations only when needed\n  async getFullProfile(id: string): Promise<User> {\n    return this.repo.findOne({\n      where: { id },\n      relations: ['posts'], // Only immediate relation\n      select: {\n        id: true,\n        name: true,\n        email: true,\n        posts: {\n          id: true,\n          title: true,\n        },\n      },\n    });\n  }\n}\n\n// Add indexes on frequently queried columns\n@Entity()\n@Index(['userId'])\n@Index(['status'])\n@Index(['createdAt'])\n@Index(['userId', 'status']) // Composite index for common query pattern\nexport class Order {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Column()\n  userId: string;\n\n  @Column()\n  status: string;\n\n  @CreateDateColumn()\n  createdAt: Date;\n}\n\n// Always paginate large datasets\n@Injectable()\nexport class OrdersService {\n  async findAll(page = 1, limit = 20): Promise<PaginatedResult<Order>> {\n    const [items, total] = await this.repo.findAndCount({\n      skip: (page - 1) * limit,\n      take: limit,\n      order: { createdAt: 'DESC' },\n    });\n\n    return {\n      items,\n      meta: {\n        page,\n        limit,\n        total,\n        totalPages: Math.ceil(total / limit),\n      },\n    };\n  }\n}\n```\n\nReference: [TypeORM Query Builder](https://typeorm.io/select-query-builder)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/perf-use-caching.md",
    "content": "---\ntitle: Use Caching Strategically\nimpact: HIGH\nimpactDescription: Dramatically reduces database load and response times\ntags: performance, caching, redis, optimization\n---\n\n## Use Caching Strategically\n\nImplement caching for expensive operations, frequently accessed data, and external API calls. Use NestJS CacheModule with appropriate TTLs and cache invalidation strategies. Don't cache everything - focus on high-impact areas.\n\n**Incorrect (no caching or caching everything):**\n\n```typescript\n// No caching for expensive, repeated queries\n@Injectable()\nexport class ProductsService {\n  async getPopular(): Promise<Product[]> {\n    // Runs complex aggregation query EVERY request\n    return this.productsRepo\n      .createQueryBuilder('p')\n      .leftJoin('p.orders', 'o')\n      .select('p.*, COUNT(o.id) as orderCount')\n      .groupBy('p.id')\n      .orderBy('orderCount', 'DESC')\n      .limit(20)\n      .getMany();\n  }\n}\n\n// Cache everything without thought\n@Injectable()\nexport class UsersService {\n  @CacheKey('users')\n  @CacheTTL(3600)\n  @UseInterceptors(CacheInterceptor)\n  async findAll(): Promise<User[]> {\n    // Caching user list for 1 hour is wrong if data changes frequently\n    return this.usersRepo.find();\n  }\n}\n```\n\n**Correct (strategic caching with proper invalidation):**\n\n```typescript\n// Setup caching module\n@Module({\n  imports: [\n    CacheModule.registerAsync({\n      imports: [ConfigModule],\n      inject: [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        stores: [new KeyvRedis(config.get('REDIS_URL'))],\n        ttl: 60 * 1000, // Default 60s\n      }),\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Manual caching for granular control\n@Injectable()\nexport class ProductsService {\n  constructor(\n    @Inject(CACHE_MANAGER) private cache: Cache,\n    private productsRepo: ProductRepository,\n  ) {}\n\n  async getPopular(): Promise<Product[]> {\n    const cacheKey = 'products:popular';\n\n    // Try cache first\n    const cached = await this.cache.get<Product[]>(cacheKey);\n    if (cached) return cached;\n\n    // Cache miss - fetch and cache\n    const products = await this.fetchPopularProducts();\n    await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL\n    return products;\n  }\n\n  // Invalidate cache on changes\n  async updateProduct(id: string, dto: UpdateProductDto): Promise<Product> {\n    const product = await this.productsRepo.save({ id, ...dto });\n    await this.cache.del('products:popular'); // Invalidate\n    return product;\n  }\n}\n\n// Decorator-based caching with auto-interceptor\n@Controller('categories')\n@UseInterceptors(CacheInterceptor)\nexport class CategoriesController {\n  @Get()\n  @CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change\n  findAll(): Promise<Category[]> {\n    return this.categoriesService.findAll();\n  }\n\n  @Get(':id')\n  @CacheTTL(60 * 1000) // 1 minute\n  @CacheKey('category')\n  findOne(@Param('id') id: string): Promise<Category> {\n    return this.categoriesService.findOne(id);\n  }\n}\n\n// Event-based cache invalidation\n@Injectable()\nexport class CacheInvalidationService {\n  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}\n\n  @OnEvent('product.created')\n  @OnEvent('product.updated')\n  @OnEvent('product.deleted')\n  async invalidateProductCaches(event: ProductEvent) {\n    await Promise.all([this.cache.del('products:popular'), this.cache.del(`product:${event.productId}`)]);\n  }\n}\n```\n\nReference: [NestJS Caching](https://docs.nestjs.com/techniques/caching)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/security-auth-jwt.md",
    "content": "---\ntitle: Implement Secure JWT Authentication\nimpact: CRITICAL\nimpactDescription: Essential for secure APIs\ntags: security, jwt, authentication, tokens\n---\n\n## Implement Secure JWT Authentication\n\nUse `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads.\n\n**Incorrect (insecure JWT implementation):**\n\n```typescript\n// Hardcode secrets\n@Module({\n  imports: [\n    JwtModule.register({\n      secret: 'my-secret-key', // Exposed in code\n      signOptions: { expiresIn: '7d' }, // Too long\n    }),\n  ],\n})\nexport class AuthModule {}\n\n// Store sensitive data in JWT\nasync login(user: User): Promise<{ accessToken: string }> {\n  const payload = {\n    sub: user.id,\n    email: user.email,\n    password: user.password, // NEVER include password!\n    ssn: user.ssn, // NEVER include sensitive data!\n    isAdmin: user.isAdmin, // Can be tampered if not verified\n  };\n  return { accessToken: this.jwtService.sign(payload) };\n}\n\n// Skip token validation\n@Injectable()\nexport class JwtStrategy extends PassportStrategy(Strategy) {\n  constructor() {\n    super({\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n      secretOrKey: 'my-secret',\n    });\n  }\n\n  async validate(payload: any): Promise<any> {\n    return payload; // No validation of user existence\n  }\n}\n```\n\n**Correct (secure JWT with refresh tokens):**\n\n```typescript\n// Secure JWT configuration\n@Module({\n  imports: [\n    JwtModule.registerAsync({\n      imports: [ConfigModule],\n      inject: [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        secret: config.get<string>('JWT_SECRET'),\n        signOptions: {\n          expiresIn: '15m', // Short-lived access tokens\n          issuer: config.get<string>('JWT_ISSUER'),\n          audience: config.get<string>('JWT_AUDIENCE'),\n        },\n      }),\n    }),\n    PassportModule.register({ defaultStrategy: 'jwt' }),\n  ],\n})\nexport class AuthModule {}\n\n// Minimal JWT payload\n@Injectable()\nexport class AuthService {\n  async login(user: User): Promise<TokenResponse> {\n    // Only include necessary, non-sensitive data\n    const payload: JwtPayload = {\n      sub: user.id,\n      email: user.email,\n      roles: user.roles,\n      iat: Math.floor(Date.now() / 1000),\n    };\n\n    const accessToken = this.jwtService.sign(payload);\n    const refreshToken = await this.createRefreshToken(user.id);\n\n    return { accessToken, refreshToken, expiresIn: 900 };\n  }\n\n  private async createRefreshToken(userId: string): Promise<string> {\n    const token = randomBytes(32).toString('hex');\n    const hashedToken = await bcrypt.hash(token, 10);\n\n    await this.refreshTokenRepo.save({\n      userId,\n      token: hashedToken,\n      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days\n    });\n\n    return token;\n  }\n}\n\n// Proper JWT strategy with validation\n@Injectable()\nexport class JwtStrategy extends PassportStrategy(Strategy) {\n  constructor(\n    private config: ConfigService,\n    private usersService: UsersService,\n  ) {\n    super({\n      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),\n      secretOrKey: config.get<string>('JWT_SECRET'),\n      ignoreExpiration: false,\n      issuer: config.get<string>('JWT_ISSUER'),\n      audience: config.get<string>('JWT_AUDIENCE'),\n    });\n  }\n\n  async validate(payload: JwtPayload): Promise<User> {\n    // Verify user still exists and is active\n    const user = await this.usersService.findById(payload.sub);\n\n    if (!user || !user.isActive) {\n      throw new UnauthorizedException('User not found or inactive');\n    }\n\n    // Verify token wasn't issued before password change\n    if (user.passwordChangedAt) {\n      const tokenIssuedAt = new Date(payload.iat * 1000);\n      if (tokenIssuedAt < user.passwordChangedAt) {\n        throw new UnauthorizedException('Token invalidated by password change');\n      }\n    }\n\n    return user;\n  }\n}\n```\n\nReference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/security-rate-limiting.md",
    "content": "---\ntitle: Implement Rate Limiting\nimpact: HIGH\nimpactDescription: Protects against abuse and ensures fair resource usage\ntags: security, rate-limiting, throttler, protection\n---\n\n## Implement Rate Limiting\n\nUse `@nestjs/throttler` to limit request rates per client. Apply different limits for different endpoints - stricter for auth endpoints, more relaxed for read operations. Consider using Redis for distributed rate limiting in clustered deployments.\n\n**Incorrect (no rate limiting on sensitive endpoints):**\n\n```typescript\n// No rate limiting on sensitive endpoints\n@Controller('auth')\nexport class AuthController {\n  @Post('login')\n  async login(@Body() dto: LoginDto): Promise<TokenResponse> {\n    // Attackers can brute-force credentials\n    return this.authService.login(dto);\n  }\n\n  @Post('forgot-password')\n  async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {\n    // Can be abused to spam users with emails\n    return this.authService.sendResetEmail(dto.email);\n  }\n}\n\n// Same limits for all endpoints\n@UseGuards(ThrottlerGuard)\n@Controller('api')\nexport class ApiController {\n  @Get('public-data')\n  async getPublic() {} // Should allow more requests\n\n  @Post('process-payment')\n  async payment() {} // Should be more restrictive\n}\n```\n\n**Correct (configured throttler with endpoint-specific limits):**\n\n```typescript\n// Configure throttler globally with multiple limits\nimport { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';\n\n@Module({\n  imports: [\n    ThrottlerModule.forRoot([\n      {\n        name: 'short',\n        ttl: 1000, // 1 second\n        limit: 3, // 3 requests per second\n      },\n      {\n        name: 'medium',\n        ttl: 10000, // 10 seconds\n        limit: 20, // 20 requests per 10 seconds\n      },\n      {\n        name: 'long',\n        ttl: 60000, // 1 minute\n        limit: 100, // 100 requests per minute\n      },\n    ]),\n  ],\n  providers: [\n    {\n      provide: APP_GUARD,\n      useClass: ThrottlerGuard,\n    },\n  ],\n})\nexport class AppModule {}\n\n// Override limits per endpoint\n@Controller('auth')\nexport class AuthController {\n  @Post('login')\n  @Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute\n  async login(@Body() dto: LoginDto): Promise<TokenResponse> {\n    return this.authService.login(dto);\n  }\n\n  @Post('forgot-password')\n  @Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour\n  async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {\n    return this.authService.sendResetEmail(dto.email);\n  }\n}\n\n// Skip throttling for certain routes\n@Controller('health')\nexport class HealthController {\n  @Get()\n  @SkipThrottle()\n  check(): string {\n    return 'OK';\n  }\n}\n\n// Custom throttle per user type\n@Injectable()\nexport class CustomThrottlerGuard extends ThrottlerGuard {\n  protected async getTracker(req: Request): Promise<string> {\n    // Use user ID if authenticated, IP otherwise\n    return req.user?.id || req.ip;\n  }\n\n  protected async getLimit(context: ExecutionContext): Promise<number> {\n    const request = context.switchToHttp().getRequest();\n\n    // Higher limits for authenticated users\n    if (request.user) {\n      return request.user.isPremium ? 1000 : 200;\n    }\n\n    return 50; // Anonymous users\n  }\n}\n```\n\nReference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/security-sanitize-output.md",
    "content": "---\ntitle: Sanitize Output to Prevent XSS\nimpact: HIGH\nimpactDescription: XSS vulnerabilities can compromise user sessions and data\ntags: security, xss, sanitization, html\n---\n\n## Sanitize Output to Prevent XSS\n\nWhile NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers.\n\n**Incorrect (storing raw HTML without sanitization):**\n\n```typescript\n// Store raw HTML from users\n@Injectable()\nexport class CommentsService {\n  async create(dto: CreateCommentDto): Promise<Comment> {\n    // User can inject: <script>steal(document.cookie)</script>\n    return this.repo.save({\n      content: dto.content, // Raw, unsanitized\n      authorId: dto.authorId,\n    });\n  }\n}\n\n// Return HTML without sanitization\n@Controller('pages')\nexport class PagesController {\n  @Get(':slug')\n  @Header('Content-Type', 'text/html')\n  async getPage(@Param('slug') slug: string): Promise<string> {\n    const page = await this.pagesService.findBySlug(slug);\n    // If page.content contains user input, XSS is possible\n    return `<html><body>${page.content}</body></html>`;\n  }\n}\n\n// Reflect user input in errors\n@Get(':id')\nasync findOne(@Param('id') id: string): Promise<User> {\n  const user = await this.repo.findOne({ where: { id } });\n  if (!user) {\n    // XSS if id contains malicious content and error is rendered\n    throw new NotFoundException(`User ${id} not found`);\n  }\n  return user;\n}\n```\n\n**Correct (sanitize content and use proper headers):**\n\n```typescript\n// Sanitize HTML content before storage\nimport * as sanitizeHtml from 'sanitize-html';\n\n@Injectable()\nexport class CommentsService {\n  private readonly sanitizeOptions: sanitizeHtml.IOptions = {\n    allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],\n    allowedAttributes: {\n      a: ['href', 'title'],\n    },\n    allowedSchemes: ['http', 'https', 'mailto'],\n  };\n\n  async create(dto: CreateCommentDto): Promise<Comment> {\n    return this.repo.save({\n      content: sanitizeHtml(dto.content, this.sanitizeOptions),\n      authorId: dto.authorId,\n    });\n  }\n}\n\n// Use validation pipe to strip HTML\nimport { Transform } from 'class-transformer';\n\nexport class CreatePostDto {\n  @IsString()\n  @MaxLength(1000)\n  @Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))\n  title: string;\n\n  @IsString()\n  @Transform(({ value }) =>\n    sanitizeHtml(value, {\n      allowedTags: ['p', 'br', 'b', 'i', 'a'],\n      allowedAttributes: { a: ['href'] },\n    }),\n  )\n  content: string;\n}\n\n// Set proper Content-Type headers\n@Controller('api')\nexport class ApiController {\n  @Get('data')\n  @Header('Content-Type', 'application/json')\n  async getData(): Promise<DataResponse> {\n    // JSON response - browser won't execute scripts\n    return this.service.getData();\n  }\n}\n\n// Sanitize error messages\n@Get(':id')\nasync findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {\n  const user = await this.repo.findOne({ where: { id } });\n  if (!user) {\n    // UUID validation ensures safe format\n    throw new NotFoundException('User not found');\n  }\n  return user;\n}\n\n// Use Helmet for CSP headers\nimport helmet from 'helmet';\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  app.use(\n    helmet({\n      contentSecurityPolicy: {\n        directives: {\n          defaultSrc: [\"'self'\"],\n          scriptSrc: [\"'self'\"],\n          styleSrc: [\"'self'\", \"'unsafe-inline'\"],\n          imgSrc: [\"'self'\", 'data:', 'https:'],\n        },\n      },\n    }),\n  );\n\n  await app.listen(3000);\n}\n```\n\nReference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/security-use-guards.md",
    "content": "---\ntitle: Use Guards for Authentication and Authorization\nimpact: HIGH\nimpactDescription: Enforces access control before handlers execute\ntags: security, guards, authentication, authorization\n---\n\n## Use Guards for Authentication and Authorization\n\nGuards determine whether a request should be handled based on authentication state, roles, permissions, or other conditions. They run after middleware but before pipes and interceptors, making them ideal for access control. Use guards instead of manual checks in controllers.\n\n**Incorrect (manual auth checks in every handler):**\n\n```typescript\n// Manual auth checks in every handler\n@Controller('admin')\nexport class AdminController {\n  @Get('users')\n  async getUsers(@Request() req) {\n    if (!req.user) {\n      throw new UnauthorizedException();\n    }\n    if (!req.user.roles.includes('admin')) {\n      throw new ForbiddenException();\n    }\n    return this.adminService.getUsers();\n  }\n\n  @Delete('users/:id')\n  async deleteUser(@Request() req, @Param('id') id: string) {\n    if (!req.user) {\n      throw new UnauthorizedException();\n    }\n    if (!req.user.roles.includes('admin')) {\n      throw new ForbiddenException();\n    }\n    return this.adminService.deleteUser(id);\n  }\n}\n```\n\n**Correct (guards with declarative decorators):**\n\n```typescript\n// JWT Auth Guard\n@Injectable()\nexport class JwtAuthGuard implements CanActivate {\n  constructor(\n    private jwtService: JwtService,\n    private reflector: Reflector,\n  ) {}\n\n  async canActivate(context: ExecutionContext): Promise<boolean> {\n    // Check for @Public() decorator\n    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [context.getHandler(), context.getClass()]);\n    if (isPublic) return true;\n\n    const request = context.switchToHttp().getRequest();\n    const token = this.extractToken(request);\n\n    if (!token) {\n      throw new UnauthorizedException('No token provided');\n    }\n\n    try {\n      request.user = await this.jwtService.verifyAsync(token);\n      return true;\n    } catch {\n      throw new UnauthorizedException('Invalid token');\n    }\n  }\n\n  private extractToken(request: Request): string | undefined {\n    const [type, token] = request.headers.authorization?.split(' ') ?? [];\n    return type === 'Bearer' ? token : undefined;\n  }\n}\n\n// Roles Guard\n@Injectable()\nexport class RolesGuard implements CanActivate {\n  constructor(private reflector: Reflector) {}\n\n  canActivate(context: ExecutionContext): boolean {\n    const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [context.getHandler(), context.getClass()]);\n\n    if (!requiredRoles) return true;\n\n    const { user } = context.switchToHttp().getRequest();\n    return requiredRoles.some(role => user.roles?.includes(role));\n  }\n}\n\n// Decorators\nexport const Public = () => SetMetadata('isPublic', true);\nexport const Roles = (...roles: Role[]) => SetMetadata('roles', roles);\n\n// Register guards globally\n@Module({\n  providers: [\n    { provide: APP_GUARD, useClass: JwtAuthGuard },\n    { provide: APP_GUARD, useClass: RolesGuard },\n  ],\n})\nexport class AppModule {}\n\n// Clean controller\n@Controller('admin')\n@Roles(Role.Admin) // Applied to all routes\nexport class AdminController {\n  @Get('users')\n  getUsers(): Promise<User[]> {\n    return this.adminService.getUsers();\n  }\n\n  @Delete('users/:id')\n  deleteUser(@Param('id') id: string): Promise<void> {\n    return this.adminService.deleteUser(id);\n  }\n\n  @Public() // Override: no auth required\n  @Get('health')\n  health() {\n    return { status: 'ok' };\n  }\n}\n```\n\nReference: [NestJS Guards](https://docs.nestjs.com/guards)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/security-validate-all-input.md",
    "content": "---\ntitle: Validate All Input with DTOs and Pipes\nimpact: HIGH\nimpactDescription: First line of defense against attacks\ntags: security, validation, dto, pipes\n---\n\n## Validate All Input with DTOs and Pipes\n\nAlways validate incoming data using class-validator decorators on DTOs and the global ValidationPipe. Never trust user input. Validate all request bodies, query parameters, and route parameters before processing.\n\n**Incorrect (trust raw input without validation):**\n\n```typescript\n// Trust raw input without validation\n@Controller('users')\nexport class UsersController {\n  @Post()\n  create(@Body() body: any) {\n    // body could contain anything - SQL injection, XSS, etc.\n    return this.usersService.create(body);\n  }\n\n  @Get()\n  findAll(@Query() query: any) {\n    // query.limit could be \"'; DROP TABLE users; --\"\n    return this.usersService.findAll(query.limit);\n  }\n}\n\n// DTOs without validation decorators\nexport class CreateUserDto {\n  name: string; // No validation\n  email: string; // Could be \"not-an-email\"\n  age: number; // Could be \"abc\" or -999\n}\n```\n\n**Correct (validated DTOs with global ValidationPipe):**\n\n```typescript\n// Enable ValidationPipe globally in main.ts\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule);\n\n  app.useGlobalPipes(\n    new ValidationPipe({\n      whitelist: true, // Strip unknown properties\n      forbidNonWhitelisted: true, // Throw on unknown properties\n      transform: true, // Auto-transform to DTO types\n      transformOptions: {\n        enableImplicitConversion: true,\n      },\n    }),\n  );\n\n  await app.listen(3000);\n}\n\n// Create well-validated DTOs\nimport {\n  IsString,\n  IsEmail,\n  IsInt,\n  Min,\n  Max,\n  IsOptional,\n  MinLength,\n  MaxLength,\n  Matches,\n  IsNotEmpty,\n} from 'class-validator';\nimport { Transform, Type } from 'class-transformer';\n\nexport class CreateUserDto {\n  @IsString()\n  @IsNotEmpty()\n  @MinLength(2)\n  @MaxLength(100)\n  @Transform(({ value }) => value?.trim())\n  name: string;\n\n  @IsEmail()\n  @Transform(({ value }) => value?.toLowerCase().trim())\n  email: string;\n\n  @IsInt()\n  @Min(0)\n  @Max(150)\n  age: number;\n\n  @IsString()\n  @MinLength(8)\n  @MaxLength(100)\n  @Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/, {\n    message: 'Password must contain uppercase, lowercase, and number',\n  })\n  password: string;\n}\n\n// Query DTO with defaults and transformation\nexport class FindUsersQueryDto {\n  @IsOptional()\n  @IsString()\n  @MaxLength(100)\n  search?: string;\n\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(1)\n  @Max(100)\n  limit: number = 20;\n\n  @IsOptional()\n  @Type(() => Number)\n  @IsInt()\n  @Min(0)\n  offset: number = 0;\n}\n\n// Param validation\nexport class UserIdParamDto {\n  @IsUUID('4')\n  id: string;\n}\n\n@Controller('users')\nexport class UsersController {\n  @Post()\n  create(@Body() dto: CreateUserDto): Promise<User> {\n    // dto is guaranteed to be valid\n    return this.usersService.create(dto);\n  }\n\n  @Get()\n  findAll(@Query() query: FindUsersQueryDto): Promise<User[]> {\n    // query.limit is a number, query.search is sanitized\n    return this.usersService.findAll(query);\n  }\n\n  @Get(':id')\n  findOne(@Param() params: UserIdParamDto): Promise<User> {\n    // params.id is a valid UUID\n    return this.usersService.findById(params.id);\n  }\n}\n```\n\nReference: [NestJS Validation](https://docs.nestjs.com/techniques/validation)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/test-e2e-supertest.md",
    "content": "---\ntitle: Use Supertest for E2E Testing\nimpact: HIGH\nimpactDescription: Validates the full request/response cycle\ntags: testing, e2e, supertest, integration\n---\n\n## Use Supertest for E2E Testing\n\nEnd-to-end tests use Supertest to make real HTTP requests against your NestJS application. They test the full stack including middleware, guards, pipes, and interceptors. E2E tests catch integration issues that unit tests miss.\n\n**Incorrect (no proper E2E setup or teardown):**\n\n```typescript\n// Only unit test controllers\ndescribe('UsersController', () => {\n  it('should return users', async () => {\n    const service = { findAll: jest.fn().mockResolvedValue([]) };\n    const controller = new UsersController(service as any);\n\n    const result = await controller.findAll();\n\n    expect(result).toEqual([]);\n    // Doesn't test: routes, guards, pipes, serialization\n  });\n});\n\n// E2E tests without proper setup/teardown\ndescribe('Users API', () => {\n  it('should create user', async () => {\n    const app = await NestFactory.create(AppModule);\n    // No proper initialization\n    // No cleanup after test\n    // Hits real database\n  });\n});\n```\n\n**Correct (proper E2E setup with Supertest):**\n\n```typescript\n// Proper E2E test setup\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { INestApplication, ValidationPipe } from '@nestjs/common';\nimport * as request from 'supertest';\nimport { AppModule } from '../src/app.module';\n\ndescribe('UsersController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeAll(async () => {\n    const moduleFixture: TestingModule = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n\n    // Apply same config as production\n    app.useGlobalPipes(\n      new ValidationPipe({\n        whitelist: true,\n        transform: true,\n        forbidNonWhitelisted: true,\n      }),\n    );\n\n    await app.init();\n  });\n\n  afterAll(async () => {\n    await app.close();\n  });\n\n  describe('/users (POST)', () => {\n    it('should create a user', () => {\n      return request(app.getHttpServer())\n        .post('/users')\n        .send({ name: 'John', email: 'john@test.com' })\n        .expect(201)\n        .expect(res => {\n          expect(res.body).toHaveProperty('id');\n          expect(res.body.name).toBe('John');\n          expect(res.body.email).toBe('john@test.com');\n        });\n    });\n\n    it('should return 400 for invalid email', () => {\n      return request(app.getHttpServer())\n        .post('/users')\n        .send({ name: 'John', email: 'invalid-email' })\n        .expect(400)\n        .expect(res => {\n          expect(res.body.message).toContain('email');\n        });\n    });\n  });\n\n  describe('/users/:id (GET)', () => {\n    it('should return 404 for non-existent user', () => {\n      return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);\n    });\n  });\n});\n\n// Testing with authentication\ndescribe('Protected Routes (e2e)', () => {\n  let app: INestApplication;\n  let authToken: string;\n\n  beforeAll(async () => {\n    const moduleFixture = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));\n    await app.init();\n\n    // Get auth token\n    const loginResponse = await request(app.getHttpServer())\n      .post('/auth/login')\n      .send({ email: 'test@test.com', password: 'password' });\n\n    authToken = loginResponse.body.accessToken;\n  });\n\n  it('should return 401 without token', () => {\n    return request(app.getHttpServer()).get('/users/me').expect(401);\n  });\n\n  it('should return user profile with valid token', () => {\n    return request(app.getHttpServer())\n      .get('/users/me')\n      .set('Authorization', `Bearer ${authToken}`)\n      .expect(200)\n      .expect(res => {\n        expect(res.body.email).toBe('test@test.com');\n      });\n  });\n});\n\n// Database isolation for E2E tests\ndescribe('Orders API (e2e)', () => {\n  let app: INestApplication;\n  let dataSource: DataSource;\n\n  beforeAll(async () => {\n    const moduleFixture = await Test.createTestingModule({\n      imports: [\n        ConfigModule.forRoot({\n          envFilePath: '.env.test', // Test database config\n        }),\n        AppModule,\n      ],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n    dataSource = moduleFixture.get(DataSource);\n    await app.init();\n  });\n\n  beforeEach(async () => {\n    // Clean database between tests\n    await dataSource.synchronize(true);\n  });\n\n  afterAll(async () => {\n    await dataSource.destroy();\n    await app.close();\n  });\n});\n```\n\nReference: [NestJS E2E Testing](https://docs.nestjs.com/fundamentals/testing#end-to-end-testing)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/test-mock-external-services.md",
    "content": "---\ntitle: Mock External Services in Tests\nimpact: HIGH\nimpactDescription: Ensures fast, reliable, deterministic tests\ntags: testing, mocking, external-services, jest\n---\n\n## Mock External Services in Tests\n\nNever call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors.\n\n**Incorrect (calling real APIs and databases):**\n\n```typescript\n// Call real APIs in tests\ndescribe('PaymentService', () => {\n  it('should process payment', async () => {\n    const service = new PaymentService(new StripeClient(realApiKey));\n    // Hits real Stripe API!\n    const result = await service.charge('tok_visa', 1000);\n    // Slow, costs money, flaky\n  });\n});\n\n// Use real database\ndescribe('UsersService', () => {\n  beforeEach(async () => {\n    await connection.query('DELETE FROM users'); // Modifies real DB\n  });\n\n  it('should create user', async () => {\n    const user = await service.create({ email: 'test@test.com' });\n    // Side effects on shared database\n  });\n});\n\n// Incomplete mocks\nconst mockHttpService = {\n  get: jest.fn().mockResolvedValue({ data: {} }),\n  // Missing error scenarios, missing other methods\n};\n```\n\n**Correct (mock all external dependencies):**\n\n```typescript\n// Mock HTTP service properly\ndescribe('WeatherService', () => {\n  let service: WeatherService;\n  let httpService: jest.Mocked<HttpService>;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [\n        WeatherService,\n        {\n          provide: HttpService,\n          useValue: {\n            get: jest.fn(),\n            post: jest.fn(),\n          },\n        },\n      ],\n    }).compile();\n\n    service = module.get(WeatherService);\n    httpService = module.get(HttpService);\n  });\n\n  it('should return weather data', async () => {\n    const mockResponse = {\n      data: { temperature: 72, humidity: 45 },\n      status: 200,\n      statusText: 'OK',\n      headers: {},\n      config: {},\n    };\n\n    httpService.get.mockReturnValue(of(mockResponse));\n\n    const result = await service.getWeather('NYC');\n\n    expect(result).toEqual({ temperature: 72, humidity: 45 });\n  });\n\n  it('should handle API timeout', async () => {\n    httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));\n\n    await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');\n  });\n\n  it('should handle rate limiting', async () => {\n    httpService.get.mockReturnValue(\n      throwError(() => ({\n        response: { status: 429, data: { message: 'Rate limited' } },\n      })),\n    );\n\n    await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);\n  });\n});\n\n// Mock repository instead of database\ndescribe('UsersService', () => {\n  let service: UsersService;\n  let repo: jest.Mocked<Repository<User>>;\n\n  beforeEach(async () => {\n    const mockRepo = {\n      find: jest.fn(),\n      findOne: jest.fn(),\n      save: jest.fn(),\n      delete: jest.fn(),\n      createQueryBuilder: jest.fn(),\n    };\n\n    const module = await Test.createTestingModule({\n      providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],\n    }).compile();\n\n    service = module.get(UsersService);\n    repo = module.get(getRepositoryToken(User));\n  });\n\n  it('should find user by id', async () => {\n    const mockUser = { id: '1', name: 'John', email: 'john@test.com' };\n    repo.findOne.mockResolvedValue(mockUser);\n\n    const result = await service.findById('1');\n\n    expect(result).toEqual(mockUser);\n    expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });\n  });\n});\n\n// Create mock factory for complex SDKs\nfunction createMockStripe(): jest.Mocked<Stripe> {\n  return {\n    paymentIntents: {\n      create: jest.fn(),\n      retrieve: jest.fn(),\n      confirm: jest.fn(),\n      cancel: jest.fn(),\n    },\n    customers: {\n      create: jest.fn(),\n      retrieve: jest.fn(),\n    },\n  } as any;\n}\n\n// Mock time for time-dependent tests\ndescribe('TokenService', () => {\n  beforeEach(() => {\n    jest.useFakeTimers();\n    jest.setSystemTime(new Date('2024-01-15'));\n  });\n\n  afterEach(() => {\n    jest.useRealTimers();\n  });\n\n  it('should expire token after 1 hour', async () => {\n    const token = await service.createToken();\n\n    // Fast-forward time\n    jest.advanceTimersByTime(61 * 60 * 1000);\n\n    expect(await service.isValid(token)).toBe(false);\n  });\n});\n```\n\nReference: [Jest Mocking](https://jestjs.io/docs/mock-functions)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/rules/test-use-testing-module.md",
    "content": "---\ntitle: Use Testing Module for Unit Tests\nimpact: HIGH\nimpactDescription: Enables proper isolated testing with mocked dependencies\ntags: testing, unit-tests, mocking, jest\n---\n\n## Use Testing Module for Unit Tests\n\nUse `@nestjs/testing` module to create isolated test environments with mocked dependencies. This ensures your tests run fast, don't depend on external services, and properly test your business logic in isolation.\n\n**Incorrect (manual instantiation bypassing DI):**\n\n```typescript\n// Instantiate services manually without DI\ndescribe('UsersService', () => {\n  it('should create user', async () => {\n    // Manual instantiation bypasses DI\n    const repo = new UserRepository(); // Real repo!\n    const service = new UsersService(repo);\n\n    const user = await service.create({ name: 'Test' });\n    // This hits the real database!\n  });\n});\n\n// Test implementation details\ndescribe('UsersController', () => {\n  it('should call service', async () => {\n    const service = { create: jest.fn() };\n    const controller = new UsersController(service as any);\n\n    await controller.create({ name: 'Test' });\n\n    expect(service.create).toHaveBeenCalled(); // Tests implementation, not behavior\n  });\n});\n```\n\n**Correct (use Test.createTestingModule with mocked dependencies):**\n\n```typescript\n// Use Test.createTestingModule for proper DI\nimport { Test, TestingModule } from '@nestjs/testing';\n\ndescribe('UsersService', () => {\n  let service: UsersService;\n  let repo: jest.Mocked<UserRepository>;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        UsersService,\n        {\n          provide: UserRepository,\n          useValue: {\n            save: jest.fn(),\n            findOne: jest.fn(),\n            find: jest.fn(),\n          },\n        },\n      ],\n    }).compile();\n\n    service = module.get<UsersService>(UsersService);\n    repo = module.get(UserRepository);\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe('create', () => {\n    it('should save and return user', async () => {\n      const dto = { name: 'John', email: 'john@test.com' };\n      const expectedUser = { id: '1', ...dto };\n\n      repo.save.mockResolvedValue(expectedUser);\n\n      const result = await service.create(dto);\n\n      expect(result).toEqual(expectedUser);\n      expect(repo.save).toHaveBeenCalledWith(dto);\n    });\n\n    it('should throw on duplicate email', async () => {\n      repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });\n\n      await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(ConflictException);\n    });\n  });\n\n  describe('findById', () => {\n    it('should return user when found', async () => {\n      const user = { id: '1', name: 'John' };\n      repo.findOne.mockResolvedValue(user);\n\n      const result = await service.findById('1');\n\n      expect(result).toEqual(user);\n    });\n\n    it('should throw NotFoundException when not found', async () => {\n      repo.findOne.mockResolvedValue(null);\n\n      await expect(service.findById('999')).rejects.toThrow(NotFoundException);\n    });\n  });\n});\n\n// Testing guards and interceptors\ndescribe('RolesGuard', () => {\n  let guard: RolesGuard;\n  let reflector: Reflector;\n\n  beforeEach(async () => {\n    const module = await Test.createTestingModule({\n      providers: [RolesGuard, Reflector],\n    }).compile();\n\n    guard = module.get<RolesGuard>(RolesGuard);\n    reflector = module.get<Reflector>(Reflector);\n  });\n\n  it('should allow when no roles required', () => {\n    const context = createMockExecutionContext({ user: { roles: [] } });\n    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);\n\n    expect(guard.canActivate(context)).toBe(true);\n  });\n\n  it('should allow admin for admin-only route', () => {\n    const context = createMockExecutionContext({ user: { roles: ['admin'] } });\n    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);\n\n    expect(guard.canActivate(context)).toBe(true);\n  });\n});\n\nfunction createMockExecutionContext(request: Partial<Request>): ExecutionContext {\n  return {\n    switchToHttp: () => ({\n      getRequest: () => request,\n    }),\n    getHandler: () => jest.fn(),\n    getClass: () => jest.fn(),\n  } as ExecutionContext;\n}\n```\n\nReference: [NestJS Testing](https://docs.nestjs.com/fundamentals/testing)"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/scripts/build-agents.ts",
    "content": "#!/usr/bin/env npx ts-node\n\n/**\n * Build script for generating AGENTS.md from individual rule files\n *\n * Usage: npx ts-node scripts/build-agents.ts\n *\n * This script:\n * 1. Reads all rule files from the rules/ directory\n * 2. Parses YAML frontmatter for metadata\n * 3. Groups rules by category based on filename prefix\n * 4. Generates a consolidated AGENTS.md file\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { fileURLToPath } from 'url';\nimport { dirname } from 'path';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n// Category definitions with ordering and metadata\nconst CATEGORIES = [\n  { prefix: 'arch-', name: 'Architecture', impact: 'CRITICAL', section: 1 },\n  { prefix: 'di-', name: 'Dependency Injection', impact: 'CRITICAL', section: 2 },\n  { prefix: 'error-', name: 'Error Handling', impact: 'HIGH', section: 3 },\n  { prefix: 'security-', name: 'Security', impact: 'HIGH', section: 4 },\n  { prefix: 'perf-', name: 'Performance', impact: 'HIGH', section: 5 },\n  { prefix: 'test-', name: 'Testing', impact: 'MEDIUM-HIGH', section: 6 },\n  { prefix: 'db-', name: 'Database & ORM', impact: 'MEDIUM-HIGH', section: 7 },\n  { prefix: 'api-', name: 'API Design', impact: 'MEDIUM', section: 8 },\n  { prefix: 'micro-', name: 'Microservices', impact: 'MEDIUM', section: 9 },\n  { prefix: 'devops-', name: 'DevOps & Deployment', impact: 'LOW-MEDIUM', section: 10 },\n];\n\ninterface RuleFrontmatter {\n  title: string;\n  impact: string;\n  impactDescription: string;\n  tags: string[];\n}\n\ninterface Rule {\n  filename: string;\n  frontmatter: RuleFrontmatter;\n  content: string;\n  category: string;\n  categorySection: number;\n}\n\nfunction parseFrontmatter(content: string): { frontmatter: RuleFrontmatter | null; body: string } {\n  const frontmatterRegex = /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/;\n  const match = content.match(frontmatterRegex);\n\n  if (!match) {\n    return { frontmatter: null, body: content };\n  }\n\n  const frontmatterStr = match[1];\n  const body = match[2];\n\n  // Simple YAML parsing for our expected format\n  const frontmatter: Partial<RuleFrontmatter> = {};\n  const lines = frontmatterStr.split('\\n');\n  let currentKey = '';\n  let inArray = false;\n  const arrayItems: string[] = [];\n\n  for (const line of lines) {\n    if (line.match(/^[a-zA-Z]+:/)) {\n      // Save previous array if we were collecting one\n      if (inArray && currentKey === 'tags') {\n        frontmatter.tags = arrayItems;\n      }\n      inArray = false;\n      arrayItems.length = 0;\n\n      const [key, ...valueParts] = line.split(':');\n      const value = valueParts.join(':').trim();\n      currentKey = key.trim();\n\n      if (value === '') {\n        // Might be start of array\n        inArray = true;\n      } else {\n        (frontmatter as any)[currentKey] = value;\n      }\n    } else if (inArray && line.trim().startsWith('-')) {\n      arrayItems.push(line.trim().replace(/^-\\s*/, ''));\n    }\n  }\n\n  // Save final array if needed\n  if (inArray && currentKey === 'tags') {\n    frontmatter.tags = arrayItems;\n  }\n\n  return {\n    frontmatter: frontmatter as RuleFrontmatter,\n    body: body.trim(),\n  };\n}\n\nfunction getCategoryForFile(filename: string): { name: string; section: number } | null {\n  for (const cat of CATEGORIES) {\n    if (filename.startsWith(cat.prefix)) {\n      return { name: cat.name, section: cat.section };\n    }\n  }\n  return null;\n}\n\nfunction readMetadata(): any {\n  const metadataPath = path.join(__dirname, '..', 'metadata.json');\n  return JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));\n}\n\nfunction readRules(): Rule[] {\n  const rulesDir = path.join(__dirname, '..', 'rules');\n  const files = fs.readdirSync(rulesDir).filter(f => f.endsWith('.md') && !f.startsWith('_'));\n\n  const rules: Rule[] = [];\n\n  for (const file of files) {\n    const filePath = path.join(rulesDir, file);\n    const content = fs.readFileSync(filePath, 'utf-8');\n    const { frontmatter, body } = parseFrontmatter(content);\n\n    if (!frontmatter) {\n      console.warn(`Warning: No frontmatter found in ${file}`);\n      continue;\n    }\n\n    const category = getCategoryForFile(file);\n    if (!category) {\n      console.warn(`Warning: Unknown category for ${file}`);\n      continue;\n    }\n\n    rules.push({\n      filename: file,\n      frontmatter,\n      content: body,\n      category: category.name,\n      categorySection: category.section,\n    });\n  }\n\n  return rules;\n}\n\nfunction generateTableOfContents(rulesByCategory: Map<string, Rule[]>): string {\n  let toc = '## Table of Contents\\n\\n';\n\n  for (const cat of CATEGORIES) {\n    const rules = rulesByCategory.get(cat.name);\n    if (!rules || rules.length === 0) continue;\n\n    // Section anchor format: #1-architecture\n    const sectionAnchor = `${cat.section}-${cat.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;\n    toc += `${cat.section}. [${cat.name}](#${sectionAnchor}) — **${cat.impact}**\\n`;\n\n    for (let i = 0; i < rules.length; i++) {\n      const rule = rules[i];\n      // Rule anchor format: #11-rule-title\n      const ruleNum = `${cat.section}${i + 1}`;\n      const anchor = `${ruleNum}-${rule.frontmatter.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`;\n      toc += `   - ${cat.section}.${i + 1} [${rule.frontmatter.title}](#${anchor})\\n`;\n    }\n  }\n\n  return toc;\n}\n\nfunction generateAgentsMd(rules: Rule[], metadata: any): string {\n  // Group rules by category\n  const rulesByCategory = new Map<string, Rule[]>();\n\n  for (const rule of rules) {\n    if (!rulesByCategory.has(rule.category)) {\n      rulesByCategory.set(rule.category, []);\n    }\n    rulesByCategory.get(rule.category)!.push(rule);\n  }\n\n  // Sort rules within each category alphabetically\n  for (const [category, categoryRules] of rulesByCategory) {\n    categoryRules.sort((a, b) => a.filename.localeCompare(b.filename));\n  }\n\n  // Build document\n  let doc = `# NestJS Best Practices\n\n**Version ${metadata.version}**\n${metadata.organization}\n${metadata.date}\n\n> **Note:**\n> This document is mainly for agents and LLMs to follow when maintaining,\n> generating, or refactoring NestJS codebases. Humans may also find it\n> useful, but guidance here is optimized for automation and consistency\n> by AI-assisted workflows.\n\n---\n\n## Abstract\n\n${metadata.abstract}\n\n---\n\n`;\n\n  // Add table of contents\n  doc += generateTableOfContents(rulesByCategory);\n  doc += '\\n---\\n\\n';\n\n  // Add rules by category\n  for (const cat of CATEGORIES) {\n    const categoryRules = rulesByCategory.get(cat.name);\n    if (!categoryRules || categoryRules.length === 0) continue;\n\n    doc += `## ${cat.section}. ${cat.name}\\n\\n`;\n    doc += `**Section Impact: ${cat.impact}**\\n\\n`;\n\n    for (let i = 0; i < categoryRules.length; i++) {\n      const rule = categoryRules[i];\n      const ruleNumber = `${cat.section}.${i + 1}`;\n\n      // Add rule header with number (anchor will be auto-generated as #11-title)\n      doc += `### ${ruleNumber} ${rule.frontmatter.title}\\n\\n`;\n      doc += `**Impact: ${rule.frontmatter.impact}** — ${rule.frontmatter.impactDescription}\\n\\n`;\n\n      // Add rule content (skip the first header since we already added it)\n      let ruleContent = rule.content;\n      // Remove the first h1 or h2 header if it matches the title\n      ruleContent = ruleContent.replace(/^#{1,2}\\s+.*\\n+/, '');\n      // Remove the impact line if present (we already added it)\n      ruleContent = ruleContent.replace(/^\\*\\*Impact:.*\\*\\*.*\\n+/, '');\n\n      doc += ruleContent;\n      doc += '\\n\\n---\\n\\n';\n    }\n  }\n\n  // Add references footer\n  doc += `## References\n\n`;\n  for (const ref of metadata.references) {\n    doc += `- ${ref}\\n`;\n  }\n\n  doc += `\n---\n\n*Generated by build-agents.ts on ${new Date().toISOString().split('T')[0]}*\n`;\n\n  return doc;\n}\n\nfunction main() {\n  console.log('Building AGENTS.md...\\n');\n\n  const metadata = readMetadata();\n  console.log(`Version: ${metadata.version}`);\n  console.log(`Organization: ${metadata.organization}\\n`);\n\n  const rules = readRules();\n  console.log(`Found ${rules.length} rules\\n`);\n\n  // Count by category\n  const counts = new Map<string, number>();\n  for (const rule of rules) {\n    counts.set(rule.category, (counts.get(rule.category) || 0) + 1);\n  }\n\n  console.log('Rules by category:');\n  for (const cat of CATEGORIES) {\n    const count = counts.get(cat.name) || 0;\n    if (count > 0) {\n      console.log(`  ${cat.name}: ${count}`);\n    }\n  }\n  console.log('');\n\n  const agentsMd = generateAgentsMd(rules, metadata);\n\n  const outputPath = path.join(__dirname, '..', 'AGENTS.md');\n  fs.writeFileSync(outputPath, agentsMd);\n\n  console.log(`Generated AGENTS.md (${agentsMd.length} bytes)`);\n  console.log(`Output: ${outputPath}`);\n}\n\nmain();\n"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/scripts/build.sh",
    "content": "#!/bin/bash\n\n# Build script for generating AGENTS.md\n# Usage: ./build.sh\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$SCRIPT_DIR\"\n\n# Check if ts-node is available\nif command -v npx &> /dev/null; then\n    echo \"Running build with ts-node...\"\n    npx ts-node build-agents.ts\nelse\n    echo \"Error: npx not found. Please install Node.js.\"\n    exit 1\nfi\n"
  },
  {
    "path": ".agents/skills/nestjs-best-practices/scripts/package.json",
    "content": "{\n  \"name\": \"nestjs-best-practices-scripts\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"description\": \"Build scripts for NestJS Best Practices skillset\",\n  \"scripts\": {\n    \"build\": \"npx ts-node build-agents.ts\",\n    \"build:watch\": \"npx nodemon --watch ../rules --ext md --exec 'npx ts-node build-agents.ts'\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.0.0\",\n    \"ts-node\": \"^10.9.0\",\n    \"@types/node\": \"^20.0.0\"\n  }\n}\n"
  },
  {
    "path": ".agents/skills/typeorm/SKILL.md",
    "content": "---\nname: typeorm\ndescription: Guidelines for developing with TypeORM, a full-featured ORM for TypeScript and JavaScript supporting multiple databases\n---\n\n# TypeORM Development Guidelines\n\nYou are an expert in TypeORM, TypeScript, and database design with a focus on the Data Mapper pattern and enterprise application architecture.\n\n## Core Principles\n\n- TypeORM supports both Active Record and Data Mapper patterns\n- Uses TypeScript decorators for entity and column definitions\n- Supports MySQL, PostgreSQL, MariaDB, SQLite, MS SQL Server, Oracle, and more\n- Works in Node.js, Browser, Ionic, Cordova, React Native, NativeScript, Expo, and Electron\n- First-class support for database migrations\n\n## TypeScript Configuration\n\nRequired settings in tsconfig.json:\n\n```json\n{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"strict\": true,\n    \"target\": \"ES2020\",\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\"\n  }\n}\n```\n\n## Entity Definition\n\n### Basic Entity\n\n```typescript\nimport { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';\n\n@Entity('users')\nexport class User {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column({ type: 'varchar', length: 255, unique: true })\n  email: string;\n\n  @Column({ type: 'varchar', length: 255, nullable: true })\n  name: string | null;\n\n  @Column({ type: 'boolean', default: true })\n  isActive: boolean;\n\n  @CreateDateColumn()\n  createdAt: Date;\n\n  @UpdateDateColumn()\n  updatedAt: Date;\n}\n```\n\n### Primary Key Options\n\n```typescript\n// Auto-increment\n@PrimaryGeneratedColumn()\nid: number;\n\n// UUID\n@PrimaryGeneratedColumn(\"uuid\")\nid: string;\n\n// Custom primary key\n@PrimaryColumn()\nid: string;\n\n// Composite primary key\n@Entity()\nexport class OrderItem {\n  @PrimaryColumn()\n  orderId: number;\n\n  @PrimaryColumn()\n  productId: number;\n}\n```\n\n### Column Decorators\n\n```typescript\n@Entity()\nexport class Product {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  // String columns\n  @Column({ type: 'varchar', length: 255 })\n  name: string;\n\n  @Column({ type: 'text', nullable: true })\n  description: string | null;\n\n  // Numeric columns\n  @Column({ type: 'decimal', precision: 10, scale: 2 })\n  price: number;\n\n  @Column({ type: 'int', default: 0 })\n  stock: number;\n\n  // Boolean\n  @Column({ type: 'boolean', default: true })\n  isAvailable: boolean;\n\n  // JSON\n  @Column({ type: 'jsonb', nullable: true })\n  metadata: Record<string, any> | null;\n\n  // Enum\n  @Column({\n    type: 'enum',\n    enum: ['active', 'inactive', 'pending'],\n    default: 'pending',\n  })\n  status: 'active' | 'inactive' | 'pending';\n\n  // Timestamps\n  @CreateDateColumn()\n  createdAt: Date;\n\n  @UpdateDateColumn()\n  updatedAt: Date;\n\n  @DeleteDateColumn()\n  deletedAt: Date | null; // For soft deletes\n\n  // Version column for optimistic locking\n  @VersionColumn()\n  version: number;\n}\n```\n\n## Relationships\n\n### One-to-One\n\n```typescript\n@Entity()\nexport class User {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @OneToOne(() => Profile, profile => profile.user, { cascade: true })\n  @JoinColumn()\n  profile: Profile;\n}\n\n@Entity()\nexport class Profile {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  bio: string;\n\n  @OneToOne(() => User, user => user.profile)\n  user: User;\n}\n```\n\n### One-to-Many / Many-to-One\n\n```typescript\n@Entity()\nexport class User {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @OneToMany(() => Post, post => post.author)\n  posts: Post[];\n}\n\n@Entity()\nexport class Post {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  title: string;\n\n  @ManyToOne(() => User, user => user.posts, { onDelete: 'CASCADE' })\n  @JoinColumn({ name: 'author_id' })\n  author: User;\n\n  @Column()\n  authorId: number; // Explicit foreign key column\n}\n```\n\n### Many-to-Many\n\n```typescript\n@Entity()\nexport class Post {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  title: string;\n\n  @ManyToMany(() => Tag, tag => tag.posts)\n  @JoinTable({\n    name: 'post_tags',\n    joinColumn: { name: 'post_id' },\n    inverseJoinColumn: { name: 'tag_id' },\n  })\n  tags: Tag[];\n}\n\n@Entity()\nexport class Tag {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column({ unique: true })\n  name: string;\n\n  @ManyToMany(() => Post, post => post.tags)\n  posts: Post[];\n}\n```\n\n## Repository Pattern\n\n### Basic Repository Usage\n\n```typescript\nimport { AppDataSource } from './data-source';\nimport { User } from './entities/User';\n\nconst userRepository = AppDataSource.getRepository(User);\n\n// Find all\nconst users = await userRepository.find();\n\n// Find with conditions\nconst activeUsers = await userRepository.find({\n  where: { isActive: true },\n});\n\n// Find one\nconst user = await userRepository.findOne({\n  where: { id: 1 },\n});\n\n// Find or fail\nconst user = await userRepository.findOneOrFail({\n  where: { id: 1 },\n});\n\n// Save\nconst newUser = userRepository.create({\n  email: 'user@example.com',\n  name: 'John Doe',\n});\nawait userRepository.save(newUser);\n\n// Update\nawait userRepository.update({ id: 1 }, { name: 'Jane Doe' });\n\n// Delete\nawait userRepository.delete({ id: 1 });\n\n// Soft delete (requires @DeleteDateColumn)\nawait userRepository.softDelete({ id: 1 });\n```\n\n### Custom Repository\n\n```typescript\nimport { Repository, DataSource } from 'typeorm';\nimport { User } from './entities/User';\n\nexport class UserRepository extends Repository<User> {\n  constructor(private dataSource: DataSource) {\n    super(User, dataSource.createEntityManager());\n  }\n\n  async findByEmail(email: string): Promise<User | null> {\n    return this.findOne({ where: { email } });\n  }\n\n  async findActiveUsers(): Promise<User[]> {\n    return this.find({\n      where: { isActive: true },\n      order: { createdAt: 'DESC' },\n    });\n  }\n\n  async findWithPosts(userId: number): Promise<User | null> {\n    return this.findOne({\n      where: { id: userId },\n      relations: ['posts'],\n    });\n  }\n}\n```\n\n### Query Builder\n\n```typescript\nconst users = await userRepository\n  .createQueryBuilder('user')\n  .leftJoinAndSelect('user.posts', 'post')\n  .where('user.isActive = :isActive', { isActive: true })\n  .andWhere('post.publishedAt IS NOT NULL')\n  .orderBy('user.createdAt', 'DESC')\n  .skip(0)\n  .take(10)\n  .getMany();\n\n// With raw results\nconst result = await userRepository\n  .createQueryBuilder('user')\n  .select('COUNT(*)', 'count')\n  .where('user.isActive = :isActive', { isActive: true })\n  .getRawOne();\n\n// Insert with query builder\nawait userRepository\n  .createQueryBuilder()\n  .insert()\n  .into(User)\n  .values([\n    { email: 'user1@example.com', name: 'User 1' },\n    { email: 'user2@example.com', name: 'User 2' },\n  ])\n  .execute();\n```\n\n## Data Source Configuration\n\n```typescript\n// data-source.ts\nimport { DataSource } from 'typeorm';\nimport { User } from './entities/User';\nimport { Post } from './entities/Post';\n\nexport const AppDataSource = new DataSource({\n  type: 'postgres',\n  host: process.env.DB_HOST || 'localhost',\n  port: parseInt(process.env.DB_PORT || '5432'),\n  username: process.env.DB_USERNAME,\n  password: process.env.DB_PASSWORD,\n  database: process.env.DB_NAME,\n\n  // Entity configuration\n  entities: [User, Post],\n  // Or use glob pattern: entities: [\"src/entities/**/*.ts\"]\n\n  // Migrations\n  migrations: ['src/migrations/**/*.ts'],\n\n  // Synchronize - NEVER use in production\n  synchronize: false,\n\n  // Logging\n  logging: process.env.NODE_ENV === 'development',\n\n  // Connection pool\n  poolSize: 10,\n\n  // SSL (for production)\n  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,\n});\n\n// Initialize connection\nAppDataSource.initialize()\n  .then(() => console.log('Data Source initialized'))\n  .catch(error => console.error('Error initializing Data Source:', error));\n```\n\n## Migrations\n\n### Creating Migrations\n\n```bash\n# Generate migration from entity changes\nnpx typeorm migration:generate src/migrations/CreateUsers -d src/data-source.ts\n\n# Create empty migration\nnpx typeorm migration:create src/migrations/SeedUsers\n\n# Run migrations\nnpx typeorm migration:run -d src/data-source.ts\n\n# Revert last migration\nnpx typeorm migration:revert -d src/data-source.ts\n```\n\n### Migration File Structure\n\n```typescript\nimport { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';\n\nexport class CreateUsers1234567890 implements MigrationInterface {\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.createTable(\n      new Table({\n        name: 'users',\n        columns: [\n          {\n            name: 'id',\n            type: 'int',\n            isPrimary: true,\n            isGenerated: true,\n            generationStrategy: 'increment',\n          },\n          {\n            name: 'email',\n            type: 'varchar',\n            length: '255',\n            isUnique: true,\n          },\n          {\n            name: 'name',\n            type: 'varchar',\n            length: '255',\n            isNullable: true,\n          },\n          {\n            name: 'is_active',\n            type: 'boolean',\n            default: true,\n          },\n          {\n            name: 'created_at',\n            type: 'timestamp',\n            default: 'CURRENT_TIMESTAMP',\n          },\n          {\n            name: 'updated_at',\n            type: 'timestamp',\n            default: 'CURRENT_TIMESTAMP',\n            onUpdate: 'CURRENT_TIMESTAMP',\n          },\n        ],\n      }),\n      true,\n    );\n\n    await queryRunner.createIndex(\n      'users',\n      new TableIndex({\n        name: 'IDX_USERS_EMAIL',\n        columnNames: ['email'],\n      }),\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.dropIndex('users', 'IDX_USERS_EMAIL');\n    await queryRunner.dropTable('users');\n  }\n}\n```\n\n## Transactions\n\n```typescript\n// Using QueryRunner\nconst queryRunner = AppDataSource.createQueryRunner();\nawait queryRunner.connect();\nawait queryRunner.startTransaction();\n\ntry {\n  const user = queryRunner.manager.create(User, {\n    email: 'user@example.com',\n    name: 'User',\n  });\n  await queryRunner.manager.save(user);\n\n  const post = queryRunner.manager.create(Post, {\n    title: 'First Post',\n    author: user,\n  });\n  await queryRunner.manager.save(post);\n\n  await queryRunner.commitTransaction();\n} catch (error) {\n  await queryRunner.rollbackTransaction();\n  throw error;\n} finally {\n  await queryRunner.release();\n}\n\n// Using transaction method\nawait AppDataSource.transaction(async manager => {\n  const user = manager.create(User, {\n    email: 'user@example.com',\n    name: 'User',\n  });\n  await manager.save(user);\n\n  const post = manager.create(Post, {\n    title: 'First Post',\n    author: user,\n  });\n  await manager.save(post);\n});\n```\n\n## NestJS Integration\n\n```typescript\n// app.module.ts\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { User } from './entities/user.entity';\nimport { UsersModule } from './users/users.module';\n\n@Module({\n  imports: [\n    TypeOrmModule.forRoot({\n      type: 'postgres',\n      host: 'localhost',\n      port: 5432,\n      username: 'user',\n      password: 'password',\n      database: 'db',\n      entities: [User],\n      synchronize: false,\n    }),\n    UsersModule,\n  ],\n})\nexport class AppModule {}\n\n// users/users.module.ts\n@Module({\n  imports: [TypeOrmModule.forFeature([User])],\n  providers: [UsersService],\n  controllers: [UsersController],\n})\nexport class UsersModule {}\n\n// users/users.service.ts\n@Injectable()\nexport class UsersService {\n  constructor(\n    @InjectRepository(User)\n    private usersRepository: Repository<User>,\n  ) {}\n\n  findAll(): Promise<User[]> {\n    return this.usersRepository.find();\n  }\n\n  findOne(id: number): Promise<User | null> {\n    return this.usersRepository.findOneBy({ id });\n  }\n}\n```\n\n## Best Practices\n\n### Use Migrations in Production\n\nNever use `synchronize: true` in production. Always use migrations:\n\n```typescript\n// Development: Use migrations, not sync\nsynchronize: false,\n```\n\n### Eager vs Lazy Loading\n\n```typescript\n// Eager loading - loads relations automatically\n@OneToMany(() => Post, (post) => post.author, { eager: true })\nposts: Post[];\n\n// Lazy loading - loads relations on access\n@OneToMany(() => Post, (post) => post.author)\nposts: Promise<Post[]>;\n\n// Explicit loading (recommended)\nconst user = await userRepository.findOne({\n  where: { id: 1 },\n  relations: [\"posts\"],\n});\n```\n\n### Avoid N+1 Queries\n\n```typescript\n// Bad: N+1 queries\nconst users = await userRepository.find();\nfor (const user of users) {\n  console.log(user.posts); // Separate query for each user\n}\n\n// Good: Eager load relations\nconst users = await userRepository.find({\n  relations: ['posts'],\n});\n```\n\n### Use Indexes\n\n```typescript\n@Entity()\n@Index(['email'])\n@Index(['firstName', 'lastName'])\nexport class User {\n  @Column()\n  @Index()\n  email: string;\n\n  @Column()\n  firstName: string;\n\n  @Column()\n  lastName: string;\n}\n```\n\n### Cascade Operations\n\n```typescript\n@OneToMany(() => Post, (post) => post.author, {\n  cascade: true, // Saves/removes related posts\n  onDelete: \"CASCADE\", // Database-level cascade\n})\nposts: Post[];\n```\n\n### Naming Strategies\n\nFor consistent naming between TypeScript and database:\n\n```typescript\nimport { DefaultNamingStrategy, NamingStrategyInterface } from \"typeorm\";\nimport { snakeCase } from \"typeorm/util/StringUtils\";\n\nexport class SnakeNamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {\n  tableName(targetName: string, userSpecifiedName: string | undefined): string {\n    return userSpecifiedName ? userSpecifiedName : snakeCase(targetName);\n  }\n\n  columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string {\n    return snakeCase(embeddedPrefixes.join(\"_\")) + (customName ? customName : snakeCase(propertyName));\n  }\n}\n\n// Use in data source config\nnamingStrategy: new SnakeNamingStrategy(),\n```"
  },
  {
    "path": ".agents/skills/typescript-advanced-types/SKILL.md",
    "content": "---\nname: typescript-advanced-types\ndescription: Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects.\n---\n\n# TypeScript Advanced Types\n\nComprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.\n\n## When to Use This Skill\n\n- Building type-safe libraries or frameworks\n- Creating reusable generic components\n- Implementing complex type inference logic\n- Designing type-safe API clients\n- Building form validation systems\n- Creating strongly-typed configuration objects\n- Implementing type-safe state management\n- Migrating JavaScript codebases to TypeScript\n\n## Core Concepts\n\n### 1. Generics\n\n**Purpose:** Create reusable, type-flexible components while maintaining type safety.\n\n**Basic Generic Function:**\n\n```typescript\nfunction identity<T>(value: T): T {\n  return value;\n}\n\nconst num = identity<number>(42); // Type: number\nconst str = identity<string>('hello'); // Type: string\nconst auto = identity(true); // Type inferred: boolean\n```\n\n**Generic Constraints:**\n\n```typescript\ninterface HasLength {\n  length: number;\n}\n\nfunction logLength<T extends HasLength>(item: T): T {\n  console.log(item.length);\n  return item;\n}\n\nlogLength('hello'); // OK: string has length\nlogLength([1, 2, 3]); // OK: array has length\nlogLength({ length: 10 }); // OK: object has length\n// logLength(42);             // Error: number has no length\n```\n\n**Multiple Type Parameters:**\n\n```typescript\nfunction merge<T, U>(obj1: T, obj2: U): T & U {\n  return { ...obj1, ...obj2 };\n}\n\nconst merged = merge({ name: 'John' }, { age: 30 });\n// Type: { name: string } & { age: number }\n```\n\n### 2. Conditional Types\n\n**Purpose:** Create types that depend on conditions, enabling sophisticated type logic.\n\n**Basic Conditional Type:**\n\n```typescript\ntype IsString<T> = T extends string ? true : false;\n\ntype A = IsString<string>; // true\ntype B = IsString<number>; // false\n```\n\n**Extracting Return Types:**\n\n```typescript\ntype ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;\n\nfunction getUser() {\n  return { id: 1, name: 'John' };\n}\n\ntype User = ReturnType<typeof getUser>;\n// Type: { id: number; name: string; }\n```\n\n**Distributive Conditional Types:**\n\n```typescript\ntype ToArray<T> = T extends any ? T[] : never;\n\ntype StrOrNumArray = ToArray<string | number>;\n// Type: string[] | number[]\n```\n\n**Nested Conditions:**\n\n```typescript\ntype TypeName<T> = T extends string\n  ? 'string'\n  : T extends number\n    ? 'number'\n    : T extends boolean\n      ? 'boolean'\n      : T extends undefined\n        ? 'undefined'\n        : T extends Function\n          ? 'function'\n          : 'object';\n\ntype T1 = TypeName<string>; // \"string\"\ntype T2 = TypeName<() => void>; // \"function\"\n```\n\n### 3. Mapped Types\n\n**Purpose:** Transform existing types by iterating over their properties.\n\n**Basic Mapped Type:**\n\n```typescript\ntype Readonly<T> = {\n  readonly [P in keyof T]: T[P];\n};\n\ninterface User {\n  id: number;\n  name: string;\n}\n\ntype ReadonlyUser = Readonly<User>;\n// Type: { readonly id: number; readonly name: string; }\n```\n\n**Optional Properties:**\n\n```typescript\ntype Partial<T> = {\n  [P in keyof T]?: T[P];\n};\n\ntype PartialUser = Partial<User>;\n// Type: { id?: number; name?: string; }\n```\n\n**Key Remapping:**\n\n```typescript\ntype Getters<T> = {\n  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];\n};\n\ninterface Person {\n  name: string;\n  age: number;\n}\n\ntype PersonGetters = Getters<Person>;\n// Type: { getName: () => string; getAge: () => number; }\n```\n\n**Filtering Properties:**\n\n```typescript\ntype PickByType<T, U> = {\n  [K in keyof T as T[K] extends U ? K : never]: T[K];\n};\n\ninterface Mixed {\n  id: number;\n  name: string;\n  age: number;\n  active: boolean;\n}\n\ntype OnlyNumbers = PickByType<Mixed, number>;\n// Type: { id: number; age: number; }\n```\n\n### 4. Template Literal Types\n\n**Purpose:** Create string-based types with pattern matching and transformation.\n\n**Basic Template Literal:**\n\n```typescript\ntype EventName = 'click' | 'focus' | 'blur';\ntype EventHandler = `on${Capitalize<EventName>}`;\n// Type: \"onClick\" | \"onFocus\" | \"onBlur\"\n```\n\n**String Manipulation:**\n\n```typescript\ntype UppercaseGreeting = Uppercase<'hello'>; // \"HELLO\"\ntype LowercaseGreeting = Lowercase<'HELLO'>; // \"hello\"\ntype CapitalizedName = Capitalize<'john'>; // \"John\"\ntype UncapitalizedName = Uncapitalize<'John'>; // \"john\"\n```\n\n**Path Building:**\n\n```typescript\ntype Path<T> = T extends object\n  ? {\n      [K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;\n    }[keyof T]\n  : never;\n\ninterface Config {\n  server: {\n    host: string;\n    port: number;\n  };\n  database: {\n    url: string;\n  };\n}\n\ntype ConfigPath = Path<Config>;\n// Type: \"server\" | \"database\" | \"server.host\" | \"server.port\" | \"database.url\"\n```\n\n### 5. Utility Types\n\n**Built-in Utility Types:**\n\n```typescript\n// Partial<T> - Make all properties optional\ntype PartialUser = Partial<User>;\n\n// Required<T> - Make all properties required\ntype RequiredUser = Required<PartialUser>;\n\n// Readonly<T> - Make all properties readonly\ntype ReadonlyUser = Readonly<User>;\n\n// Pick<T, K> - Select specific properties\ntype UserName = Pick<User, 'name' | 'email'>;\n\n// Omit<T, K> - Remove specific properties\ntype UserWithoutPassword = Omit<User, 'password'>;\n\n// Exclude<T, U> - Exclude types from union\ntype T1 = Exclude<'a' | 'b' | 'c', 'a'>; // \"b\" | \"c\"\n\n// Extract<T, U> - Extract types from union\ntype T2 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // \"a\" | \"b\"\n\n// NonNullable<T> - Exclude null and undefined\ntype T3 = NonNullable<string | null | undefined>; // string\n\n// Record<K, T> - Create object type with keys K and values T\ntype PageInfo = Record<'home' | 'about', { title: string }>;\n```\n\n## Advanced Patterns\n\n### Pattern 1: Type-Safe Event Emitter\n\n```typescript\ntype EventMap = {\n  'user:created': { id: string; name: string };\n  'user:updated': { id: string };\n  'user:deleted': { id: string };\n};\n\nclass TypedEventEmitter<T extends Record<string, any>> {\n  private listeners: {\n    [K in keyof T]?: Array<(data: T[K]) => void>;\n  } = {};\n\n  on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event]!.push(callback);\n  }\n\n  emit<K extends keyof T>(event: K, data: T[K]): void {\n    const callbacks = this.listeners[event];\n    if (callbacks) {\n      callbacks.forEach(callback => callback(data));\n    }\n  }\n}\n\nconst emitter = new TypedEventEmitter<EventMap>();\n\nemitter.on('user:created', data => {\n  console.log(data.id, data.name); // Type-safe!\n});\n\nemitter.emit('user:created', { id: '1', name: 'John' });\n// emitter.emit(\"user:created\", { id: \"1\" });  // Error: missing 'name'\n```\n\n### Pattern 2: Type-Safe API Client\n\n```typescript\ntype HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\ntype EndpointConfig = {\n  '/users': {\n    GET: { response: User[] };\n    POST: { body: { name: string; email: string }; response: User };\n  };\n  '/users/:id': {\n    GET: { params: { id: string }; response: User };\n    PUT: { params: { id: string }; body: Partial<User>; response: User };\n    DELETE: { params: { id: string }; response: void };\n  };\n};\n\ntype ExtractParams<T> = T extends { params: infer P } ? P : never;\ntype ExtractBody<T> = T extends { body: infer B } ? B : never;\ntype ExtractResponse<T> = T extends { response: infer R } ? R : never;\n\nclass APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {\n  async request<Path extends keyof Config, Method extends keyof Config[Path]>(\n    path: Path,\n    method: Method,\n    ...[options]: ExtractParams<Config[Path][Method]> extends never\n      ? ExtractBody<Config[Path][Method]> extends never\n        ? []\n        : [{ body: ExtractBody<Config[Path][Method]> }]\n      : [\n          {\n            params: ExtractParams<Config[Path][Method]>;\n            body?: ExtractBody<Config[Path][Method]>;\n          },\n        ]\n  ): Promise<ExtractResponse<Config[Path][Method]>> {\n    // Implementation here\n    return {} as any;\n  }\n}\n\nconst api = new APIClient<EndpointConfig>();\n\n// Type-safe API calls\nconst users = await api.request('/users', 'GET');\n// Type: User[]\n\nconst newUser = await api.request('/users', 'POST', {\n  body: { name: 'John', email: 'john@example.com' },\n});\n// Type: User\n\nconst user = await api.request('/users/:id', 'GET', {\n  params: { id: '123' },\n});\n// Type: User\n```\n\n### Pattern 3: Builder Pattern with Type Safety\n\n```typescript\ntype BuilderState<T> = {\n  [K in keyof T]: T[K] | undefined;\n};\n\ntype RequiredKeys<T> = {\n  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;\n}[keyof T];\n\ntype OptionalKeys<T> = {\n  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;\n}[keyof T];\n\ntype IsComplete<T, S> = RequiredKeys<T> extends keyof S ? (S[RequiredKeys<T>] extends undefined ? false : true) : false;\n\nclass Builder<T, S extends BuilderState<T> = {}> {\n  private state: S = {} as S;\n\n  set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> {\n    this.state[key] = value;\n    return this as any;\n  }\n\n  build(this: IsComplete<T, S> extends true ? this : never): T {\n    return this.state as T;\n  }\n}\n\ninterface User {\n  id: string;\n  name: string;\n  email: string;\n  age?: number;\n}\n\nconst builder = new Builder<User>();\n\nconst user = builder.set('id', '1').set('name', 'John').set('email', 'john@example.com').build(); // OK: all required fields set\n\n// const incomplete = builder\n//   .set(\"id\", \"1\")\n//   .build();  // Error: missing required fields\n```\n\n### Pattern 4: Deep Readonly/Partial\n\n```typescript\ntype DeepReadonly<T> = {\n  readonly [P in keyof T]: T[P] extends object ? (T[P] extends Function ? T[P] : DeepReadonly<T[P]>) : T[P];\n};\n\ntype DeepPartial<T> = {\n  [P in keyof T]?: T[P] extends object\n    ? T[P] extends Array<infer U>\n      ? Array<DeepPartial<U>>\n      : DeepPartial<T[P]>\n    : T[P];\n};\n\ninterface Config {\n  server: {\n    host: string;\n    port: number;\n    ssl: {\n      enabled: boolean;\n      cert: string;\n    };\n  };\n  database: {\n    url: string;\n    pool: {\n      min: number;\n      max: number;\n    };\n  };\n}\n\ntype ReadonlyConfig = DeepReadonly<Config>;\n// All nested properties are readonly\n\ntype PartialConfig = DeepPartial<Config>;\n// All nested properties are optional\n```\n\n### Pattern 5: Type-Safe Form Validation\n\n```typescript\ntype ValidationRule<T> = {\n  validate: (value: T) => boolean;\n  message: string;\n};\n\ntype FieldValidation<T> = {\n  [K in keyof T]?: ValidationRule<T[K]>[];\n};\n\ntype ValidationErrors<T> = {\n  [K in keyof T]?: string[];\n};\n\nclass FormValidator<T extends Record<string, any>> {\n  constructor(private rules: FieldValidation<T>) {}\n\n  validate(data: T): ValidationErrors<T> | null {\n    const errors: ValidationErrors<T> = {};\n    let hasErrors = false;\n\n    for (const key in this.rules) {\n      const fieldRules = this.rules[key];\n      const value = data[key];\n\n      if (fieldRules) {\n        const fieldErrors: string[] = [];\n\n        for (const rule of fieldRules) {\n          if (!rule.validate(value)) {\n            fieldErrors.push(rule.message);\n          }\n        }\n\n        if (fieldErrors.length > 0) {\n          errors[key] = fieldErrors;\n          hasErrors = true;\n        }\n      }\n    }\n\n    return hasErrors ? errors : null;\n  }\n}\n\ninterface LoginForm {\n  email: string;\n  password: string;\n}\n\nconst validator = new FormValidator<LoginForm>({\n  email: [\n    {\n      validate: v => v.includes('@'),\n      message: 'Email must contain @',\n    },\n    {\n      validate: v => v.length > 0,\n      message: 'Email is required',\n    },\n  ],\n  password: [\n    {\n      validate: v => v.length >= 8,\n      message: 'Password must be at least 8 characters',\n    },\n  ],\n});\n\nconst errors = validator.validate({\n  email: 'invalid',\n  password: 'short',\n});\n// Type: { email?: string[]; password?: string[]; } | null\n```\n\n### Pattern 6: Discriminated Unions\n\n```typescript\ntype Success<T> = {\n  status: 'success';\n  data: T;\n};\n\ntype Error = {\n  status: 'error';\n  error: string;\n};\n\ntype Loading = {\n  status: 'loading';\n};\n\ntype AsyncState<T> = Success<T> | Error | Loading;\n\nfunction handleState<T>(state: AsyncState<T>): void {\n  switch (state.status) {\n    case 'success':\n      console.log(state.data); // Type: T\n      break;\n    case 'error':\n      console.log(state.error); // Type: string\n      break;\n    case 'loading':\n      console.log('Loading...');\n      break;\n  }\n}\n\n// Type-safe state machine\ntype State =\n  | { type: 'idle' }\n  | { type: 'fetching'; requestId: string }\n  | { type: 'success'; data: any }\n  | { type: 'error'; error: Error };\n\ntype Event =\n  | { type: 'FETCH'; requestId: string }\n  | { type: 'SUCCESS'; data: any }\n  | { type: 'ERROR'; error: Error }\n  | { type: 'RESET' };\n\nfunction reducer(state: State, event: Event): State {\n  switch (state.type) {\n    case 'idle':\n      return event.type === 'FETCH' ? { type: 'fetching', requestId: event.requestId } : state;\n    case 'fetching':\n      if (event.type === 'SUCCESS') {\n        return { type: 'success', data: event.data };\n      }\n      if (event.type === 'ERROR') {\n        return { type: 'error', error: event.error };\n      }\n      return state;\n    case 'success':\n    case 'error':\n      return event.type === 'RESET' ? { type: 'idle' } : state;\n  }\n}\n```\n\n## Type Inference Techniques\n\n### 1. Infer Keyword\n\n```typescript\n// Extract array element type\ntype ElementType<T> = T extends (infer U)[] ? U : never;\n\ntype NumArray = number[];\ntype Num = ElementType<NumArray>; // number\n\n// Extract promise type\ntype PromiseType<T> = T extends Promise<infer U> ? U : never;\n\ntype AsyncNum = PromiseType<Promise<number>>; // number\n\n// Extract function parameters\ntype Parameters<T> = T extends (...args: infer P) => any ? P : never;\n\nfunction foo(a: string, b: number) {}\ntype FooParams = Parameters<typeof foo>; // [string, number]\n```\n\n### 2. Type Guards\n\n```typescript\nfunction isString(value: unknown): value is string {\n  return typeof value === 'string';\n}\n\nfunction isArrayOf<T>(value: unknown, guard: (item: unknown) => item is T): value is T[] {\n  return Array.isArray(value) && value.every(guard);\n}\n\nconst data: unknown = ['a', 'b', 'c'];\n\nif (isArrayOf(data, isString)) {\n  data.forEach(s => s.toUpperCase()); // Type: string[]\n}\n```\n\n### 3. Assertion Functions\n\n```typescript\nfunction assertIsString(value: unknown): asserts value is string {\n  if (typeof value !== 'string') {\n    throw new Error('Not a string');\n  }\n}\n\nfunction processValue(value: unknown) {\n  assertIsString(value);\n  // value is now typed as string\n  console.log(value.toUpperCase());\n}\n```\n\n## Best Practices\n\n1. **Use `unknown` over `any`**: Enforce type checking\n2. **Prefer `interface` for object shapes**: Better error messages\n3. **Use `type` for unions and complex types**: More flexible\n4. **Leverage type inference**: Let TypeScript infer when possible\n5. **Create helper types**: Build reusable type utilities\n6. **Use const assertions**: Preserve literal types\n7. **Avoid type assertions**: Use type guards instead\n8. **Document complex types**: Add JSDoc comments\n9. **Use strict mode**: Enable all strict compiler options\n10. **Test your types**: Use type tests to verify type behavior\n\n## Type Testing\n\n```typescript\n// Type assertion tests\ntype AssertEqual<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false;\n\ntype Test1 = AssertEqual<string, string>; // true\ntype Test2 = AssertEqual<string, number>; // false\ntype Test3 = AssertEqual<string | number, string>; // false\n\n// Expect error helper\ntype ExpectError<T extends never> = T;\n\n// Example usage\ntype ShouldError = ExpectError<AssertEqual<string, number>>;\n```\n\n## Common Pitfalls\n\n1. **Over-using `any`**: Defeats the purpose of TypeScript\n2. **Ignoring strict null checks**: Can lead to runtime errors\n3. **Too complex types**: Can slow down compilation\n4. **Not using discriminated unions**: Misses type narrowing opportunities\n5. **Forgetting readonly modifiers**: Allows unintended mutations\n6. **Circular type references**: Can cause compiler errors\n7. **Not handling edge cases**: Like empty arrays or null values\n\n## Performance Considerations\n\n- Avoid deeply nested conditional types\n- Use simple types when possible\n- Cache complex type computations\n- Limit recursion depth in recursive types\n- Use build tools to skip type checking in production\n\n## Resources\n\n- **TypeScript Handbook**: https://www.typescriptlang.org/docs/handbook/\n- **Type Challenges**: https://github.com/type-challenges/type-challenges\n- **TypeScript Deep Dive**: https://basarat.gitbook.io/typescript/\n- **Effective TypeScript**: Book by Dan Vanderkam"
  },
  {
    "path": ".dockerignore",
    "content": "client/.next/cache\n# Keep empty dir. Next.js scans it on startup even with CDN assets\nclient/.next/static/**\n!client/.next/static/\nclient/.turbo\nnode_modules\nnpm-debug.log\n.turbo\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\ninsert_final_newline = false\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintignore",
    "content": "# don't ever lint node_modules\nnode_modules\n# don't lint build output (make sure it's set to your correct build folder name)\ndist"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [dzmitry-varabei]\npatreon: # Replace with a single Patreon username\nopen_collective: rsschool\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: 🐞 Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Device (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Browser [e.g. chrome, safari]\n- Version [e.g. 22]\n\n**Additional context**\nAdd any other context about the problem here."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/data-issue-report.md",
    "content": "---\nname: 🗃 Data issue\nabout: Create a issue to fix data\ntitle: ''\nlabels: data issue\n---\n\n**Course**\nSpecify your course (e.g. rs-2019-q1)\n\n**What is wrong**\nDescribe data issue"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: 🚀 Feature request\nabout: Request a feature to implement\ntitle: ''\nlabels: feature\n---\n\n**Describe the feature**\nPlease describe the requested feature in details.\n\n**Additional context**\nAdd any other context about the feature here."
  },
  {
    "path": ".github/auto-label.json",
    "content": "{\n  \"rules\": {\n    \"code:client\": [\"client/\"],\n    \"code:server\": [\"server/\"],\n    \"⛓ dependencies\": \"**/package.json\"\n  }\n}\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "Use instructions from `AGENTS.md` to guide your work."
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "[Pull Request Guidelines](https://github.com/rolling-scopes/rsschool-app/blob/master/CONTRIBUTING.md#pull-requests)\n\n**Issue**:\n_Link to the relevant GitHub Issue (e.g., `#123` for issue number 123)._\n\n**Description**:\n_Please provide a description of the changes in this Pull Request. Include screenshots or GIFs if applicable. The description should clearly explain the purpose of this PR._\n\n**Self-Check**:\n\n- [ ] Database migration added (if required)\n- [ ] Changes tested locally"
  },
  {
    "path": ".github/workflows/deploy-sloths.yaml",
    "content": "name: Deploy sloths.rs.school\non:\n  push:\n    branches: [master]\n    paths:\n      - 'tools/sloths/**'\n      - '.github/workflows/deploy-sloths.yaml'\n\nconcurrency:\n  group: deploy-sloths\n  cancel-in-progress: true\n\njobs:\n  build_deploy:\n    name: Build and Deploy\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./tools/sloths\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('tools/sloths/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Build\n        env:\n          VITE_CDN_URL: https://cdn.rs.school/sloths\n        run: npm run build-only\n\n      - name: Upload to S3\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID  }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY  }}\n          AWS_DEFAULT_REGION: eu-central-1\n        run: |\n          aws s3 cp dist s3://sloths.rs.school/ --recursive --cache-control \"public,max-age=3600,immutable\"\n"
  },
  {
    "path": ".github/workflows/deploy.yaml",
    "content": "name: Deploy RS School App\n\non:\n  push:\n    branches: [master]\n\nenv:\n  DO_NOT_TRACK: '1'\n  NODE_OPTIONS: '--no-warnings'\n  TURBO_VERSION: '2.8.12'\n\njobs:\n  checks:\n    name: Test, Lint\n    concurrency:\n      group: deploy-checks\n      cancel-in-progress: true\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Test\n        run: npm run test\n\n      - name: Lint\n        run: npm run lint\n\n  build_client:\n    name: Build (Client)\n    concurrency:\n      group: deploy-build-client\n      cancel-in-progress: true\n    needs: [checks]\n    runs-on: ubuntu-latest\n    timeout-minutes: 20\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Generate isolated subworkspace for Client\n        run: npx turbo@${{ env.TURBO_VERSION }} prune --scope=client --docker\n\n      - name: Add lockfile and package.json's and source of isolated subworkspace\n        run: |\n          mkdir -p app\n          cp -r out/json/. app\n          cp -r out/full/. app\n          cp -r out/package-lock.json app\n          cp -r .dockerignore app\n          cp -r common app/common\n\n      - name: Install dependencies\n        run: npm ci\n        working-directory: ./app\n\n      - name: Build (npm run build)\n        env:\n          NODE_ENV: production\n          RSSHCOOL_UI_GCP_MAPS_API_KEY: ${{ secrets.RSSHCOOL_UI_GCP_MAPS_API_KEY }}\n          CDN_HOST: 'https://cdn.rs.school'\n          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n        working-directory: ./app\n        run: npm run build\n\n      - name: Clean modules\n        working-directory: ./app\n        run: npm prune --omit=dev\n\n      - name: Upload to CDN\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_DEFAULT_REGION: eu-central-1\n        run: aws s3 cp ./app/client/.next/static s3://cdn.rs.school/_next/static/ --recursive --cache-control \"public,max-age=15552000,immutable\"\n\n      - name: Build container\n        run: docker build -t ghcr.io/rolling-scopes/rsschool-app-client:master -f client/Dockerfile ./app\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Publish \"client\" container\n        run: docker push ghcr.io/rolling-scopes/rsschool-app-client:master\n\n  build_server:\n    name: Build (Server)\n    concurrency:\n      group: deploy-build-server\n      cancel-in-progress: true\n    needs: [checks]\n    runs-on: ubuntu-latest\n    timeout-minutes: 20\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Generate isolated subworkspace for Server\n        run: npx turbo@${{ env.TURBO_VERSION }} prune --scope=server --docker\n\n      - name: Add lockfile and package.json's and source of isolated subworkspace\n        run: |\n          mkdir -p app\n          cp -r out/json/. app\n          cp -r out/full/. app\n          cp -r out/package-lock.json app\n          cp -r common app/common\n\n      - name: Install dependencies\n        run: npm ci\n        working-directory: ./app\n\n      - name: Build (npm run build)\n        env:\n          NODE_ENV: production\n        run: npm run build\n        working-directory: ./app\n\n      - name: Build container\n        run: docker build -t ghcr.io/rolling-scopes/rsschool-app-server:master -f server/Dockerfile ./app\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Publish \"server\" container\n        run: docker push ghcr.io/rolling-scopes/rsschool-app-server:master\n\n  build_nestjs:\n    name: Build (Nest.js)\n    concurrency:\n      group: deploy-build-nestjs\n      cancel-in-progress: true\n    needs: [checks]\n    runs-on: ubuntu-latest\n    timeout-minutes: 20\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Generate isolated subworkspace for Nest.js\n        run: npx turbo@${{ env.TURBO_VERSION }} prune --scope=server --scope=nestjs --docker\n\n      - name: Add lockfile and package.json's and source of isolated subworkspace\n        run: |\n          mkdir -p app\n          cp -r out/json/. app\n          cp -r out/full/. app\n          cp -r out/package-lock.json app\n          cp -r common app/common\n\n      - name: Install dependencies\n        run: npm ci\n        working-directory: ./app\n\n      - name: Build (npm run build)\n        env:\n          NODE_ENV: production\n        run: npx turbo run build --filter=nestjs\n        working-directory: ./app\n\n      - name: Build container\n        run: docker build -t ghcr.io/rolling-scopes/rsschool-app-nestjs:master -f nestjs/Dockerfile ./app\n\n      - name: Login to GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Publish \"nestjs\" container\n        run: docker push ghcr.io/rolling-scopes/rsschool-app-nestjs:master\n\n  deploy:\n    name: deploy\n    concurrency:\n      group: deploy-aws\n      cancel-in-progress: false\n    needs: [build_client, build_server, build_nestjs]\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    env:\n      PRIVATE_KEY: ${{ secrets.AWS_PRIVATE_KEY }}\n      HOSTNAME: ${{ secrets.EC2_HOSTNAME }}\n      USERNAME: ${{ secrets.EC2_USERNAME }}\n    environment:\n      name: production\n      url: https://app.rs.school\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Pull AWS SSM Params\n        uses: deptno/action-aws-ssm-to-dotenv@v1.3.2\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_DEFAULT_REGION: eu-central-1\n        with:\n          ssm-path: /prod/app\n          format: dotenv\n          output: .env\n\n      - name: Setup SSH key\n        run: echo \"$PRIVATE_KEY\" > private_key && chmod 600 private_key\n\n      - name: Pull the latest images\n        run: |\n          scp -o StrictHostKeyChecking=no -i private_key ./.env ${USERNAME}@${HOSTNAME}:~/.env\n          scp -o StrictHostKeyChecking=no -i private_key ./docker-compose.yml ${USERNAME}@${HOSTNAME}:~/docker-compose.yml\n          scp -o StrictHostKeyChecking=no -i private_key ./setup/nginx/nginx.conf ${USERNAME}@${HOSTNAME}:~/nginx/nginx.conf\n          ssh -o StrictHostKeyChecking=no -i private_key ${USERNAME}@${HOSTNAME} '\n            sleep 10\n            docker-compose pull\n          '\n\n      - name: Restart\n        run: |\n          ssh -o StrictHostKeyChecking=no -i private_key ${USERNAME}@${HOSTNAME} '\n            docker-compose up -d\n            docker-compose restart nginx\n          '\n\n      - name: Clean up\n        run: |\n          ssh -o StrictHostKeyChecking=no -i private_key ${USERNAME}@${HOSTNAME} '\n            docker system prune -f\n          '\n          rm -f private_key\n"
  },
  {
    "path": ".github/workflows/pull_request.yml",
    "content": "name: Pull Request\n\non:\n  pull_request:\n    branches: [master]\n    types: [opened, synchronize, reopened, labeled, unlabeled]\n\nenv:\n  DO_NOT_TRACK: '1'\n  NODE_OPTIONS: '--no-warnings'\n\njobs:\n  pr_checks:\n    name: Lint, Format, Test\n    concurrency:\n      group: pr-checks_${{ github.head_ref }}\n      cancel-in-progress: true\n    runs-on: ubuntu-latest\n    timeout-minutes: 20\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Format\n        run: npm run ci:format\n\n      - name: Lint\n        run: npm run lint\n\n      - name: Test (client)\n        run: npm run test:ci\n        if: success() || failure()\n        working-directory: client\n\n      - name: Test (server)\n        run: npm run test:ci\n        if: success() || failure()\n        working-directory: server\n\n      - name: Test (nestjs)\n        run: npm run test:ci\n        if: success() || failure()\n        working-directory: nestjs\n\n  pr_build:\n    name: Build\n    concurrency:\n      group: pr-build_${{ github.head_ref }}\n      cancel-in-progress: true\n    runs-on: ubuntu-latest\n    timeout-minutes: 20\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore next cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-next\n        with:\n          path: |\n            client/.next/cache\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ github.ref_name }}-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-${{ github.ref_name }}\n\n      - name: Restore Turborepo cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-turbo\n        with:\n          path: .turbo\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json', 'turbo.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        with:\n          platforms: linux/amd64\n\n      - name: Login to ECR\n        uses: docker/login-action@v3\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        with:\n          registry: 511361162520.dkr.ecr.eu-central-1.amazonaws.com\n          username: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          password: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Install dependencies\n        env:\n          PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 'true'\n        run: npm ci\n\n      - name: Build (npm run build)\n        env:\n          AWS_LAMBDA: 'true'\n          NODE_ENV: production\n          RSSCHOOL_DEV_TOOLS: 'true'\n          RSSHCOOL_UI_GCP_MAPS_API_KEY: ${{ secrets.RSSHCOOL_UI_GCP_MAPS_API_KEY }}\n        run: npm run build\n\n      - name: Upload to CDN\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        working-directory: ./client\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_DEFAULT_REGION: eu-central-1\n        run: aws s3 cp .next/static s3://cdn.rs.school/_next/static/ --recursive --cache-control \"public,max-age=15552000,immutable\"\n\n      - name: Package Client\n        uses: docker/build-push-action@v6\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        with:\n          platforms: linux/amd64\n          provenance: false\n          context: .\n          file: ./client/Dockerfile.lambda\n          push: true\n          tags: 511361162520.dkr.ecr.eu-central-1.amazonaws.com/rsschool-ui:pr${{ github.event.pull_request.number }}\n          cache-from: type=gha,scope=rsschool-ui-lambda\n          cache-to: type=gha,mode=max,scope=rsschool-ui-lambda\n\n      - name: Pull AWS SSM Params\n        uses: deptno/action-aws-ssm-to-dotenv@v1.3.2\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_DEFAULT_REGION: eu-central-1\n        with:\n          ssm-path: /staging/app\n          format: dotenv\n          output: server/.env\n\n      - name: Package Server\n        uses: docker/build-push-action@v6\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        with:\n          platforms: linux/amd64\n          provenance: false\n          context: .\n          file: ./server/Dockerfile.lambda\n          push: true\n          tags: 511361162520.dkr.ecr.eu-central-1.amazonaws.com/rsschool-server:pr${{ github.event.pull_request.number }}\n          cache-from: type=gha,scope=rsschool-server-lambda\n          cache-to: type=gha,mode=max,scope=rsschool-server-lambda\n\n      - name: Pull AWS SSM Params\n        uses: deptno/action-aws-ssm-to-dotenv@v1.3.2\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_DEFAULT_REGION: eu-central-1\n        with:\n          ssm-path: /staging/app\n          format: dotenv\n          output: nestjs/.env\n\n      - name: Package Nestjs\n        uses: docker/build-push-action@v6\n        if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n        with:\n          platforms: linux/amd64\n          provenance: false\n          context: .\n          file: ./nestjs/Dockerfile.lambda\n          push: true\n          tags: 511361162520.dkr.ecr.eu-central-1.amazonaws.com/rsschool-nestjs:pr${{ github.event.pull_request.number }}\n          cache-from: type=gha,scope=rsschool-nestjs-lambda\n          cache-to: type=gha,mode=max,scope=rsschool-nestjs-lambda\n\n  pr_deploy:\n    needs: [pr_build]\n    name: Deploy\n    concurrency:\n      group: pr-deploy_${{ github.head_ref }}\n      cancel-in-progress: false\n    timeout-minutes: 30\n    runs-on: ubuntu-latest\n    if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('setup/cdk/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Install dependencies\n        working-directory: ./setup/cdk\n        run: npm ci\n\n      - name: Deploy\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_DEFAULT_REGION: eu-central-1\n        working-directory: ./setup/cdk\n        run: |\n          npx cdk deploy --require-approval never -c feature=pr${{ github.event.pull_request.number }} -c deployId=${GITHUB_RUN_ID}\n\n      - name: Create GitHub deployment\n        uses: apalchys/deployment-action@v1.2.6\n        id: deployment\n        with:\n          ref: ${{ github.event.pull_request.head.ref }}\n          pr: true\n          pr_id: ${{ github.event.pull_request.number }}\n          token: ${{ secrets.GITHUB_TOKEN }}\n          target_url: https://pr${{ github.event.pull_request.number }}.app.rs.school\n          environment: pr${{ github.event.pull_request.number }}\n          initial_status: success\n\n  pr_e2e:\n    needs: [pr_deploy]\n    name: End-to-End Tests\n    concurrency:\n      group: pr-e2e_${{ github.head_ref }}\n      cancel-in-progress: true\n    timeout-minutes: 10\n    runs-on: ubuntu-latest\n    if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Restore browsers cache\n        uses: actions/cache@v4\n        with:\n          path: '~/.cache/ms-playwright/'\n          key: ${{ runner.os }}-browsers-${{ hashFiles('package-lock.json') }}\n\n      - name: Install Dependencies\n        working-directory: ./client\n        run: npm ci\n\n      - name: Install Playwright\n        working-directory: ./client\n        run: npx playwright install --with-deps\n\n      - name: Run Playwright tests\n        working-directory: ./client\n        env:\n          CI: 'true'\n          BASE_URL: https://pr${{ github.event.pull_request.number }}.app.rs.school\n        run: npx playwright test\n\n      - name: Save Report\n        uses: actions/upload-artifact@v4\n        if: always()\n        with:\n          name: playwright-report\n          path: client/playwright-report/\n          retention-days: 15\n"
  },
  {
    "path": ".github/workflows/pull_request_close.yml",
    "content": "name: Pull Request Delete\n\non:\n  pull_request:\n    types: [closed]\n    branches: [master]\n\njobs:\n  pr_delete:\n    runs-on: ubuntu-latest\n    if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}\n    steps:\n      - name: Setup Node.js environment\n        uses: actions/setup-node@v4\n        with:\n          node-version: '24'\n\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Restore npm cache\n        uses: actions/cache@v4\n        env:\n          cache-name: cache-npm\n        with:\n          path: ~/.npm\n          key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('setup/cdk/package-lock.json') }}\n          restore-keys: |\n            ${{ runner.os }}-${{ env.cache-name }}-\n\n      - name: Install Dependencies\n        working-directory: setup/cdk\n        run: npm ci\n\n      - name: Delete Stack\n        env:\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_DEFAULT_REGION: eu-central-1\n        working-directory: setup/cdk\n        run: |\n          npx cdk destroy --force --all -c feature=pr${{ github.event.pull_request.number }} -c deployId=${GITHUB_RUN_ID}\n\n      - name: Delete Deployment\n        uses: strumwolf/delete-deployment-environment@v2\n        with:\n          token: ${{ secrets.DELETE_ENV_TOKEN }}\n          environment: pr${{ github.event.pull_request.number }}\n"
  },
  {
    "path": ".github/workflows/renovate.yml",
    "content": "name: Renovate\non:\n  schedule:\n    # once a month\n    - cron: '0 0 1 * *'\n  workflow_dispatch:\njobs:\n  renovate:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Self-hosted Renovate\n        uses: renovatebot/github-action@v34.82.0\n        with:\n          configurationFile: renovate.json\n          token: ${{ secrets.RENOVATE_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.env\n.history/\n.idea/\n.next/\n.sentryclirc\n.swc/\n.turbo\n.vscode\napp/\ncoverage\ndist/\nnode_modules/\nout\nplaywright-report/\nreports/*.xml\nsetup/backup.sql\nsetup/cdk/cdk.out\ntest-results/\nclient/tsconfig.tsbuildinfo\n/tmp\n"
  },
  {
    "path": ".oxfmtrc.json",
    "content": "{\n  \"trailingComma\": \"all\",\n  \"tabWidth\": 2,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"printWidth\": 120,\n  \"arrowParens\": \"avoid\",\n  \"sortPackageJson\": false,\n  \"ignorePatterns\": [\"client/src/api/\", \"client/playwright-report/\", \"package-lock.json\"]\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# LLM Agents Instructions\n\n## General\n\n- Read `README.md` first for project overview and architecture\n- Read `CONTRIBUTING.md` for local development setup, commands, commit messages, and PR conventions\n- Read `GUIDELINES.md` for code style and testing patterns\n- Read `DOMAIN.md` for application domain and data models\n- Read all `*/README.md` files for workspace specific architecture and development guidelines\n\n## Instructions\n\n- Use the `tmp` folder in the repository to store temporary files during execution and planning if needed\n- Never run deploy commands\n- Run `npm run lint`, `npm run test`, `npm run compile`, and `npm run format` at the end of each completed task if you modified any files. If any of these steps fail, fix the issues and run the commands again.\n- Prefer telegraph style for communications and reasoning. Sacrifice grammar and punctuation for clarity and conciseness. Save tokens.\n\n## Documentation Style\n\nUse the following writing style for documentation, comments and README files.\n\nAudience: Software Engineers. Expect half to be native speakers and half non-native speakers.\n\n- Keep the reading level at or below 9th grade (Flesch-Kincaid)\n- Write in clear, concise, active voice; don't assume passive voice reads well\n- Keep noun phrases short and avoid stacked modifiers\n- Limit embedded clauses to one level\n- Use conditionals sparingly and stick to simple if/then forms; avoid mixed or inverted versions\n- Use transition words (however, therefore, because) to show logic instead of expecting readers to infer it\n- Spread new ideas across sentences; avoid packing many concepts into one\n- Do not end list items with a period\n- Do not use emojis\n- Do not write obvious comments; explain why, not how or what\n\n## Commands\n\n- `npm run lint`: Run linting\n- `npm run test`: Run tests\n- `npm run compile`: Compile the project\n- `npm run format`: Format the code"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# How To Contribute\n\n## Local development\n\n### Prerequisites\n\n- [Git 2.10+](https://git-scm.com/downloads)\n- [NodeJS LTS](https://nodejs.org/en/)\n- [Podman](https://podman.io/docs/installation)\n- [podman-compose](https://github.com/containers/podman-compose)\n\n### Steps\n\n1. Fork the repository (<https://help.github.com/articles/fork-a-repo/>)\n\n2. Clone the repository to your local machine (<https://help.github.com/articles/cloning-a-repository/>)\n\n   ```command-line\n   git clone git@github.com:[username]/rsschool-app.git\n   ```\n\n3. Navigate into the directory where you've cloned the source code and install NPM dependencies\n\n   ```command-line\n   cd rsschool-app\n   npm install\n   ```\n\n4. Create a branch for your feature. Prefix the branch with your GitHub Username. Example: `git checkout -b AlreadyBored/feature-x master`\n\n   ```command-line\n   git checkout -b <username>/feature-x master\n   ```\n\n5. The application requires a connection to a Postgres database. Here is how to get test database running locally:\n\n   Run a Postgres Database locally using Podman & podman-compose\n\n   ```command-line\n   npm run db:up\n   ```\n\n   Restore a test database snapshot\n\n   ```command-line\n   npm run db:restore\n   ```\n\n   If you are done with development, stop the database;\n\n   ```command-line\n   npm run db:down\n   ```\n\n6. Run the application in development mode with live reload:\n\n   ```command-line\n   npm start\n   ```\n\n7. Do hacking 👩‍💻👨‍💻\n\n8. You could specify any environment variable during development using `.env` file. Make a copy of `server/.env.example` and `nestjs/.env.example` and rename it to `server/.env` or `nestjs/.env` respectively. We support it via `dotenv` package. More information about usage here: <https://github.com/motdotla/dotenv>\n\n9. By default locally, you will be logged with `admin` access. If you want to change it, need to set `RSSCHOOL_AUTH_DEV_ADMIN` to `false` in `nestjs/.env` file\n\n   **IMPORTANT:** Never commit changes to `.env` file\n\n10. Do not forget to write unit-tests for your feature following [Unit-Tests Style Guide](#unit-tests-style-guide). We use [Jest](https://facebook.github.io/jest/) for unit-tests.\n\n11. Write end-to-end tests for your feature if applicable. Please see `client/specs` directory for more information. We use [Playwright](https://playwright.dev/) for end-to-end tests. You can run them using `npm run test:e2e` command and they supposed to work against test database snapshot.\n\n12. Make sure tests, lints pass and code formatted properly (they'll be run on a git pre-commit hook too)\n\n    ```command-line\n    npm test\n    npm run lint\n    npm run pretty\n    ```\n\n13. Commit your changes using a descriptive commit message that follows our [Commit Message Conventions](#git-commit-messages)\n\n    ```command-line\n    git commit -m \"feat: implement feature X\"\n    ```\n\n14. Push your branch to GitHub:\n\n    ```command-line\n    git push origin <username>/feature-x\n    ```\n\n15. Create a pull request. We support \"feature branch\" deployments. If you want to deploy your pull request, please add `deploy` label during creation.\n\n## API client generation\n\nWe use [OpenAPI Generator](https://openapi-generator.tech/) to generate API client for `NestJS` API. Here are steps how to do it:\n\n- Make sure database is running locally\n- Navigate to `./nestjs`\n- Run\n\n  ```sh\n  npm run openapi\n  ```\n\n- Commit updated files (`/client/src/api/*` and `./nestjs/src/spec.json`)\n\n_NOTE: in case of problems with running `openapi` you might need to install [Java](https://www.java.com/) or use some other way from [OpenAPI Generator Installation docs](https://openapi-generator.tech/docs/installation/)_\n\n## Database Migrations\n\nIf you made changes to DB models, you need to create a DB migration. Here are steps how to do it\n\n1. Go to `/server`\n2. Run `npm run typeorm:migration:generate src/migrations/{MigrationName}` where `{MigrationName}` is your migration name.\n3. Import your migration to `migrations` array at `./server/src/migrations/index.ts`\n4. Commit and push your changes\n\nSee more about TypeORM migrations at official docs [Migrations](https://github.com/typeorm/typeorm/blob/master/docs/migrations.md)\n\n## Pull Requests\n\n- Branch name must be prefixed with your GitHub Username. Example: `AlreadyBored/feature-x`. For LLM agents, use `agent/` prefix. Example: `agent/feature-x`.\n- Check how to create a [Pull Request](https://help.github.com/articles/creating-a-pull-request/) (PR).\n- PR titles must follow [Conventional Commits](#git-commit-messages).\n- Do not include issue IDs in the PR title.\n- Use GitHub's [Draft PR](https://github.blog/2019-02-14-introducing-draft-pull-requests/) feature instead of using \"WIP\" (Work In Progress) in the title.\n- Consider adding relevant `area:*` label(s) to your PR.\n- Add the `deploy` label if you want the PR deployed to the staging environment. **NOTE**: This feature does not work for PRs opened from forks due to security limitations.\n- Write a clear and meaningful description for your PR.\n- Include screenshots and animated GIFs in your PR description whenever possible to demonstrate changes.\n\n## Style Guides\n\n### Git Commit Messages\n\n- Use [Conventional Commits](https://conventionalcommits.org/) format\n- Allowed Types:\n  - build: - _changes that affect the build system or external dependencies (example scopes: npm, webpack)_\n  - ci: - _changes to our CI configuration files and scripts (example scopes: drone)_\n  - docs: - _documentation only changes_\n  - feat: - _a new feature_\n  - fix: - _a bug fix_\n  - perf: - _a code change that improves performance_\n  - refactor: - _a code change that neither fixes a bug nor adds a feature_\n  - style: - _changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)_\n  - test: - _adding missing tests or correcting existing tests_\n- Use the present tense (\"add feature\" not \"added feature\")\n- Use the imperative mood (\"move cursor to...\" not \"moves cursor to...\")\n- Limit the first line to 72 characters or less\n- Reference issues and pull requests liberally after the first line"
  },
  {
    "path": "DOMAIN.md",
    "content": "# Domain Model\n\nThis document describes the core domain model implemented in `server/src/models` and how the entities relate. It is meant as a quick map for agents working on the platform.\n\n## Core identity and profile\n\n- `User` is the primary identity (GitHub-based) with contact info, profile details, and visibility settings.\n- `ProfilePermissions` stores per-user visibility rules for profile fields (public vs student vs mentor).\n- `ExternalAccount` records auxiliary learning platforms (codewars, etc) on `User`.\n- `LoginState` stores short-lived state for auth or channel linking flows.\n- `Session` defines derived course roles used by authorization (`CourseRole`).\n\n## Courses, roles, and enrollment\n\n- `Course` defines a cohort with dates, discipline, location, flags (planned/completed/invite-only), mentoring settings, and certificate rules.\n- `Discipline` is a taxonomy item used by courses, tasks, and events.\n- `DiscordServer` holds course communication endpoints (gratitude URL, mentors chat, etc).\n- `Student` is a per-course role for a `User` with progress flags (failed/expelled/completed), scores, rank, mentoring, and certificate.\n- `Mentor` is a per-course role for a `User` with mentor settings and assigned students.\n- `CourseUser` stores course-scoped role flags (manager, supervisor, activist, task owner, etc).\n- `CourseManager` is a simple join for course managers (legacy or specific flows).\n- `Registry` is a course registration request for a user (student or mentor) with status.\n- `MentorRegistry` stores mentor preferences and capacity for course assignment.\n- `UserGroup` is a static group of users tied to course roles.\n\n## Tasks and assessment\n\n- `Task` is the reusable definition of a task (type, description, skills, tags, criteria).\n- `CourseTask` is a `Task` scheduled for a specific course with deadlines, scoring, checker type, and cross-check status.\n- `TaskCriteria` stores per-task cross-check criteria definitions.\n- `TaskResult` is the authoritative score and submission links for a student on a course task.\n- `TaskChecker` links a mentor to a student for a course task review.\n- `TaskArtefact` stores optional artefacts (video, presentation, notes) per student and course task.\n- `TaskVerification` stores auto-check results for tests (status, score, answers, metadata).\n- `TaskInterviewStudent` enrolls a student into a task-based interview flow.\n- `TaskInterviewResult` stores mentor interview results and form answers for a task.\n\n## Cross-check (peer review)\n\nUsed when `CourseTask.checker = crossCheck`.\n\n- `TaskSolution` is a student's submitted solution URL plus review metadata.\n- `TaskSolutionChecker` assigns a reviewer (student) to a submitted solution.\n- `TaskSolutionResult` stores a review score, criteria-based review, and discussion messages.\n- `CourseTask.crossCheckStatus` tracks lifecycle: `initial` -> `distributed` -> `completed`.\n\n## Interviews and stage assessments\n\n- `StageInterview` records a mentor-student interview tied to a course and optional task/stage.\n- `StageInterviewStudent` registers a student into stage interview flow.\n- `StageInterviewFeedback` stores structured JSON feedback for a stage interview.\n\n## Teams and distributions\n\n- `TeamDistribution` defines a team formation window and constraints (size, score thresholds).\n- `TeamDistributionStudent` tracks who participates in distribution and whether they are assigned.\n- `Team` is a concrete team with members, lead, and chat metadata.\n\n## Schedule and events\n\n- `Event` is a reusable schedule entity (lecture, workshop, etc) with description and type.\n- `CourseEvent` schedules an `Event` in a course with date/time, organizer, and details.\n\n## Feedback, reputation, and certificates\n\n- `Feedback` is a public gratitude/badge from one user to another (course-scoped).\n- `PrivateFeedback` is internal feedback not visible publicly.\n- `StudentFeedback` is mentor feedback with structured soft-skill ratings and recommendations.\n- `Certificate` is an issued course certificate linked to a student.\n- `Contributor` represents platform contributors with profile descriptions.\n\n## CV and hiring\n\n- `Resume` stores CV data for a user and visibility to employers.\n\n## Notifications\n\n- `Notification` defines notification types and parent/child relationships.\n- `NotificationChannel` defines delivery channels (`email`, `telegram`, `discord`).\n- `NotificationChannelSettings` provides per-notification templates per channel.\n- `NotificationUserSettings` stores per-user subscription preferences per notification+channel.\n- `NotificationUserConnection` stores channel identifiers linked to users (e.g. Telegram id).\n- `Alert` is a global or course-scoped banner message.\n\n## Audit and integrations\n\n- `History` stores audit records for inserts/updates/removals with before/after snapshots.\n- `RepositoryEvent` stores webhook-like activity from GitHub repositories.\n- `Prompt` stores configurable text prompts for AI-assisted features.\n\n## Relationship map (high level)\n\n- `User` -> `Student` / `Mentor` (per course)\n- `Course` -> `Student`, `Mentor`, `CourseTask`, `CourseEvent`, `Registry`\n- `Task` -> `CourseTask` -> `TaskResult` / `TaskSolution` / `TaskVerification`\n- `TaskSolution` -> `TaskSolutionChecker` -> `TaskSolutionResult` (cross-check)\n- `CourseTask` -> `TaskInterviewStudent` / `TaskInterviewResult`\n- `Course` -> `TeamDistribution` -> `Team` / `TeamDistributionStudent`\n- `Course` -> `Certificate` (via `Student`)\n- `User` -> `Resume` / `ProfilePermissions` / `NotificationUserSettings`"
  },
  {
    "path": "GUIDELINES.md",
    "content": "# Guidelines\n\n## Project Structure\n\n- `client` - Next.js frontend (active)\n- `nestjs` - NestJS backend (active)\n- `server` - Old Koa.js backend. Still used for legacy endpoints and TypeORM entities\n\nNew features should be implemented in `nestjs`. If changes need to be made to functionality in `server/`, migrate it to `nestjs` first (exception: small critical hotfixes). Only allowed to modify TypeORM entities in `server`\n\n### Path Aliases\n\n| Alias         | Resolves To           | Used In |\n| ------------- | --------------------- | ------- |\n| `@entities/*` | `server/src/models/*` | nestjs  |\n| `@client/*`   | `client/src/*`        | client  |\n\n### File Naming\n\n| Type       | Pattern              | Example                |\n| ---------- | -------------------- | ---------------------- |\n| Component  | PascalCase.tsx       | `CourseCard.tsx`       |\n| Hook       | camelCase.ts         | `useExpelledStats.ts`  |\n| Service    | kebab-case.ts        | `courses.service.ts`   |\n| DTO        | kebab-case.dto.ts    | `create-course.dto.ts` |\n| Test       | \\*.test.ts(x)        | `auth.service.test.ts` |\n| Module     | kebab-case.module.ts | `courses.module.ts`    |\n| Entity     | camelCase.ts         | `course.ts`            |\n| CSS Module | \\*.module.css        | `Card.module.css`      |\n\n## TypeScript Conventions\n\n- Prefer `unknown` over `any` for truly unknown types\n- Use utility types (`Pick`, `Omit`, `Partial`) instead of manual type construction\n- Specify return types for public functions explicitly\n- Use `readonly` modifier for data that should not be mutated\n- Treat function arguments as immutable\n- Use `const` by default, `let` only when reassignment is needed\n- Prefix unused variables with underscore: `_unused`\n- Return early to reduce nesting depth\n- Use `async/await` over `.then()` chains\n- Use `Promise.all()` for parallel async operations\n- Order imports: external packages, internal aliases, relative imports\n- Use path aliases instead of deep relative imports\n\n## Client (React/Next.js)\n\n### Module Structure\n\n```\nmodules/<Feature>/\n  components/     # Feature-specific UI\n  hooks/          # Feature-specific hooks\n  pages/          # Page components\n  services/       # API wrappers\n  types.ts        # Feature types\n  index.ts        # Barrel exports\n```\n\n### Components\n\n- Use functional components with hooks exclusively\n- Export as named exports (not default)\n- One component per file as primary export\n- Use `React.memo()` for components with expensive renders\n- Keep components pure when possible\n- Define props with `type`, not `interface`\n\n### Pages\n\n- Keep `pages/*.tsx` as thin wrappers only\n- Compose providers and render single module page component\n- Delegate all logic to module components\n\n### State Management\n\n- Minimize `useEffect` - use only when truly necessary\n- Derive computed values instead of storing in state\n- Lift state up rather than prop drilling\n- Use context for cross-cutting concerns\n\n### Hooks\n\n- Use hooks from `react-use` or `ahooks` before writing custom\n- Use `useRequest` from `ahooks` for API calls\n- Keep custom hooks focused and simple\n\n### Services\n\n- Initialize clients at module level (singleton pattern)\n- Use OpenAPI client for `/api/v2/*`, axios services for legacy `/api/*`\n\n### Ant Design\n\n- Use `Form.useForm<T>()` with typed form values\n- Use `Form.Item` with `rules` for validation\n- Use `theme.useToken()` for theme colors\n- Use `clsx` for conditional class composition\n\n### Styling\n\n- Use CSS modules exclusively: `*.module.css`\n- Co-locate styles with components\n- Do not use styled-jsx (deprecated)\n\n## Backend (NestJS)\n\n### Module Structure\n\n```\nnestjs/src/<domain>/\n  <domain>.module.ts       # Module definition\n  <domain>.controller.ts   # HTTP endpoints\n  <domain>.service.ts      # Business logic\n  dto/\n    index.ts               # Barrel exports\n    <entity>.dto.ts        # Response DTO\n    create-<entity>.dto.ts # Input DTO with validation\n    update-<entity>.dto.ts # Partial input (extends create)\n```\n\n### Controllers\n\n- Add `@ApiTags('domain')` to controller class\n- Add `@ApiOperation({ operationId: 'verbNoun' })` to every endpoint\n- Add `@ApiOkResponse({ type: DtoClass })` for type safety\n- Use `ParseIntPipe` for numeric params\n- Transform entities to DTOs before returning\n- Keep controllers thin - delegate business logic to services\n\n### Guards and Roles\n\n- `@UseGuards(DefaultGuard)` - authenticated users\n- `@UseGuards(DefaultGuard, RoleGuard)` + `@RequiredRoles([...])` - role-based\n- Use `Role.Admin`, `CourseRole.Manager` enums, not strings\n\n### Services\n\n- Inject repositories: `@InjectRepository(Entity)`\n- Use `findOneOrFail` / `findOneByOrFail` when entity must exist\n- Keep services focused on single domain\n\n### DTOs\n\n- Response DTO: constructor takes entity, maps to properties\n- Input DTO: validation decorators (`@IsNotEmpty`, `@IsString`, etc.)\n- Update DTO: `extends PartialType(CreateDto)`\n- All properties need `@ApiProperty()` for OpenAPI\n\n### Error Handling\n\n- Include context in error messages: `Entity with id ${id} not found`\n\n## TypeORM Entities\n\nAll entities live in `server/src/models/`. NestJS imports via `@entities/*` alias.\n\n### Relations\n\n- Keep both relation property and `fieldId` column\n\n### Migrations\n\n- Register migrations in `server/src/migrations/index.ts`\n- Export all entities from `server/src/models/index.ts`\n\n## Testing\n\n### File Organization\n\n- Unit tests: `{source}.test.ts(x)` next to source file\n- E2E tests: `client/specs/*.spec.ts` (Playwright)\n- Never use separate `__tests__` directories for new code\n\n### Test Structure\n\n- `describe` as noun/situation: `describe('AuthService')`\n- `it` should describe behavior: `it('should return null when not found')`\n- Group related tests with nested `describe` blocks\n- Extract shared setup to `beforeEach`\n- Extract shared mock data to reusable constants\n\n### Test Independence\n\n- Each test must be independent and not rely on others\n- Reset mocks and state in `beforeEach`\n- Ensure tests are deterministic (no random values)\n\n### Assertions\n\n- Assert full object shapes over field-by-field checks\n- Use `expect.objectContaining()` for partial matching\n- Test both success and error paths\n- Test edge cases: empty arrays, null values, boundaries\n\n### Async Testing\n\n- Use `await expect().rejects.toThrow()` for error cases\n- In React: use `findBy*` for first async query, then `getBy*` for rest\n- Keep only ONE assertion inside `waitFor` callback\n\n### NestJS Tests\n\n- Use `Test.createTestingModule()` for setup\n- Mock repositories via `getRepositoryToken(Entity)`\n- Mock services with `jest.fn()` methods\n\n### React Tests\n\n- Use `@testing-library/react` utilities\n- Query by role/text over test IDs\n- Test user behavior, not implementation\n\n### Mock Data Typing\n\n```typescript\n// Correct\nconst mockData = { id: 1, name: 'Test' } as User;\n\n// Avoid\nconst mockData = { ... } as unknown as User;\n```"
  },
  {
    "path": "LICENSE",
    "content": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "README.md",
    "content": "[![Deploy](https://github.com/rolling-scopes/rsschool-app/actions/workflows/deploy.yaml/badge.svg?branch=master)](https://github.com/rolling-scopes/rsschool-app/actions/workflows/deploy.yaml)\n\n<img src=\"./tools/sloths/public/img/logo.svg\" width=\"250px\"/>\n\n# RS School App\n\n[app.rs.school](https://app.rs.school) is an application for the [RS School](https://rs.school) educational process. It helps thousands of students become front-end, back-end, mobile, and data science engineers.\n\n<img src=\"https://user-images.githubusercontent.com/618807/138608245-f00471ce-f982-4901-a32e-7246720ed13b.png\" width=\"600px\"/>\n\n## Monitoring\n\nhttps://status.rs.school/\n\n## Technology Overview\n\n### Stack\n\n- Language: [TypeScript](https://www.typescriptlang.org/)\n- Front-end: [Next.js](https://nextjs.org/) / [React](https://reactjs.org/)\n- Back-end: [NestJS](https://nestjs.com/) and [Koa.js](https://koajs.com/) (deprecated backend) / [Node.js](https://nodejs.org/en/)\n- End-to-end: [Playwright](https://playwright.dev/)\n- Database: [PostgreSQL](https://www.postgresql.org/)\n- Deployment: [Podman](https://podman.io/)\n\n### Infrastructure\n\n- Cloud: [AWS EC2](https://aws.amazon.com/ec2/), [AWS RDS](https://aws.amazon.com/rds/postgresql/), [AWS S3](https://aws.amazon.com/s3/), [AWS CloudWatch](https://aws.amazon.com/cloudwatch/)\n- Monitoring: [Sentry](https://rs-school.sentry.io/), [Checkly](https://app.checklyhq.com/)\n- CI/CD: [GitHub Actions](https://github.com/rolling-scopes/rsschool-app/tree/master/.github/workflows)\n\n## Getting Started\n\nRepository is organized into 3 workspaces: `server`, `nestjs`, `client`. Each workspace has its own package.json and npm scripts. We use [Turbo](https://turbo.build/) to run scripts across workspaces.\n\n### Prerequisites\n\nPlease install the following software before starting development:\n\n- [Git 2.10+](https://git-scm.com/downloads)\n- [Node.js LTS](https://nodejs.org/en/download/)\n- [Podman](https://podman.io/docs/installation)\n- [podman-compose](https://github.com/containers/podman-compose)\n\n### Steps\n\n- Clone [repository](https://github.com/rolling-scopes/rsschool-app)\n- Run `npm install` (installs dependencies in the root folder and `client` / `server` folders.)\n- Run `npm run db:up` (starts local database)\n- Run `npm run db:restore` (restores a test DB snapshot)\n- Make copies of `server/.env.example` and `nestjs/.env.example`, then rename them to `server/.env` and `nestjs/.env` respectively\n- Run `npm start` (starts application by running Next.js and REST API server)\n- Open `http://localhost:3000` in a browser\n- See more in [CONTRIBUTING](https://github.com/rolling-scopes/rsschool-app/blob/master/CONTRIBUTING.md) guide\n\n### Running docs locally\n\n- Install docsify globally: `npm i -g docsify-cli`\n- Run `docsify serve -p 4000 docs`\n\n## Contributing\n\nSee [CONTRIBUTING](https://github.com/rolling-scopes/rsschool-app/blob/master/CONTRIBUTING.md) guide\n\n## Contributors\n\n### Code Contributors\n\nThis project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].\n\n<a href=\"https://github.com/rolling-scopes/rsschool-app/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=rolling-scopes/rsschool-app\" />\n</a>\n\n_Made with [contrib.rocks](https://contrib.rocks)_\n\n### Financial Contributors\n\nBecome a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/rsschool/contribute)]\n\n#### Individuals\n\n<a href=\"https://opencollective.com/rsschool\"><img src=\"https://opencollective.com/rsschool/individuals.svg?width=890\"></a>\n\n#### Organizations\n\nSupport this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/rsschool/contribute)]\n\n<a href=\"https://opencollective.com/rsschool/organization/0/website\"><img src=\"https://opencollective.com/rsschool/organization/0/avatar.svg\"></a>\n<a href=\"https://opencollective.com/rsschool/organization/1/website\"><img src=\"https://opencollective.com/rsschool/organization/1/avatar.svg\"></a>\n\n## License\n\n[Mozilla Public License 2.0](https://github.com/rolling-scopes/rsschool-app/blob/master/LICENSE)"
  },
  {
    "path": "VITEST.md",
    "content": "# Vitest Migration Plan\n\n## Goal\n\nMigrate all automated tests in this repository from Jest to latest Vitest with no feature regressions, stable CI reports, and same or better local developer experience\n\n## Current Baseline\n\n- Monorepo with three active workspaces: `client`, `server`, `nestjs`\n- Jest is used in all three workspaces through local scripts and dedicated config files\n- Current usage footprint is large (`421` Jest-related references in source and test files, across `97` files)\n- Current CI uploads `jest-junit-*.xml` artifacts and relies on per-workspace `test:ci`\n- Root ESLint setup loads `eslint-plugin-jest` test recommendations\n\n## Migration Strategy\n\n- Use phased migration with strict checkpoints\n- Keep repository green after each phase\n- Prefer adapter-compatible migration first (test runtime switch), then API cleanup\n- Migrate workspace by workspace, not all at once\n- Keep rollback path simple by limiting each pull request scope\n\n## Work Breakdown\n\n### Phase 0 - Planning and guardrails\n\n1. Create tracking issue and checklist for all steps in this document\n2. Freeze non-critical test refactors during migration window\n3. Define acceptance criteria\n   - all current test scripts have Vitest equivalents\n   - CI test jobs pass in all workspaces\n   - XML reports still published and consumed by report workflow\n   - no Jest runtime package remains in active dependencies\n4. Define branch strategy\n   - one umbrella branch for the migration\n   - optional child branches by workspace if team parallelizes\n5. Define rollback strategy\n   - each phase merged only after green CI\n   - keep isolated commits for dependency changes, config changes, and test API updates\n\n### Phase 1 - Repository inventory and mapping\n\n1. Build complete inventory of Jest touchpoints\n   - dependency graph (`jest`, `ts-jest`, `@types/jest`, `jest-environment-jsdom`, `jest-junit`, `jest-mock-axios`)\n   - config files (`client/jest.config.mjs`, `server/jest.config.mjs`, `nestjs/jest.config.mjs`, `nestjs/test/jest-e2e.json`)\n   - scripts in `package.json` files\n   - ESLint test plugin configuration\n   - CI workflow references to Jest output names\n2. Build test API usage inventory\n   - global APIs (`jest.fn`, `jest.spyOn`, `jest.mock`, fake timers, reset helpers)\n   - type usages (`jest.Mock`, `jest.mocked`)\n   - setup files and global matchers\n3. Build workspace-specific complexity score\n   - `client`: Next.js + jsdom + module mocks + `jest-mock-axios`\n   - `server`: Node + TypeScript transform + legacy backend\n   - `nestjs`: Node + Nest testing utilities + e2e config\n4. Lock expected command matrix for final validation\n   - root: `npm run lint`, `npm run test`, `npm run compile`, `npm run format`\n   - plus workspace-level targeted test runs during migration\n\n### Phase 2 - Target architecture decisions\n\n1. Choose Vitest version and lockfile policy\n2. Decide transform strategy for TypeScript in each workspace\n   - native Vitest + Vite transform where possible\n   - avoid keeping Jest-specific transform chain\n3. Decide coverage provider and reporter format parity\n4. Decide XML reporter replacement strategy for `jest-junit`\n   - select Vitest-compatible junit reporter\n   - preserve artifact naming or update workflows accordingly\n5. Decide lint strategy\n   - replace `eslint-plugin-jest` with Vitest-aware ruleset, or mixed mode during transition\n6. Decide test file naming policy\n   - keep current `.test`/`.spec` names for minimal churn\n7. Decide future approach for Nest e2e tests\n   - keep in Vitest as integration tests\n   - or keep separate runner temporarily with clear deprecation date\n\n### Phase 3 - Foundation changes at root level\n\n1. Update root dependencies\n   - add Vitest core and shared helpers\n   - remove root Jest packages when no longer needed\n2. Add shared base Vitest config pattern\n   - either reusable root config module or per-workspace local configs extending shared defaults\n3. Update root lint config for test files\n   - switch from Jest plugin defaults to Vitest-aware defaults\n4. Update root scripts only if needed for monorepo ergonomics\n   - keep `turbo run test` behavior unchanged for contributors\n5. Regenerate lockfile and confirm deterministic install\n\n### Phase 4 - Migrate client workspace\n\n1. Replace Jest config with Vitest config compatible with Next.js client tests\n2. Recreate jsdom environment setup and test globals\n3. Migrate `setupJest` file naming and imports to Vitest-compatible setup\n4. Replace Jest scripts in `client/package.json`\n   - `test`, `test:ci`, `test:watch`, `coverage`\n5. Replace or adapt `jest-mock-axios` usage\n   - evaluate native module mocking and spies first\n   - if replacement package is needed, add it explicitly\n6. Update test files in batches\n   - convert `jest.*` runtime calls to `vi.*`\n   - convert typed mocks from Jest types to Vitest types\n   - fix hoisting-sensitive mocks (`jest.mock` patterns)\n7. Run client tests repeatedly until green\n8. Ensure junit XML output generated for CI artifact upload\n\n### Phase 5 - Migrate server workspace\n\n1. Replace `server/jest.config.mjs` with Vitest config for Node environment\n2. Replace Jest scripts in `server/package.json`\n3. Convert server test runtime APIs from `jest.*` to `vi.*`\n4. Validate module resolution parity (`moduleDirectories`, aliases, TS paths)\n5. Validate timers and mocks behavior in legacy service tests\n6. Run server test suite and coverage commands\n7. Ensure CI XML report is emitted at expected path\n\n### Phase 6 - Migrate nestjs workspace\n\n1. Replace `nestjs/jest.config.mjs` with Vitest config\n2. Replace `nestjs/test/jest-e2e.json` strategy\n   - either merge into Vitest project config\n   - or split unit/integration projects in a single Vitest config\n3. Replace Jest scripts in `nestjs/package.json`\n4. Convert Nest unit tests from `jest.*` to `vi.*`\n5. Validate compatibility with Nest testing module patterns\n6. Rework debug script strategy (`test:debug`) for Vitest runtime\n7. Migrate e2e command to Vitest-equivalent workflow\n8. Ensure junit XML report is emitted at expected path\n\n### Phase 7 - CI and reporting migration\n\n1. Update `.github/workflows/pull_request.yml`\n   - keep per-workspace test jobs\n   - update artifact paths and names if reporter file names changed\n2. Update `.github/workflows/test_report.yml`\n   - replace any Jest-specific reporter assumptions\n   - ensure parsed reports still annotate PRs\n3. Update deploy workflow test command assumptions where relevant\n4. Validate that all workflows still run under current Node version policy\n\n### Phase 8 - TypeScript and tooling cleanup\n\n1. Remove Jest-only types from tsconfig include/types if present\n2. Ensure Vitest globals typing configured per workspace or explicit imports used\n3. Remove obsolete Jest dependencies and config files\n4. Remove stale test setup filenames referencing Jest\n5. Remove dead helper code or mocks created only for Jest behavior\n\n### Phase 9 - Verification and hardening\n\n1. Full local validation\n   - `npm run lint`\n   - `npm run test`\n   - `npm run compile`\n   - `npm run format`\n2. Spot-check changed tests for deterministic behavior\n   - fake timers\n   - async expectations\n   - module mocks\n3. Run CI on migration branch and verify all jobs green\n4. Compare test duration before/after and capture regression notes\n5. Compare coverage before/after and capture gaps\n\n### Phase 10 - Documentation and handoff\n\n1. Update contributor docs\n   - root `README.md` testing section if needed\n   - `CONTRIBUTING.md` test commands and local workflow\n   - workspace READMEs if they mention Jest\n2. Add migration notes\n   - known incompatibilities\n   - new mocking patterns\n   - timer usage guidance\n3. Create follow-up backlog for non-blocking improvements\n   - optimize slow tests\n   - improve test isolation\n   - enforce new lint rules for Vitest patterns\n\n## Execution Model for LLM Worker\n\n### Operating rules\n\n1. Work in small PRs with one phase or one workspace per PR\n2. Keep PRs reviewable (target under ~400 changed lines unless mechanical rename)\n3. Do not mix runtime migration with unrelated refactors\n4. After each PR, run full required checks and attach outputs\n5. If migration blocks on one workspace, ship completed workspace migrations first behind stable CI\n\n### Suggested PR sequence\n\n1. PR 1: root dependencies, lint plugin transition, shared Vitest base\n2. PR 2: client migration\n3. PR 3: server migration\n4. PR 4: nestjs migration including e2e strategy\n5. PR 5: CI/reporting cleanup and docs\n6. PR 6: final Jest removal and dead-code cleanup\n\n### Definition of done\n\n- No active workspace uses Jest runtime commands\n- No active workspace depends on Jest-only packages\n- All local required commands pass at root\n- CI test and reporting workflows pass with Vitest outputs\n- Documentation reflects Vitest-based workflow\n\n## Risk Register and Mitigations\n\n1. Mock hoisting differences break tests\n   - mitigate with batch conversion and per-file verification\n2. Timer behavior differences create flaky tests\n   - mitigate by standardizing fake timer lifecycle in setup/teardown\n3. Next.js client transform differences cause import failures\n   - mitigate by validating module transform and alias mapping early in client phase\n4. Nest e2e behavior differs from Jest config assumptions\n   - mitigate by isolating e2e migration and validating with dedicated command before merge\n5. CI report parser mismatch after reporter switch\n   - mitigate by updating report workflow and testing artifact parsing in draft PR\n6. Hidden Jest references survive in tooling\n   - mitigate with final repository-wide scan for `jest` tokens and config filenames\n\n## Final Migration Checklist\n\n- [ ] Root dependencies switched to Vitest stack\n- [ ] ESLint test rules updated for Vitest\n- [ ] Client tests green on Vitest\n- [ ] Server tests green on Vitest\n- [ ] NestJS tests green on Vitest\n- [ ] NestJS e2e path migrated or formally isolated with deadline\n- [ ] CI test jobs green\n- [ ] CI test reports parsed and published\n- [ ] Jest configs removed\n- [ ] Jest packages removed from lockfile and package manifests\n- [ ] Root validation commands pass\n- [ ] Documentation updated"
  },
  {
    "path": "client/.dockerignore",
    "content": ".next/cache\n# Keep empty dir. Next.js scans it on startup even with CDN assets\n.next/static/**\n!.next/static/\nnode_modules\n.turbo\n"
  },
  {
    "path": "client/Dockerfile",
    "content": "FROM node:24-alpine\n\nEXPOSE 8080\n\nENV NODE_ENV=production\nENV NODE_PORT=8080\n\nWORKDIR /app\n\nCOPY client/next.config.mjs /app/client/next.config.mjs\nCOPY client/next.config.prod.mjs /app/client/next.config.prod.mjs\nCOPY client/package.json /app/client/package.json\nCOPY client/public /app/client/public\n\nCOPY package.json /app\nCOPY package-lock.json /app\n\nRUN npm ci --production --no-optional\n\nCOPY client/.next /app/client/.next\n\nCMD cd /app/client && npm run prod\n"
  },
  {
    "path": "client/Dockerfile.lambda",
    "content": "FROM node:24-bullseye-slim AS builder\n\nENV NEXT_RUNTIME=nodejs\n\nWORKDIR /container_out\n\nCOPY package.json package.json\nCOPY package-lock.json package-lock.json\nCOPY client/public client/public\nCOPY client/package.json client/package.json\nCOPY client/next.config.prod.mjs client/next.config.mjs\n\nRUN npm ci --production --no-optional\n\n# .next build folder\nCOPY client/.next client/.next\n\n# Lambda Container with AWS Lambda Web Adapter\nFROM node:24-bullseye-slim\n\nENV NODE_ENV=production\nENV TZ=utc\nENV NEXT_RUNTIME=nodejs\nENV PORT=8080\nENV AWS_LWA_PORT=8080\n\nWORKDIR /var/task\n\nCOPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.0-rc1 /lambda-adapter /opt/extensions/lambda-adapter\n\nCOPY --from=builder /container_out /var/task/\n\nCMD [ \"npm\", \"run\", \"prod\", \"--prefix\", \"/var/task/client\" ]\n"
  },
  {
    "path": "client/README.md",
    "content": "# Client\n\nThis document defines the unified client architecture for the project.\n\n## Core Principles\n\n- Feature ownership lives in `modules/`.\n- Shared code lives in `shared/`.\n- Domain rules live in `entities/`.\n- Pages are thin route wrappers.\n\n## Folder Structure\n\n```\nclient/src/\n  pages/          # Next.js routes (thin wrappers only)\n  modules/        # Feature modules (vertical slices)\n  shared/         # Cross-feature components, hooks, utils, services\n  entities/       # Domain models and pure logic\n  api/            # OpenAPI generated client (read-only)\n  providers/      # App-level providers\n  styles/         # Global CSS (minimal)\n  configs/        # Global config\n```\n\n## Module Template\n\n```\nmodules/<Feature>/\n  components/\n  hooks/\n  pages/\n  services/\n  data/\n  utils/\n  styles/          # CSS modules only\n  types.ts\n  index.ts\n```\n\n## Architecture Rules\n\n### Pages\n\n- `pages/**` should only:\n  - Compose providers.\n  - Render a module page component.\n- No business logic in pages.\n\n### Modules\n\n- Feature-specific UI, hooks, and services remain in the module.\n- Modules can import from `shared/`, `entities/`, or `api/`.\n- Modules should not import from other modules directly.\n\n### Shared\n\n- `shared/` holds cross-feature utilities and UI.\n- Only move to `shared/` if used in at least 2 modules.\n- `shared/` must not import from `modules/`.\n\n### Entities\n\n- `entities/` contains domain types and pure logic.\n- No React or side effects in `entities/`.\n\n### Components\n\n- Single file components should be placed into a file with the same name as the component but with the `.tsx` extension.\n- If the component is complex and requires multiple files (many internal only components), it should be placed in a folder with the same name as the component. The folder should contain an `index.ts` file that exports the final component.\n\n### API Usage\n\n- `/api/v2/*` -> OpenAPI client only (`client/src/api`).\n- `/api/*` -> Axios services only.\n- Wrap API access inside module services for testability.\n\n### Styling\n\n- CSS modules only: `*.module.css`.\n- styled-jsx is deprecated and must be removed when files are touched.\n- Keep `styles/` minimal for global resets and theme-level CSS.\n\n### Naming & Tests\n\n- Consistent folder naming within modules.\n- Tests live next to the source file and have the same name as the source file but with the `.test.ts(x)` extension.\n\n### Imports\n\n- Use path aliases (`@client/modules`, `@client/shared`, `@client/api`).\n- Avoid deep relative imports across module boundaries.\n\n## Migration Policy\n\n- Refactor only when a file is touched.\n- Convert styled-jsx to CSS modules during touch-based changes.\n- Do not initiate large-scale rewrites without approval.\n\n## Enforcement (Planned)\n\n- ESLint boundary rules to prevent cross-module imports.\n- ESLint rule to ban styled-jsx usage (error level).\n- Lint rule for page thinness.\n\n## Example Page Wrapper\n\n```tsx\nimport { SessionProvider } from '@client/modules/Course/contexts';\nimport { ScorePage } from '@client/modules/Score/pages/ScorePage';\n\nexport default function Page() {\n  return (\n    <SessionProvider>\n      <ScorePage />\n    </SessionProvider>\n  );\n}\n```"
  },
  {
    "path": "client/eslint.config.mjs",
    "content": "import path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport boundaries from 'eslint-plugin-boundaries';\nimport testingLibrary from 'eslint-plugin-testing-library';\nimport defaultConfig from '../eslint.config.mjs';\n\nexport default [\n  ...defaultConfig,\n  {\n    ...testingLibrary.configs['flat/react'],\n    files: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],\n  },\n  {\n    files: ['src/**/*.{ts,tsx}'],\n    plugins: {\n      boundaries,\n    },\n    settings: {\n      'import/resolver': {\n        typescript: {\n          project: path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json'),\n        },\n      },\n      'boundaries/include': ['src/**/*.{ts,tsx}'],\n      'boundaries/ignore': ['src/**/*.css'],\n      'boundaries/elements': [\n        { type: 'pages', pattern: 'src/pages/**/*.{ts,tsx}', mode: 'file' },\n        { type: 'modules', pattern: 'src/modules/*', capture: ['moduleName'] },\n        { type: 'shared', pattern: 'src/shared/**/*.{ts,tsx}', mode: 'file' },\n      ],\n    },\n    rules: {\n      'boundaries/element-types': [\n        'warn',\n        {\n          default: 'allow',\n          rules: [\n            {\n              from: 'modules',\n              disallow: 'modules',\n              message: 'Modules must not import other modules directly.',\n            },\n            {\n              from: 'shared',\n              disallow: 'modules',\n              message: 'Shared code must not import from modules.',\n            },\n          ],\n        },\n      ],\n      'no-restricted-imports': [\n        'error',\n        {\n          paths: [\n            {\n              name: 'styled-jsx/css',\n              message: \"The 'jsx' attribute from styled-jsx is deprecated. Use CSS modules instead.\",\n            },\n            {\n              name: 'styled-jsx',\n              message: \"The 'jsx' attribute from styled-jsx is deprecated. Use CSS modules instead.\",\n            },\n          ],\n        },\n      ],\n      'no-restricted-syntax': [\n        'error',\n        {\n          selector: \"JSXAttribute[name.name='jsx']\",\n          message: \"The 'jsx' attribute from styled-jsx is deprecated. Use CSS modules instead.\",\n        },\n      ],\n    },\n  },\n];\n"
  },
  {
    "path": "client/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport './.next/types/routes.d.ts';\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "client/next.config.mjs",
    "content": "import prodConfig from './next.config.prod.mjs';\n\nconst isProd = process.env.NODE_ENV === 'production';\n\nconst server = process.env.SERVER_HOST || 'http://localhost:3001';\nconst nestjs = process.env.NESTJS_HOST || 'http://localhost:3002';\n\nconst nextConfig = {\n  ...prodConfig,\n  rewrites: () =>\n    isProd\n      ? []\n      : [\n          { source: '/certificate/:path*', destination: `${nestjs}/certificate/:path*` },\n          { source: '/swagger', destination: `${nestjs}/swagger/` },\n          { source: '/swagger-json', destination: `${nestjs}/swagger-json` },\n          { source: '/swagger:path', destination: `${nestjs}/swagger/swagger:path` },\n          { source: '/api/v2/:path*', destination: `${nestjs}/:path*` },\n          { source: '/api/:path*', destination: `${server}/:path*` },\n        ],\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "client/next.config.prod.mjs",
    "content": "const isProd = process.env.NODE_ENV === 'production';\n\nconst nextConfig = {\n  assetPrefix: isProd ? 'https://cdn.rs.school' : '',\n  env: {\n    BUILD_VERSION: process.env.BUILD_VERSION || '0.0.0.0.0',\n    APP_VERSION: process.env.APP_VERSION,\n    RSSHCOOL_UI_GCP_MAPS_API_KEY: process.env.RSSHCOOL_UI_GCP_MAPS_API_KEY,\n    CDN_HOST: process.env.CDN_HOST || '',\n    RSSCHOOL_DEV_TOOLS: process.env.RSSCHOOL_DEV_TOOLS || 'false',\n  },\n};\n\nexport default nextConfig;\n"
  },
  {
    "path": "client/package.json",
    "content": "{\n  \"name\": \"client\",\n  \"private\": true,\n  \"version\": \"1.0.0\",\n  \"license\": \"Mozilla Public License 2.0\",\n  \"browserslist\": [\n    \"> 1%\"\n  ],\n  \"scripts\": {\n    \"start\": \"next dev\",\n    \"build\": \"next build\",\n    \"compile\": \"tsc -p tsconfig.json --noEmit\",\n    \"prod\": \"next start -p 8080\",\n    \"lint\": \"eslint src\",\n    \"test\": \"vitest run\",\n    \"test:ci\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:e2e\": \"npx playwright test\",\n    \"coverage\": \"vitest run --coverage\"\n  },\n  \"dependencies\": {\n    \"@ant-design/cssinjs\": \"^2.1.0\",\n    \"@ant-design/icons\": \"6.1.0\",\n    \"@ant-design/plots\": \"2.6.8\",\n    \"@dnd-kit/core\": \"^6.3.1\",\n    \"@dnd-kit/modifiers\": \"^9.0.0\",\n    \"@dnd-kit/sortable\": \"^10.0.0\",\n    \"@dnd-kit/utilities\": \"^3.2.2\",\n    \"ahooks\": \"3.8.4\",\n    \"antd\": \"6.3.1\",\n    \"clsx\": \"2.1.1\",\n    \"cookie\": \"0.7.0\",\n    \"csvtojson\": \"2.0.10\",\n    \"next\": \"^16.1.6\",\n    \"react\": \"18.3.1\",\n    \"react-dom\": \"18.3.1\",\n    \"react-markdown\": \"8.0.7\",\n    \"react-masonry-css\": \"1.0.16\",\n    \"react-quill\": \"2.0.0\",\n    \"react-use\": \"17.4.0\",\n    \"remark-gfm\": \"3.0.1\",\n    \"use-places-autocomplete\": \"4.0.1\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.51.1\",\n    \"@testing-library/jest-dom\": \"6.1.4\",\n    \"@testing-library/react\": \"14.1.2\",\n    \"@testing-library/user-event\": \"14.5.1\",\n    \"@types/aws-lambda\": \"8.10.126\",\n    \"@types/cookie\": \"0.5.4\",\n    \"@types/react\": \"18.3.18\",\n    \"@types/react-dom\": \"18.3.5\",\n    \"eslint-plugin-testing-library\": \"7.16.0\",\n    \"mq-polyfill\": \"1.1.8\"\n  },\n  \"nextBundleAnalysis\": {\n    \"budget\": 512000,\n    \"budgetPercentIncreaseRed\": 5,\n    \"showDetails\": true\n  }\n}\n"
  },
  {
    "path": "client/playwright.config.ts",
    "content": "import type { PlaywrightTestConfig } from '@playwright/test';\nimport { devices } from '@playwright/test';\n\n/**\n * Read environment variables from file.\n * https://github.com/motdotla/dotenv\n */\n// require('dotenv').config();\n\n/**\n * See https://playwright.dev/docs/test-configuration.\n */\nconst config: PlaywrightTestConfig = {\n  testDir: './specs',\n  /* Maximum time one test can run for. */\n  timeout: 30 * 1000,\n  expect: {\n    /**\n     * Maximum time expect() should wait for the condition to be met.\n     * For example in `await expect(locator).toHaveText();`\n     */\n    timeout: 3000,\n  },\n  /* Fail the build on CI if you accidentally left test.only in the source code. */\n  forbidOnly: !!process.env.CI,\n  /* Retry on CI only */\n  retries: process.env.CI ? 1 : 0,\n  /* Opt out of parallel tests on CI. */\n  workers: process.env.CI ? 1 : undefined,\n  /* Reporter to use. See https://playwright.dev/docs/test-reporters */\n  reporter: 'html',\n  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */\n  use: {\n    /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */\n    actionTimeout: 0,\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    // baseURL: 'http://localhost:3000',\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: 'on-first-retry',\n    screenshot: 'on',\n  },\n\n  /* Configure projects for major browsers */\n  projects: [\n    {\n      name: 'chromium',\n      use: {\n        ...devices['Desktop Chrome'],\n      },\n    },\n  ],\n};\n\nexport default config;\n"
  },
  {
    "path": "client/public/static/empty.txt",
    "content": ""
  },
  {
    "path": "client/specs/smoke.spec.ts",
    "content": "import { test, expect } from '@playwright/test';\n\nconst url = process.env.BASE_URL || 'http://localhost:3000';\n\ntest.beforeEach(async ({ page }) => {\n  await page.context().clearCookies();\n  await page.goto(url);\n  await page.getByRole('button', { name: /sign up with gitHub/i }).click();\n  await page.waitForSelector('.page-header');\n});\n\ntest.describe('Home', () => {\n  test('should have link to Score page', async ({ page }) => {\n    expect(await page.locator('css=a >> text=\"Score\"').isVisible()).toBe(true);\n\n    const href = await page.locator('css=a >> text=\"Score\"').getAttribute('href');\n    expect(href?.includes('/course/score')).toBeTruthy();\n  });\n\n  test('should navigate to Courses page and verify threshold', async ({ page }) => {\n    await page.click('span[role=\"img\"][aria-label=\"menu-unfold\"]');\n\n    await page.click('text=Admin Area');\n\n    const coursesLink = page.locator('text=Courses');\n    await expect(coursesLink).toBeVisible();\n    await coursesLink.click();\n\n    await page.getByRole('button', { name: /add course/i }).click();\n\n    const certificateThreshold = page.locator('input#certificateThreshold');\n    await expect(certificateThreshold).toHaveValue('70');\n  });\n});\n"
  },
  {
    "path": "client/src/__mocks__/axios.js",
    "content": "import { vi } from 'vitest';\n\nconst mockAxios = {\n  get: vi.fn(),\n  post: vi.fn(),\n  put: vi.fn(),\n  patch: vi.fn(),\n  delete: vi.fn(),\n  create: vi.fn(() => mockAxios),\n  defaults: { headers: { common: {} } },\n  interceptors: {\n    request: { use: vi.fn(), eject: vi.fn() },\n    response: { use: vi.fn(), eject: vi.fn() },\n  },\n  reset() {\n    this.get.mockReset();\n    this.post.mockReset();\n    this.put.mockReset();\n    this.patch.mockReset();\n    this.delete.mockReset();\n  },\n};\n\nexport default mockAxios;\n"
  },
  {
    "path": "client/src/__mocks__/hooks/index.ts",
    "content": "export { useMessage } from './useMessage';\nexport { useTheme } from './useTheme';\n"
  },
  {
    "path": "client/src/__mocks__/hooks/useMessage.tsx",
    "content": "import { vi } from 'vitest';\n\nexport const useMessage = () => ({\n  message: {\n    success: vi.fn(),\n    error: vi.fn(),\n    info: vi.fn(),\n    warning: vi.fn(),\n  },\n  notification: {\n    success: vi.fn(),\n    error: vi.fn(),\n    info: vi.fn(),\n    warning: vi.fn(),\n    open: vi.fn(),\n    close: vi.fn(),\n  },\n});\n"
  },
  {
    "path": "client/src/__mocks__/hooks/useTheme.tsx",
    "content": "import { vi } from 'vitest';\n\nexport const useTheme = () => ({\n  theme: 'light',\n  themeChange: vi.fn(),\n  autoTheme: true,\n  changeAutoTheme: vi.fn(),\n});\n"
  },
  {
    "path": "client/src/__mocks__/next/config.ts",
    "content": "export default function getConfig() {\n  return {};\n}\n"
  },
  {
    "path": "client/src/__tests__/ProfilePage.test.tsx",
    "content": "import { Session } from '@client/components/withSession';\nimport ProfilePage from '@client/pages/profile';\nimport { render } from '@testing-library/react';\nimport { SessionApi } from '@client/api';\nimport { NextRouter, useRouter } from 'next/router';\n\nvi.mock('next/config', () => () => ({}));\nvi.mock('next/router', async () => ({\n  ...(await vi.importActual('next/router')),\n  useRouter: vi.fn(),\n}));\n\nvi.mock('@client/api', async () => ({\n  ...(await vi.importActual('@client/api')),\n  ProfileApi: vi.fn(),\n  UsersNotificationsApi: vi.fn(),\n  NotificationsApi: vi.fn(),\n  CoursesApi: vi.fn(),\n  CoursesTasksApi: vi.fn(),\n  StudentsScoreApi: vi.fn(),\n  UpdateUserDtoLanguagesEnum: {},\n}));\n\nconst session = {\n  id: 2020,\n  githubId: 'mikhama',\n  isAdmin: true,\n  isHirer: false,\n  isActivist: false,\n  courses: {\n    13: {\n      roles: ['manager'],\n    },\n    1: {\n      roles: ['mentor'],\n    },\n    2: {\n      roles: ['student'],\n    },\n  },\n} as Session;\n\nSessionApi.prototype.getSession = vi.fn().mockResolvedValue({ data: session });\n\nconst router = {\n  query: {\n    githubId: 'petrov',\n  },\n} as unknown as NextRouter;\n\ndescribe('ProfilePage', () => {\n  describe('Should render correctly', () => {\n    it('if full profile info is in the state', () => {\n      vi.mocked(useRouter).mockReturnValue(router);\n      const { container } = render(<ProfilePage />);\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/__tests__/__snapshots__/ProfilePage.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`ProfilePage > Should render correctly > if full profile info is in the state 1`] = `\n<div>\n  <div\n    class=\"_loadingScreen_7068aa\"\n    data-testid=\"loading-screen\"\n    style=\"justify-content: center; align-items: center; display: flex; width: 100vw; position: fixed; height: 100vh; background: rgb(255, 255, 255);\"\n  >\n    <div\n      aria-busy=\"true\"\n      aria-live=\"polite\"\n      class=\"ant-spin ant-spin-spinning css-dev-only-do-not-override-1enej14 css-var-root\"\n    >\n      <div\n        class=\"ant-spin-section\"\n      >\n        <span\n          class=\"ant-spin-dot-holder\"\n        >\n          <span\n            class=\"ant-spin-dot ant-spin-dot-spin\"\n          >\n            <i\n              class=\"ant-spin-dot-item\"\n            />\n            <i\n              class=\"ant-spin-dot-item\"\n            />\n            <i\n              class=\"ant-spin-dot-item\"\n            />\n            <i\n              class=\"ant-spin-dot-item\"\n            />\n          </span>\n        </span>\n        <div\n          class=\"ant-spin-description\"\n        >\n          Loading...\n        </div>\n      </div>\n      <div\n        class=\"ant-spin-container\"\n      >\n        <div\n          style=\"padding: 50px;\"\n        />\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/api/.gitignore",
    "content": "wwwroot/*.js\nnode_modules\ntypings\ndist\n"
  },
  {
    "path": "client/src/api/.npmignore",
    "content": "# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm"
  },
  {
    "path": "client/src/api/.openapi-generator/FILES",
    "content": ".gitignore\n.npmignore\napi.ts\nbase.ts\ncommon.ts\nconfiguration.ts\ngit_push.sh\nindex.ts\n"
  },
  {
    "path": "client/src/api/.openapi-generator/VERSION",
    "content": "5.4.0"
  },
  {
    "path": "client/src/api/.openapi-generator-ignore",
    "content": "# OpenAPI Generator Ignore\n# Generated by openapi-generator https://github.com/openapitools/openapi-generator\n\n# Use this file to prevent files from being overwritten by the generator.\n# The patterns follow closely to .gitignore or .dockerignore.\n\n# As an example, the C# client generator defines ApiClient.cs.\n# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:\n#ApiClient.cs\n\n# You can match any string of characters against a directory, file or extension with a single asterisk (*):\n#foo/*/qux\n# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux\n\n# You can recursively match patterns against a directory, file or extension with a double asterisk (**):\n#foo/**/qux\n# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux\n\n# You can also negate patterns with an exclamation (!).\n# For example, you can ignore all files in a docs folder with the file extension .md:\n#docs/*.md\n# Then explicitly reverse the ignore rule for a single file:\n#!docs/README.md\n"
  },
  {
    "path": "client/src/api/api.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * \n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 1.0.0\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport { Configuration } from './configuration';\nimport globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';\n// @ts-ignore\nimport { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from './base';\n\n/**\n * \n * @export\n * @interface ActivityDto\n */\nexport interface ActivityDto {\n    /**\n     * \n     * @type {number}\n     * @memberof ActivityDto\n     */\n    'lastActivityTime': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ActivityDto\n     */\n    'isActive': boolean;\n}\n/**\n * \n * @export\n * @interface AlertDto\n */\nexport interface AlertDto {\n    /**\n     * \n     * @type {number}\n     * @memberof AlertDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof AlertDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AlertDto\n     */\n    'text': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof AlertDto\n     */\n    'enabled': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof AlertDto\n     */\n    'courseId': number | null;\n    /**\n     * \n     * @type {string}\n     * @memberof AlertDto\n     */\n    'updatedDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AlertDto\n     */\n    'createdDate': string;\n}\n/**\n * \n * @export\n * @interface ApplicantResumeDto\n */\nexport interface ApplicantResumeDto {\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'uuid': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'avatarLink': string | null;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof ApplicantResumeDto\n     */\n    'visibleCourses': Array<number>;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'desiredPosition': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'email': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'englishLevel': ApplicantResumeDtoEnglishLevelEnum;\n    /**\n     * \n     * @type {number}\n     * @memberof ApplicantResumeDto\n     */\n    'expires': number | null;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ApplicantResumeDto\n     */\n    'fullTime': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof ApplicantResumeDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'linkedin': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'locations': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'militaryService': ApplicantResumeDtoMilitaryServiceEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'name': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'notes': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'phone': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'selfIntroLink': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'skype': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'startFrom': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'telegram': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ApplicantResumeDto\n     */\n    'website': string | null;\n}\n\nexport const ApplicantResumeDtoEnglishLevelEnum = {\n    Unknown: 'unknown',\n    A0: 'a0',\n    A1: 'a1',\n    A2: 'a2',\n    B1: 'b1',\n    B2: 'b2',\n    C1: 'c1',\n    C2: 'c2'\n} as const;\n\nexport type ApplicantResumeDtoEnglishLevelEnum = typeof ApplicantResumeDtoEnglishLevelEnum[keyof typeof ApplicantResumeDtoEnglishLevelEnum];\nexport const ApplicantResumeDtoMilitaryServiceEnum = {\n    Served: 'served',\n    Liable: 'liable',\n    NotLiable: 'notLiable'\n} as const;\n\nexport type ApplicantResumeDtoMilitaryServiceEnum = typeof ApplicantResumeDtoMilitaryServiceEnum[keyof typeof ApplicantResumeDtoMilitaryServiceEnum];\n\n/**\n * \n * @export\n * @interface ApproveMentorDto\n */\nexport interface ApproveMentorDto {\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof ApproveMentorDto\n     */\n    'preselectedCourses': Array<string>;\n}\n/**\n * \n * @export\n * @interface Attributes\n */\nexport interface Attributes {\n    /**\n     * \n     * @type {string}\n     * @memberof Attributes\n     */\n    'template'?: string | null;\n}\n/**\n * \n * @export\n * @interface AuthConnectionDto\n */\nexport interface AuthConnectionDto {\n    /**\n     * \n     * @type {string}\n     * @memberof AuthConnectionDto\n     */\n    'channelId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AuthConnectionDto\n     */\n    'externalId': string;\n}\n/**\n * \n * @export\n * @interface AuthUserDto\n */\nexport interface AuthUserDto {\n    /**\n     * \n     * @type {number}\n     * @memberof AuthUserDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof AuthUserDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {{ [key: string]: string; }}\n     * @memberof AuthUserDto\n     */\n    'roles': { [key: string]: string; };\n    /**\n     * \n     * @type {boolean}\n     * @memberof AuthUserDto\n     */\n    'isAdmin': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof AuthUserDto\n     */\n    'isHirer': boolean;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof AuthUserDto\n     */\n    'appRoles': Array<string>;\n    /**\n     * \n     * @type {{ [key: string]: AuthUserDtoCourses; }}\n     * @memberof AuthUserDto\n     */\n    'courses': { [key: string]: AuthUserDtoCourses; };\n}\n\nexport const AuthUserDtoRolesEnum = {\n    Mentor: 'mentor',\n    Student: 'student'\n} as const;\n\nexport type AuthUserDtoRolesEnum = typeof AuthUserDtoRolesEnum[keyof typeof AuthUserDtoRolesEnum];\n\n/**\n * \n * @export\n * @interface AuthUserDtoCourses\n */\nexport interface AuthUserDtoCourses {\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof AuthUserDtoCourses\n     */\n    'roles': Array<AuthUserDtoCoursesRolesEnum>;\n}\n\nexport const AuthUserDtoCoursesRolesEnum = {\n    Manager: 'manager',\n    Supervisor: 'supervisor',\n    Student: 'student',\n    Mentor: 'mentor',\n    Dementor: 'dementor',\n    Activist: 'activist'\n} as const;\n\nexport type AuthUserDtoCoursesRolesEnum = typeof AuthUserDtoCoursesRolesEnum[keyof typeof AuthUserDtoCoursesRolesEnum];\n\n/**\n * \n * @export\n * @interface AutoTestAttributesDto\n */\nexport interface AutoTestAttributesDto {\n    /**\n     * \n     * @type {PublicAttributesDto}\n     * @memberof AutoTestAttributesDto\n     */\n    'public': PublicAttributesDto;\n    /**\n     * \n     * @type {Array<Array<number>>}\n     * @memberof AutoTestAttributesDto\n     */\n    'answers': Array<Array<number>>;\n}\n/**\n * \n * @export\n * @interface AutoTestTaskDto\n */\nexport interface AutoTestTaskDto {\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'type': AutoTestTaskDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof AutoTestTaskDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'githubRepoName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'sourceGithubRepoUrl': string;\n    /**\n     * \n     * @type {IdNameDto}\n     * @memberof AutoTestTaskDto\n     */\n    'discipline': IdNameDto;\n    /**\n     * \n     * @type {boolean}\n     * @memberof AutoTestTaskDto\n     */\n    'githubPrRequired': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AutoTestTaskDto\n     */\n    'updatedDate': string;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof AutoTestTaskDto\n     */\n    'tags': Array<string>;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof AutoTestTaskDto\n     */\n    'skills': Array<string>;\n    /**\n     * \n     * @type {AutoTestAttributesDto}\n     * @memberof AutoTestTaskDto\n     */\n    'attributes': AutoTestAttributesDto;\n    /**\n     * \n     * @type {Array<UsedCourseDto>}\n     * @memberof AutoTestTaskDto\n     */\n    'courses': Array<UsedCourseDto>;\n}\n\nexport const AutoTestTaskDtoTypeEnum = {\n    Jstask: 'jstask',\n    Kotlintask: 'kotlintask',\n    Objctask: 'objctask',\n    Htmltask: 'htmltask',\n    Ipynb: 'ipynb',\n    Selfeducation: 'selfeducation',\n    Codewars: 'codewars',\n    Test: 'test',\n    Codejam: 'codejam',\n    Interview: 'interview',\n    StageInterview: 'stage-interview',\n    Cvhtml: 'cv:html',\n    Cvmarkdown: 'cv:markdown'\n} as const;\n\nexport type AutoTestTaskDtoTypeEnum = typeof AutoTestTaskDtoTypeEnum[keyof typeof AutoTestTaskDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface AvailableReviewStatsDto\n */\nexport interface AvailableReviewStatsDto {\n    /**\n     * \n     * @type {string}\n     * @memberof AvailableReviewStatsDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof AvailableReviewStatsDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof AvailableReviewStatsDto\n     */\n    'checksCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof AvailableReviewStatsDto\n     */\n    'completedChecksCount': number;\n}\n/**\n * \n * @export\n * @interface AvailableStudentDto\n */\nexport interface AvailableStudentDto {\n    /**\n     * \n     * @type {number}\n     * @memberof AvailableStudentDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof AvailableStudentDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AvailableStudentDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof AvailableStudentDto\n     */\n    'cityName': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof AvailableStudentDto\n     */\n    'countryName': string | null;\n    /**\n     * \n     * @type {boolean}\n     * @memberof AvailableStudentDto\n     */\n    'isGoodCandidate': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof AvailableStudentDto\n     */\n    'rating': string | null;\n    /**\n     * \n     * @type {number}\n     * @memberof AvailableStudentDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {string}\n     * @memberof AvailableStudentDto\n     */\n    'registeredDate': string;\n    /**\n     * \n     * @type {number}\n     * @memberof AvailableStudentDto\n     */\n    'maxScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof AvailableStudentDto\n     */\n    'feedbackVersion': number;\n}\n/**\n * \n * @export\n * @interface BadgeDto\n */\nexport interface BadgeDto {\n    /**\n     * \n     * @type {string}\n     * @memberof BadgeDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {BadgeEnum}\n     * @memberof BadgeDto\n     */\n    'id': BadgeEnum;\n}\n/**\n * \n * @export\n * @enum {string}\n */\n\nexport const BadgeEnum = {\n    Congratulations: 'Congratulations',\n    ExpertHelp: 'Expert_help',\n    GreatSpeaker: 'Great_speaker',\n    GoodJob: 'Good_job',\n    HelpingHand: 'Helping_hand',\n    Hero: 'Hero',\n    ThankYou: 'Thank_you',\n    OutstandingWork: 'Outstanding_work',\n    TopPerformer: 'Top_performer',\n    JobOffer: 'Job_Offer',\n    RsActivist: 'RS_activist',\n    JuryTeam: 'Jury_Team',\n    Mentor: 'Mentor',\n    Contributor: 'Contributor',\n    Coordinator: 'Coordinator',\n    Thanks: 'Thanks'\n} as const;\n\nexport type BadgeEnum = typeof BadgeEnum[keyof typeof BadgeEnum];\n\n\n/**\n * \n * @export\n * @interface BasicAutoTestTaskDto\n */\nexport interface BasicAutoTestTaskDto {\n    /**\n     * \n     * @type {number}\n     * @memberof BasicAutoTestTaskDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof BasicAutoTestTaskDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof BasicAutoTestTaskDto\n     */\n    'maxAttemptsNumber': number | null;\n    /**\n     * \n     * @type {number}\n     * @memberof BasicAutoTestTaskDto\n     */\n    'numberOfQuestions': number | null;\n    /**\n     * \n     * @type {number}\n     * @memberof BasicAutoTestTaskDto\n     */\n    'strictAttemptsMode': number | null;\n    /**\n     * \n     * @type {number}\n     * @memberof BasicAutoTestTaskDto\n     */\n    'thresholdPercentage': number | null;\n}\n/**\n * \n * @export\n * @interface ChannelSettings\n */\nexport interface ChannelSettings {\n    /**\n     * \n     * @type {string}\n     * @memberof ChannelSettings\n     */\n    'channelId': string;\n    /**\n     * \n     * @type {object}\n     * @memberof ChannelSettings\n     */\n    'template': object;\n}\n/**\n * \n * @export\n * @interface CheckScheduleChangesDto\n */\nexport interface CheckScheduleChangesDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CheckScheduleChangesDto\n     */\n    'lastHours': number;\n}\n/**\n * \n * @export\n * @interface CheckTasksDeadlineDto\n */\nexport interface CheckTasksDeadlineDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CheckTasksDeadlineDto\n     */\n    'deadlineInHours': number;\n}\n/**\n * \n * @export\n * @enum {string}\n */\n\nexport const CheckerEnum = {\n    AutoTest: 'auto-test',\n    Assigned: 'assigned',\n    Mentor: 'mentor',\n    TaskOwner: 'taskOwner',\n    CrossCheck: 'crossCheck'\n} as const;\n\nexport type CheckerEnum = typeof CheckerEnum[keyof typeof CheckerEnum];\n\n\n/**\n * \n * @export\n * @interface CommentMentorRegistryDto\n */\nexport interface CommentMentorRegistryDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CommentMentorRegistryDto\n     */\n    'comment': string | null;\n}\n/**\n * \n * @export\n * @interface ConsentDto\n */\nexport interface ConsentDto {\n    /**\n     * \n     * @type {boolean}\n     * @memberof ConsentDto\n     */\n    'consent': boolean;\n}\n/**\n * \n * @export\n * @interface ContactsDto\n */\nexport interface ContactsDto {\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'phone'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'email'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'epamEmail'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'skype'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'whatsApp'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'telegram'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'notes'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'linkedIn'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ContactsDto\n     */\n    'discord'?: string | null;\n}\n/**\n * \n * @export\n * @interface ContributorDto\n */\nexport interface ContributorDto {\n    /**\n     * \n     * @type {string}\n     * @memberof ContributorDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {ContributorUserDto}\n     * @memberof ContributorDto\n     */\n    'user': ContributorUserDto;\n    /**\n     * \n     * @type {number}\n     * @memberof ContributorDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ContributorDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ContributorDto\n     */\n    'updatedDate': string;\n}\n/**\n * \n * @export\n * @interface ContributorUserDto\n */\nexport interface ContributorUserDto {\n    /**\n     * \n     * @type {number}\n     * @memberof ContributorUserDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ContributorUserDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ContributorUserDto\n     */\n    'firstName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ContributorUserDto\n     */\n    'lastName': string;\n}\n/**\n * \n * @export\n * @interface CountriesStatsDto\n */\nexport interface CountriesStatsDto {\n    /**\n     * \n     * @type {Array<CountryStatDto>}\n     * @memberof CountriesStatsDto\n     */\n    'countries': Array<CountryStatDto>;\n}\n/**\n * \n * @export\n * @interface CountryDto\n */\nexport interface CountryDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CountryDto\n     */\n    'countryName': string;\n}\n/**\n * \n * @export\n * @interface CountryStatDto\n */\nexport interface CountryStatDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CountryStatDto\n     */\n    'countryName': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CountryStatDto\n     */\n    'count': number;\n}\n/**\n * \n * @export\n * @interface CourseAggregateStatsDto\n */\nexport interface CourseAggregateStatsDto {\n    /**\n     * \n     * @type {CountriesStatsDto}\n     * @memberof CourseAggregateStatsDto\n     */\n    'studentsCountries': CountriesStatsDto;\n    /**\n     * \n     * @type {CourseStatsDto}\n     * @memberof CourseAggregateStatsDto\n     */\n    'studentsStats': CourseStatsDto;\n    /**\n     * \n     * @type {CountriesStatsDto}\n     * @memberof CourseAggregateStatsDto\n     */\n    'mentorsCountries': CountriesStatsDto;\n    /**\n     * \n     * @type {CourseMentorsStatsDto}\n     * @memberof CourseAggregateStatsDto\n     */\n    'mentorsStats': CourseMentorsStatsDto;\n    /**\n     * \n     * @type {Array<CourseTaskDto>}\n     * @memberof CourseAggregateStatsDto\n     */\n    'courseTasks': Array<CourseTaskDto>;\n    /**\n     * \n     * @type {CountriesStatsDto}\n     * @memberof CourseAggregateStatsDto\n     */\n    'studentsCertificatesCountries': CountriesStatsDto;\n}\n/**\n * \n * @export\n * @interface CourseCopyFromDto\n */\nexport interface CourseCopyFromDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseCopyFromDto\n     */\n    'copyFromCourseId': number;\n}\n/**\n * \n * @export\n * @interface CourseDto\n */\nexport interface CourseDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'updatedDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'fullName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'alias': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseDto\n     */\n    'year': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'registrationEndDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'primarySkillId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'primarySkillName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'locationName': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseDto\n     */\n    'discordServerId': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseDto\n     */\n    'completed': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseDto\n     */\n    'planned': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseDto\n     */\n    'inviteOnly': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'certificateIssuer': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseDto\n     */\n    'usePrivateRepositories': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseDto\n     */\n    'personalMentoring': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'personalMentoringStartDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'personalMentoringEndDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'logo': string;\n    /**\n     * \n     * @type {IdNameDto}\n     * @memberof CourseDto\n     */\n    'discipline': IdNameDto | null;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseDto\n     */\n    'minStudentsPerMentor': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseDto\n     */\n    'certificateThreshold': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseDto\n     */\n    'wearecommunityUrl': string | null;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof CourseDto\n     */\n    'certificateDisciplines': Array<number> | null;\n}\n/**\n * \n * @export\n * @interface CourseEventDto\n */\nexport interface CourseEventDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseEventDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseEventDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseEventDto\n     */\n    'type': CourseEventDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseEventDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseEventDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseEventDto\n     */\n    'dateTime': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseEventDto\n     */\n    'endTime': string;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof CourseEventDto\n     */\n    'organizer': PersonDto;\n}\n\nexport const CourseEventDtoTypeEnum = {\n    LectureOnline: 'lecture_online',\n    LectureOffline: 'lecture_offline',\n    LectureMixed: 'lecture_mixed',\n    LectureSelfStudy: 'lecture_self_study',\n    Warmup: 'warmup',\n    Info: 'info',\n    Workshop: 'workshop',\n    Meetup: 'meetup',\n    CrossCheckDeadline: 'cross_check_deadline',\n    Webinar: 'webinar',\n    Special: 'special'\n} as const;\n\nexport type CourseEventDtoTypeEnum = typeof CourseEventDtoTypeEnum[keyof typeof CourseEventDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface CourseMentorsStatsDto\n */\nexport interface CourseMentorsStatsDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseMentorsStatsDto\n     */\n    'mentorsActiveCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseMentorsStatsDto\n     */\n    'mentorsTotalCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseMentorsStatsDto\n     */\n    'epamMentorsCount': number;\n}\n/**\n * \n * @export\n * @interface CourseRecord\n */\nexport interface CourseRecord {\n    /**\n     * \n     * @type {string}\n     * @memberof CourseRecord\n     */\n    'courseName': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseRecord\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface CourseRolesDto\n */\nexport interface CourseRolesDto {\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseRolesDto\n     */\n    'isManager': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseRolesDto\n     */\n    'isSupervisor': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseRolesDto\n     */\n    'isDementor': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseRolesDto\n     */\n    'isActivist': boolean;\n}\n/**\n * \n * @export\n * @interface CourseScheduleItemDto\n */\nexport interface CourseScheduleItemDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseScheduleItemDto\n     */\n    'score': number | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseScheduleItemDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'status': CourseScheduleItemDtoStatusEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'crossCheckEndDate': string;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof CourseScheduleItemDto\n     */\n    'organizer': PersonDto | null;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseScheduleItemDto\n     */\n    'maxScore': number | null;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseScheduleItemDto\n     */\n    'scoreWeight': number | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'descriptionUrl': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'tag': CourseScheduleItemDtoTagEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleItemDto\n     */\n    'type': CourseScheduleItemDtoTypeEnum;\n}\n\nexport const CourseScheduleItemDtoStatusEnum = {\n    Done: 'done',\n    Available: 'available',\n    Archived: 'archived',\n    Future: 'future',\n    Missed: 'missed',\n    Review: 'review',\n    Registered: 'registered',\n    Unavailable: 'unavailable'\n} as const;\n\nexport type CourseScheduleItemDtoStatusEnum = typeof CourseScheduleItemDtoStatusEnum[keyof typeof CourseScheduleItemDtoStatusEnum];\nexport const CourseScheduleItemDtoTagEnum = {\n    Lecture: 'lecture',\n    Coding: 'coding',\n    SelfStudy: 'self-study',\n    Interview: 'interview',\n    CrossCheckSubmit: 'cross-check-submit',\n    CrossCheckReview: 'cross-check-review',\n    Test: 'test',\n    TeamDistribution: 'team-distribution'\n} as const;\n\nexport type CourseScheduleItemDtoTagEnum = typeof CourseScheduleItemDtoTagEnum[keyof typeof CourseScheduleItemDtoTagEnum];\nexport const CourseScheduleItemDtoTypeEnum = {\n    CourseTask: 'courseTask',\n    CourseEvent: 'courseEvent',\n    CourseTeamDistribution: 'courseTeamDistribution'\n} as const;\n\nexport type CourseScheduleItemDtoTypeEnum = typeof CourseScheduleItemDtoTypeEnum[keyof typeof CourseScheduleItemDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface CourseScheduleTokenDto\n */\nexport interface CourseScheduleTokenDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CourseScheduleTokenDto\n     */\n    'token': string;\n}\n/**\n * \n * @export\n * @interface CourseStatsDto\n */\nexport interface CourseStatsDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseStatsDto\n     */\n    'activeStudentsCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseStatsDto\n     */\n    'totalStudents': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseStatsDto\n     */\n    'studentsWithMentorCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseStatsDto\n     */\n    'certifiedStudentsCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseStatsDto\n     */\n    'eligibleForCertificationCount': number;\n}\n/**\n * \n * @export\n * @interface CourseTaskDetailedDto\n */\nexport interface CourseTaskDetailedDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDetailedDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDetailedDto\n     */\n    'taskId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'type': CourseTaskDetailedDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'checker': CourseTaskDetailedDtoCheckerEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'studentStartDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'studentEndDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'studentRegistrationStartDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'crossCheckEndDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof CourseTaskDetailedDto\n     */\n    'taskOwner': PersonDto | null;\n    /**\n     * \n     * @type {object}\n     * @memberof CourseTaskDetailedDto\n     */\n    'taskSolutions': object | null;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDetailedDto\n     */\n    'maxScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDetailedDto\n     */\n    'scoreWeight': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDetailedDto\n     */\n    'pairsCount': number | null;\n    /**\n     * \n     * @type {CrossCheckStatusEnum}\n     * @memberof CourseTaskDetailedDto\n     */\n    'crossCheckStatus': CrossCheckStatusEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'submitText': string | null;\n    /**\n     * \n     * @type {Validations}\n     * @memberof CourseTaskDetailedDto\n     */\n    'validations': Validations | null;\n    /**\n     * \n     * @type {object}\n     * @memberof CourseTaskDetailedDto\n     */\n    'publicAttributes': object;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'githubRepoName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDetailedDto\n     */\n    'sourceGithubRepoUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDetailedDto\n     */\n    'resultsCount': number;\n}\n\nexport const CourseTaskDetailedDtoTypeEnum = {\n    Jstask: 'jstask',\n    Kotlintask: 'kotlintask',\n    Objctask: 'objctask',\n    Htmltask: 'htmltask',\n    Ipynb: 'ipynb',\n    Selfeducation: 'selfeducation',\n    Codewars: 'codewars',\n    Test: 'test',\n    Codejam: 'codejam',\n    Interview: 'interview',\n    StageInterview: 'stage-interview',\n    Cvhtml: 'cv:html',\n    Cvmarkdown: 'cv:markdown'\n} as const;\n\nexport type CourseTaskDetailedDtoTypeEnum = typeof CourseTaskDetailedDtoTypeEnum[keyof typeof CourseTaskDetailedDtoTypeEnum];\nexport const CourseTaskDetailedDtoCheckerEnum = {\n    AutoTest: 'auto-test',\n    Assigned: 'assigned',\n    Mentor: 'mentor',\n    TaskOwner: 'taskOwner',\n    CrossCheck: 'crossCheck'\n} as const;\n\nexport type CourseTaskDetailedDtoCheckerEnum = typeof CourseTaskDetailedDtoCheckerEnum[keyof typeof CourseTaskDetailedDtoCheckerEnum];\n\n/**\n * \n * @export\n * @interface CourseTaskDto\n */\nexport interface CourseTaskDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDto\n     */\n    'taskId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'type': CourseTaskDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'checker': CourseTaskDtoCheckerEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'studentStartDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'studentEndDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'studentRegistrationStartDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'crossCheckEndDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof CourseTaskDto\n     */\n    'taskOwner': PersonDto | null;\n    /**\n     * \n     * @type {object}\n     * @memberof CourseTaskDto\n     */\n    'taskSolutions': object | null;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDto\n     */\n    'maxScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDto\n     */\n    'scoreWeight': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseTaskDto\n     */\n    'pairsCount': number | null;\n    /**\n     * \n     * @type {CrossCheckStatusEnum}\n     * @memberof CourseTaskDto\n     */\n    'crossCheckStatus': CrossCheckStatusEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseTaskDto\n     */\n    'submitText': string | null;\n    /**\n     * \n     * @type {Validations}\n     * @memberof CourseTaskDto\n     */\n    'validations': Validations | null;\n}\n\nexport const CourseTaskDtoTypeEnum = {\n    Jstask: 'jstask',\n    Kotlintask: 'kotlintask',\n    Objctask: 'objctask',\n    Htmltask: 'htmltask',\n    Ipynb: 'ipynb',\n    Selfeducation: 'selfeducation',\n    Codewars: 'codewars',\n    Test: 'test',\n    Codejam: 'codejam',\n    Interview: 'interview',\n    StageInterview: 'stage-interview',\n    Cvhtml: 'cv:html',\n    Cvmarkdown: 'cv:markdown'\n} as const;\n\nexport type CourseTaskDtoTypeEnum = typeof CourseTaskDtoTypeEnum[keyof typeof CourseTaskDtoTypeEnum];\nexport const CourseTaskDtoCheckerEnum = {\n    AutoTest: 'auto-test',\n    Assigned: 'assigned',\n    Mentor: 'mentor',\n    TaskOwner: 'taskOwner',\n    CrossCheck: 'crossCheck'\n} as const;\n\nexport type CourseTaskDtoCheckerEnum = typeof CourseTaskDtoCheckerEnum[keyof typeof CourseTaskDtoCheckerEnum];\n\n/**\n * \n * @export\n * @interface CourseUserDto\n */\nexport interface CourseUserDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CourseUserDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CourseUserDto\n     */\n    'courseId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseUserDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CourseUserDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseUserDto\n     */\n    'isManager': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseUserDto\n     */\n    'isSupervisor': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseUserDto\n     */\n    'isJuryActivist': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseUserDto\n     */\n    'isDementor': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CourseUserDto\n     */\n    'isActivist': boolean;\n}\n/**\n * \n * @export\n * @interface CreateActivityDto\n */\nexport interface CreateActivityDto {\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateActivityDto\n     */\n    'isActive': boolean;\n}\n/**\n * \n * @export\n * @interface CreateActivityWebhookDto\n */\nexport interface CreateActivityWebhookDto {\n    /**\n     * \n     * @type {SenderDto}\n     * @memberof CreateActivityWebhookDto\n     */\n    'sender': SenderDto;\n}\n/**\n * \n * @export\n * @interface CreateAlertDto\n */\nexport interface CreateAlertDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateAlertDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateAlertDto\n     */\n    'text': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateAlertDto\n     */\n    'enabled'?: boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateAlertDto\n     */\n    'courseId'?: number;\n}\n/**\n * \n * @export\n * @interface CreateContributorDto\n */\nexport interface CreateContributorDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateContributorDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateContributorDto\n     */\n    'userId': number;\n}\n/**\n * \n * @export\n * @interface CreateCourseDto\n */\nexport interface CreateCourseDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'fullName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'alias': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'registrationEndDate'?: string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateCourseDto\n     */\n    'completed'?: boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateCourseDto\n     */\n    'planned'?: boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateCourseDto\n     */\n    'inviteOnly'?: boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'description'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'descriptionUrl'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseDto\n     */\n    'disciplineId'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseDto\n     */\n    'discordServerId'?: number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateCourseDto\n     */\n    'usePrivateRepositories'?: boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'certificateIssuer'?: string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateCourseDto\n     */\n    'personalMentoring'?: boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'personalMentoringStartDate'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'personalMentoringEndDate'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'logo'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseDto\n     */\n    'minStudentsPerMentor'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseDto\n     */\n    'certificateThreshold': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseDto\n     */\n    'wearecommunityUrl': string;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof CreateCourseDto\n     */\n    'certificateDisciplines': Array<string> | null;\n}\n/**\n * \n * @export\n * @interface CreateCourseEventDto\n */\nexport interface CreateCourseEventDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseEventDto\n     */\n    'eventId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseEventDto\n     */\n    'special'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseEventDto\n     */\n    'dateTime'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseEventDto\n     */\n    'endTime'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseEventDto\n     */\n    'duration'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseEventDto\n     */\n    'place'?: string;\n    /**\n     * \n     * @type {Organizer}\n     * @memberof CreateCourseEventDto\n     */\n    'organizer'?: Organizer;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseEventDto\n     */\n    'organizerId'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseEventDto\n     */\n    'broadcastUrl'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseEventDto\n     */\n    'coordinator'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseEventDto\n     */\n    'comment'?: string;\n}\n/**\n * \n * @export\n * @interface CreateCourseTaskDto\n */\nexport interface CreateCourseTaskDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseTaskDto\n     */\n    'taskId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseTaskDto\n     */\n    'maxScore'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseTaskDto\n     */\n    'scoreWeight'?: number;\n    /**\n     * \n     * @type {CheckerEnum}\n     * @memberof CreateCourseTaskDto\n     */\n    'checker': CheckerEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseTaskDto\n     */\n    'studentStartDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseTaskDto\n     */\n    'studentEndDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseTaskDto\n     */\n    'studentRegistrationStartDate'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseTaskDto\n     */\n    'crossCheckEndDate'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseTaskDto\n     */\n    'taskOwnerId'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateCourseTaskDto\n     */\n    'pairsCount'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseTaskDto\n     */\n    'type': CreateCourseTaskDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateCourseTaskDto\n     */\n    'submitText': string;\n    /**\n     * \n     * @type {object}\n     * @memberof CreateCourseTaskDto\n     */\n    'validations': object;\n}\n\nexport const CreateCourseTaskDtoTypeEnum = {\n    Jstask: 'jstask',\n    Kotlintask: 'kotlintask',\n    Objctask: 'objctask',\n    Htmltask: 'htmltask',\n    Ipynb: 'ipynb',\n    Selfeducation: 'selfeducation',\n    Codewars: 'codewars',\n    Test: 'test',\n    Codejam: 'codejam',\n    Interview: 'interview',\n    StageInterview: 'stage-interview',\n    Cvhtml: 'cv:html',\n    Cvmarkdown: 'cv:markdown'\n} as const;\n\nexport type CreateCourseTaskDtoTypeEnum = typeof CreateCourseTaskDtoTypeEnum[keyof typeof CreateCourseTaskDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface CreateDisciplineDto\n */\nexport interface CreateDisciplineDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateDisciplineDto\n     */\n    'name': string;\n}\n/**\n * \n * @export\n * @interface CreateDiscordServerDto\n */\nexport interface CreateDiscordServerDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateDiscordServerDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateDiscordServerDto\n     */\n    'gratitudeUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateDiscordServerDto\n     */\n    'mentorsChatUrl': string;\n}\n/**\n * \n * @export\n * @interface CreateEventDto\n */\nexport interface CreateEventDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateEventDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateEventDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateEventDto\n     */\n    'disciplineId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateEventDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateEventDto\n     */\n    'description': string;\n}\n/**\n * \n * @export\n * @interface CreateGratitudeDto\n */\nexport interface CreateGratitudeDto {\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof CreateGratitudeDto\n     */\n    'userIds': Array<number>;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateGratitudeDto\n     */\n    'courseId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateGratitudeDto\n     */\n    'comment': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateGratitudeDto\n     */\n    'badgeId': string;\n}\n/**\n * \n * @export\n * @interface CreatePromptDto\n */\nexport interface CreatePromptDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreatePromptDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreatePromptDto\n     */\n    'text': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreatePromptDto\n     */\n    'temperature': number;\n}\n/**\n * \n * @export\n * @interface CreateStudentFeedbackDto\n */\nexport interface CreateStudentFeedbackDto {\n    /**\n     * \n     * @type {StudentFeedbackContentDto}\n     * @memberof CreateStudentFeedbackDto\n     */\n    'content': StudentFeedbackContentDto;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateStudentFeedbackDto\n     */\n    'recommendation': CreateStudentFeedbackDtoRecommendationEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateStudentFeedbackDto\n     */\n    'englishLevel': CreateStudentFeedbackDtoEnglishLevelEnum;\n}\n\nexport const CreateStudentFeedbackDtoRecommendationEnum = {\n    Hire: 'hire',\n    NotHire: 'not-hire'\n} as const;\n\nexport type CreateStudentFeedbackDtoRecommendationEnum = typeof CreateStudentFeedbackDtoRecommendationEnum[keyof typeof CreateStudentFeedbackDtoRecommendationEnum];\nexport const CreateStudentFeedbackDtoEnglishLevelEnum = {\n    Unknown: 'unknown',\n    A0: 'a0',\n    A1: 'a1',\n    A2: 'a2',\n    B1: 'b1',\n    B2: 'b2',\n    C1: 'c1',\n    C2: 'c2'\n} as const;\n\nexport type CreateStudentFeedbackDtoEnglishLevelEnum = typeof CreateStudentFeedbackDtoEnglishLevelEnum[keyof typeof CreateStudentFeedbackDtoEnglishLevelEnum];\n\n/**\n * \n * @export\n * @interface CreateTaskDto\n */\nexport interface CreateTaskDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTaskDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {object}\n     * @memberof CreateTaskDto\n     */\n    'attributes': object;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTaskDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTaskDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTaskDto\n     */\n    'githubRepoName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTaskDto\n     */\n    'sourceGithubRepoUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateTaskDto\n     */\n    'disciplineId': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateTaskDto\n     */\n    'githubPrRequired': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTaskDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof CreateTaskDto\n     */\n    'skills': Array<string>;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof CreateTaskDto\n     */\n    'tags': Array<string>;\n}\n/**\n * \n * @export\n * @interface CreateTaskVerificationDto\n */\nexport interface CreateTaskVerificationDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CreateTaskVerificationDto\n     */\n    'id'?: number;\n}\n/**\n * \n * @export\n * @interface CreateTeamDistributionDto\n */\nexport interface CreateTeamDistributionDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDistributionDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDistributionDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDistributionDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDistributionDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDistributionDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateTeamDistributionDto\n     */\n    'minTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateTeamDistributionDto\n     */\n    'maxTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateTeamDistributionDto\n     */\n    'strictTeamSize': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CreateTeamDistributionDto\n     */\n    'strictTeamSizeMode': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof CreateTeamDistributionDto\n     */\n    'minTotalScore': number;\n}\n/**\n * \n * @export\n * @interface CreateTeamDto\n */\nexport interface CreateTeamDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CreateTeamDto\n     */\n    'chatLink': string;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof CreateTeamDto\n     */\n    'studentIds'?: Array<number>;\n}\n/**\n * \n * @export\n * @interface CreateUserGroupDto\n */\nexport interface CreateUserGroupDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CreateUserGroupDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof CreateUserGroupDto\n     */\n    'users': Array<number>;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof CreateUserGroupDto\n     */\n    'roles': Array<CreateUserGroupDtoRolesEnum>;\n}\n\nexport const CreateUserGroupDtoRolesEnum = {\n    TaskOwner: 'taskOwner',\n    Manager: 'manager',\n    Supervisor: 'supervisor',\n    Student: 'student',\n    Mentor: 'mentor',\n    Dementor: 'dementor',\n    Activist: 'activist'\n} as const;\n\nexport type CreateUserGroupDtoRolesEnum = typeof CreateUserGroupDtoRolesEnum[keyof typeof CreateUserGroupDtoRolesEnum];\n\n/**\n * \n * @export\n * @interface CriteriaDto\n */\nexport interface CriteriaDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CriteriaDto\n     */\n    'max'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof CriteriaDto\n     */\n    'type': CriteriaDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof CriteriaDto\n     */\n    'text': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CriteriaDto\n     */\n    'key': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CriteriaDto\n     */\n    'index': number;\n}\n\nexport const CriteriaDtoTypeEnum = {\n    Title: 'title',\n    Subtask: 'subtask',\n    Penalty: 'penalty'\n} as const;\n\nexport type CriteriaDtoTypeEnum = typeof CriteriaDtoTypeEnum[keyof typeof CriteriaDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface CrossCheckAuthorDto\n */\nexport interface CrossCheckAuthorDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckAuthorDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckAuthorDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckAuthorDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {Discord}\n     * @memberof CrossCheckAuthorDto\n     */\n    'discord': Discord | null;\n}\n/**\n * \n * @export\n * @interface CrossCheckCriteriaDataDto\n */\nexport interface CrossCheckCriteriaDataDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckCriteriaDataDto\n     */\n    'key': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckCriteriaDataDto\n     */\n    'max'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckCriteriaDataDto\n     */\n    'text': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckCriteriaDataDto\n     */\n    'type': CrossCheckCriteriaDataDtoTypeEnum;\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckCriteriaDataDto\n     */\n    'point'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckCriteriaDataDto\n     */\n    'textComment'?: string;\n}\n\nexport const CrossCheckCriteriaDataDtoTypeEnum = {\n    Title: 'title',\n    Subtask: 'subtask',\n    Penalty: 'penalty'\n} as const;\n\nexport type CrossCheckCriteriaDataDtoTypeEnum = typeof CrossCheckCriteriaDataDtoTypeEnum[keyof typeof CrossCheckCriteriaDataDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface CrossCheckFeedbackDto\n */\nexport interface CrossCheckFeedbackDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckFeedbackDto\n     */\n    'url'?: string;\n    /**\n     * \n     * @type {Array<CrossCheckSolutionReviewDto>}\n     * @memberof CrossCheckFeedbackDto\n     */\n    'reviews'?: Array<CrossCheckSolutionReviewDto>;\n}\n/**\n * \n * @export\n * @interface CrossCheckMessageAuthorDto\n */\nexport interface CrossCheckMessageAuthorDto {\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckMessageAuthorDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckMessageAuthorDto\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface CrossCheckMessageDto\n */\nexport interface CrossCheckMessageDto {\n    /**\n     * \n     * @type {CrossCheckMessageAuthorDto}\n     * @memberof CrossCheckMessageDto\n     */\n    'author': CrossCheckMessageAuthorDto | null;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckMessageDto\n     */\n    'content': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckMessageDto\n     */\n    'timestamp': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CrossCheckMessageDto\n     */\n    'isReviewerRead': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof CrossCheckMessageDto\n     */\n    'isStudentRead': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckMessageDto\n     */\n    'role': CrossCheckMessageDtoRoleEnum;\n}\n\nexport const CrossCheckMessageDtoRoleEnum = {\n    Reviewer: 'reviewer',\n    Student: 'student'\n} as const;\n\nexport type CrossCheckMessageDtoRoleEnum = typeof CrossCheckMessageDtoRoleEnum[keyof typeof CrossCheckMessageDtoRoleEnum];\n\n/**\n * \n * @export\n * @interface CrossCheckPairDto\n */\nexport interface CrossCheckPairDto {\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof CrossCheckPairDto\n     */\n    'student': PersonDto;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof CrossCheckPairDto\n     */\n    'checker': PersonDto;\n    /**\n     * \n     * @type {IdNameDto}\n     * @memberof CrossCheckPairDto\n     */\n    'task': IdNameDto;\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckPairDto\n     */\n    'score': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckPairDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckPairDto\n     */\n    'comment': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckPairDto\n     */\n    'url': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckPairDto\n     */\n    'reviewedDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckPairDto\n     */\n    'privateRepository': string;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckPairDto\n     */\n    'submittedDate': string;\n    /**\n     * \n     * @type {Array<HistoricalScoreDto>}\n     * @memberof CrossCheckPairDto\n     */\n    'historicalScores': Array<HistoricalScoreDto>;\n    /**\n     * \n     * @type {Array<CrossCheckMessageDto>}\n     * @memberof CrossCheckPairDto\n     */\n    'messages': Array<CrossCheckMessageDto>;\n}\n/**\n * \n * @export\n * @interface CrossCheckPairResponseDto\n */\nexport interface CrossCheckPairResponseDto {\n    /**\n     * \n     * @type {Array<CrossCheckPairDto>}\n     * @memberof CrossCheckPairResponseDto\n     */\n    'items': Array<CrossCheckPairDto>;\n    /**\n     * \n     * @type {PaginationDto}\n     * @memberof CrossCheckPairResponseDto\n     */\n    'pagination': PaginationDto;\n}\n/**\n * \n * @export\n * @interface CrossCheckSolutionReviewDto\n */\nexport interface CrossCheckSolutionReviewDto {\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckSolutionReviewDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckSolutionReviewDto\n     */\n    'dateTime': number;\n    /**\n     * \n     * @type {string}\n     * @memberof CrossCheckSolutionReviewDto\n     */\n    'comment': string;\n    /**\n     * \n     * @type {Array<CrossCheckCriteriaDataDto>}\n     * @memberof CrossCheckSolutionReviewDto\n     */\n    'criteria'?: Array<CrossCheckCriteriaDataDto>;\n    /**\n     * \n     * @type {CrossCheckAuthorDto}\n     * @memberof CrossCheckSolutionReviewDto\n     */\n    'author': CrossCheckAuthorDto | null;\n    /**\n     * \n     * @type {number}\n     * @memberof CrossCheckSolutionReviewDto\n     */\n    'score': number;\n    /**\n     * \n     * @type {Array<CrossCheckMessageDto>}\n     * @memberof CrossCheckSolutionReviewDto\n     */\n    'messages': Array<CrossCheckMessageDto>;\n}\n/**\n * \n * @export\n * @enum {string}\n */\n\nexport const CrossCheckStatusEnum = {\n    Initial: 'initial',\n    Distributed: 'distributed',\n    Completed: 'completed'\n} as const;\n\nexport type CrossCheckStatusEnum = typeof CrossCheckStatusEnum[keyof typeof CrossCheckStatusEnum];\n\n\n/**\n * \n * @export\n * @interface DevtoolsUserDto\n */\nexport interface DevtoolsUserDto {\n    /**\n     * \n     * @type {number}\n     * @memberof DevtoolsUserDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof DevtoolsUserDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof DevtoolsUserDto\n     */\n    'mentor': Array<number>;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof DevtoolsUserDto\n     */\n    'student': Array<number>;\n}\n/**\n * \n * @export\n * @interface DisciplineDto\n */\nexport interface DisciplineDto {\n    /**\n     * \n     * @type {string}\n     * @memberof DisciplineDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof DisciplineDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof DisciplineDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof DisciplineDto\n     */\n    'updatedDate': string;\n}\n/**\n * \n * @export\n * @interface DisciplineIdsDto\n */\nexport interface DisciplineIdsDto {\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof DisciplineIdsDto\n     */\n    'ids': Array<number>;\n}\n/**\n * \n * @export\n * @interface Discord\n */\nexport interface Discord {\n    /**\n     * \n     * @type {string}\n     * @memberof Discord\n     */\n    'id': string;\n    /**\n     * \n     * @type {string}\n     * @memberof Discord\n     */\n    'username': string;\n    /**\n     * \n     * @type {string}\n     * @memberof Discord\n     */\n    'discriminator': string;\n}\n/**\n * \n * @export\n * @interface DiscordServerDto\n */\nexport interface DiscordServerDto {\n    /**\n     * \n     * @type {number}\n     * @memberof DiscordServerDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof DiscordServerDto\n     */\n    'createdDate': number;\n    /**\n     * \n     * @type {number}\n     * @memberof DiscordServerDto\n     */\n    'updatedDate': number;\n    /**\n     * \n     * @type {string}\n     * @memberof DiscordServerDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof DiscordServerDto\n     */\n    'gratitudeUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof DiscordServerDto\n     */\n    'mentorsChatUrl': string | null;\n}\n/**\n * \n * @export\n * @interface Education\n */\nexport interface Education {\n    /**\n     * \n     * @type {string}\n     * @memberof Education\n     */\n    'university': string;\n    /**\n     * \n     * @type {string}\n     * @memberof Education\n     */\n    'faculty': string;\n    /**\n     * \n     * @type {number}\n     * @memberof Education\n     */\n    'graduationYear': number;\n}\n/**\n * \n * @export\n * @interface EndorsementDataDto\n */\nexport interface EndorsementDataDto {\n    /**\n     * \n     * @type {EndorsementUserDto}\n     * @memberof EndorsementDataDto\n     */\n    'user': EndorsementUserDto;\n    /**\n     * User\\'s courses\n     * @type {Array<CourseDto>}\n     * @memberof EndorsementDataDto\n     */\n    'courses': Array<CourseDto>;\n    /**\n     * Number of students\n     * @type {number}\n     * @memberof EndorsementDataDto\n     */\n    'studentsCount': number;\n    /**\n     * Number of interviews\n     * @type {number}\n     * @memberof EndorsementDataDto\n     */\n    'interviewsCount': number;\n}\n/**\n * \n * @export\n * @interface EndorsementDto\n */\nexport interface EndorsementDto {\n    /**\n     * \n     * @type {string}\n     * @memberof EndorsementDto\n     */\n    'summary': string;\n    /**\n     * \n     * @type {object}\n     * @memberof EndorsementDto\n     */\n    'data': object | null;\n}\n/**\n * \n * @export\n * @interface EndorsementUserDto\n */\nexport interface EndorsementUserDto {\n    /**\n     * \n     * @type {number}\n     * @memberof EndorsementUserDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof EndorsementUserDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof EndorsementUserDto\n     */\n    'firstName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof EndorsementUserDto\n     */\n    'lastName': string;\n}\n/**\n * \n * @export\n * @interface EventDto\n */\nexport interface EventDto {\n    /**\n     * \n     * @type {number}\n     * @memberof EventDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof EventDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof EventDto\n     */\n    'descriptionUrl': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof EventDto\n     */\n    'description': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof EventDto\n     */\n    'type': EventDtoTypeEnum;\n    /**\n     * \n     * @type {IdNameDto}\n     * @memberof EventDto\n     */\n    'discipline': IdNameDto | null;\n}\n\nexport const EventDtoTypeEnum = {\n    LectureOnline: 'lecture_online',\n    LectureOffline: 'lecture_offline',\n    LectureMixed: 'lecture_mixed',\n    LectureSelfStudy: 'lecture_self_study',\n    Warmup: 'warmup',\n    Info: 'info',\n    Workshop: 'workshop',\n    Meetup: 'meetup',\n    CrossCheckDeadline: 'cross_check_deadline',\n    Webinar: 'webinar',\n    Special: 'special'\n} as const;\n\nexport type EventDtoTypeEnum = typeof EventDtoTypeEnum[keyof typeof EventDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface ExpelCriteriaDto\n */\nexport interface ExpelCriteriaDto {\n    /**\n     * Array of course task IDs\n     * @type {Array<number>}\n     * @memberof ExpelCriteriaDto\n     */\n    'courseTaskIds'?: Array<number>;\n    /**\n     * Minimum score threshold\n     * @type {number}\n     * @memberof ExpelCriteriaDto\n     */\n    'minScore'?: number;\n}\n/**\n * \n * @export\n * @interface ExpelOptionsDto\n */\nexport interface ExpelOptionsDto {\n    /**\n     * Whether to keep the student with their mentor\n     * @type {boolean}\n     * @memberof ExpelOptionsDto\n     */\n    'keepWithMentor'?: boolean;\n    /**\n     * Save assigning to the mentor (default: false)\n     * @type {boolean}\n     * @memberof ExpelOptionsDto\n     */\n    'saveAssigningToMentor'?: boolean;\n}\n/**\n * \n * @export\n * @interface ExpelStatusDto\n */\nexport interface ExpelStatusDto {\n    /**\n     * Criteria for expelling students\n     * @type {ExpelCriteriaDto}\n     * @memberof ExpelStatusDto\n     */\n    'criteria': ExpelCriteriaDto;\n    /**\n     * Additional options for expelling\n     * @type {ExpelOptionsDto}\n     * @memberof ExpelStatusDto\n     */\n    'options': ExpelOptionsDto;\n    /**\n     * Reason for expelling the student\n     * @type {string}\n     * @memberof ExpelStatusDto\n     */\n    'expellingReason': string;\n}\n/**\n * \n * @export\n * @interface ExpelledStatsDto\n */\nexport interface ExpelledStatsDto {\n    /**\n     * \n     * @type {string}\n     * @memberof ExpelledStatsDto\n     */\n    'id': string;\n    /**\n     * \n     * @type {CourseDto}\n     * @memberof ExpelledStatsDto\n     */\n    'course': CourseDto;\n    /**\n     * \n     * @type {UserDto}\n     * @memberof ExpelledStatsDto\n     */\n    'user': UserDto;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof ExpelledStatsDto\n     */\n    'reasonForLeaving'?: Array<string>;\n    /**\n     * \n     * @type {string}\n     * @memberof ExpelledStatsDto\n     */\n    'otherComment': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ExpelledStatsDto\n     */\n    'submittedAt': string;\n}\n/**\n * \n * @export\n * @interface FeedbackCourseDto\n */\nexport interface FeedbackCourseDto {\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackCourseDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof FeedbackCourseDto\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface FeedbackDto\n */\nexport interface FeedbackDto {\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackDto\n     */\n    'date': string;\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackDto\n     */\n    'recommendation': string;\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackDto\n     */\n    'englishLevel': string;\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackDto\n     */\n    'recommendationComment': string;\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackDto\n     */\n    'suggestions': string;\n    /**\n     * \n     * @type {Array<FeedbackSoftSkill>}\n     * @memberof FeedbackDto\n     */\n    'softSkills': Array<FeedbackSoftSkill>;\n    /**\n     * \n     * @type {ResumeCourseMentor}\n     * @memberof FeedbackDto\n     */\n    'mentor': ResumeCourseMentor;\n    /**\n     * \n     * @type {FeedbackCourseDto}\n     * @memberof FeedbackDto\n     */\n    'course': FeedbackCourseDto;\n}\n/**\n * \n * @export\n * @interface FeedbackSoftSkill\n */\nexport interface FeedbackSoftSkill {\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackSoftSkill\n     */\n    'value': FeedbackSoftSkillValueEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof FeedbackSoftSkill\n     */\n    'id': FeedbackSoftSkillIdEnum;\n}\n\nexport const FeedbackSoftSkillValueEnum = {\n    None: 'None',\n    Poor: 'Poor',\n    Fair: 'Fair',\n    Good: 'Good',\n    Great: 'Great',\n    Excellent: 'Excellent'\n} as const;\n\nexport type FeedbackSoftSkillValueEnum = typeof FeedbackSoftSkillValueEnum[keyof typeof FeedbackSoftSkillValueEnum];\nexport const FeedbackSoftSkillIdEnum = {\n    Responsible: 'skill.soft.responsible',\n    TeamPlayer: 'skill.soft.team-player',\n    Communicable: 'skill.soft.communicable'\n} as const;\n\nexport type FeedbackSoftSkillIdEnum = typeof FeedbackSoftSkillIdEnum[keyof typeof FeedbackSoftSkillIdEnum];\n\n/**\n * \n * @export\n * @interface FilterMentorRegistryResponse\n */\nexport interface FilterMentorRegistryResponse {\n    /**\n     * \n     * @type {Array<MentorRegistryDto>}\n     * @memberof FilterMentorRegistryResponse\n     */\n    'mentors': Array<MentorRegistryDto>;\n    /**\n     * \n     * @type {number}\n     * @memberof FilterMentorRegistryResponse\n     */\n    'total': number;\n}\n/**\n * \n * @export\n * @interface FormDataDto\n */\nexport interface FormDataDto {\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'avatarLink': string | null;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof FormDataDto\n     */\n    'visibleCourses': Array<number>;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'desiredPosition': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'email': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'englishLevel': FormDataDtoEnglishLevelEnum;\n    /**\n     * \n     * @type {boolean}\n     * @memberof FormDataDto\n     */\n    'fullTime': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'githubUsername': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'linkedin': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'locations': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'militaryService': FormDataDtoMilitaryServiceEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'name': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'notes': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'phone': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'selfIntroLink': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'skype': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'startFrom': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'telegram': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof FormDataDto\n     */\n    'website': string | null;\n}\n\nexport const FormDataDtoEnglishLevelEnum = {\n    Unknown: 'unknown',\n    A0: 'a0',\n    A1: 'a1',\n    A2: 'a2',\n    B1: 'b1',\n    B2: 'b2',\n    C1: 'c1',\n    C2: 'c2'\n} as const;\n\nexport type FormDataDtoEnglishLevelEnum = typeof FormDataDtoEnglishLevelEnum[keyof typeof FormDataDtoEnglishLevelEnum];\nexport const FormDataDtoMilitaryServiceEnum = {\n    Served: 'served',\n    Liable: 'liable',\n    NotLiable: 'notLiable'\n} as const;\n\nexport type FormDataDtoMilitaryServiceEnum = typeof FormDataDtoMilitaryServiceEnum[keyof typeof FormDataDtoMilitaryServiceEnum];\n\n/**\n * \n * @export\n * @interface GiveConsentDto\n */\nexport interface GiveConsentDto {\n    /**\n     * \n     * @type {boolean}\n     * @memberof GiveConsentDto\n     */\n    'consent': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof GiveConsentDto\n     */\n    'expires': number;\n}\n/**\n * \n * @export\n * @interface GratitudeDto\n */\nexport interface GratitudeDto {\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof GratitudeDto\n     */\n    'user': PersonDto;\n    /**\n     * \n     * @type {number}\n     * @memberof GratitudeDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {BadgeEnum}\n     * @memberof GratitudeDto\n     */\n    'badgeId': BadgeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof GratitudeDto\n     */\n    'comment': string;\n    /**\n     * \n     * @type {number}\n     * @memberof GratitudeDto\n     */\n    'courseId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof GratitudeDto\n     */\n    'date': string;\n}\n/**\n * \n * @export\n * @interface HeroRadarDto\n */\nexport interface HeroRadarDto {\n    /**\n     * \n     * @type {string}\n     * @memberof HeroRadarDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof HeroRadarDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof HeroRadarDto\n     */\n    'rank': number;\n    /**\n     * \n     * @type {number}\n     * @memberof HeroRadarDto\n     */\n    'total': number;\n    /**\n     * \n     * @type {Array<HeroesRadarBadgeDto>}\n     * @memberof HeroRadarDto\n     */\n    'badges': Array<HeroesRadarBadgeDto>;\n}\n/**\n * \n * @export\n * @interface HeroesRadarBadgeDto\n */\nexport interface HeroesRadarBadgeDto {\n    /**\n     * \n     * @type {string}\n     * @memberof HeroesRadarBadgeDto\n     */\n    'id': string;\n    /**\n     * \n     * @type {string}\n     * @memberof HeroesRadarBadgeDto\n     */\n    'badgeId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof HeroesRadarBadgeDto\n     */\n    'comment': string;\n    /**\n     * \n     * @type {string}\n     * @memberof HeroesRadarBadgeDto\n     */\n    'date': string;\n}\n/**\n * \n * @export\n * @interface HeroesRadarDto\n */\nexport interface HeroesRadarDto {\n    /**\n     * \n     * @type {Array<HeroRadarDto>}\n     * @memberof HeroesRadarDto\n     */\n    'content': Array<HeroRadarDto>;\n    /**\n     * \n     * @type {PaginationMetaDto}\n     * @memberof HeroesRadarDto\n     */\n    'pagination': PaginationMetaDto;\n}\n/**\n * \n * @export\n * @interface HistoricalScoreDto\n */\nexport interface HistoricalScoreDto {\n    /**\n     * \n     * @type {string}\n     * @memberof HistoricalScoreDto\n     */\n    'comment': string;\n    /**\n     * \n     * @type {string}\n     * @memberof HistoricalScoreDto\n     */\n    'dateTime': string;\n    /**\n     * \n     * @type {Array<CrossCheckCriteriaDataDto>}\n     * @memberof HistoricalScoreDto\n     */\n    'criteria'?: Array<CrossCheckCriteriaDataDto>;\n}\n/**\n * \n * @export\n * @interface IdNameDto\n */\nexport interface IdNameDto {\n    /**\n     * \n     * @type {string}\n     * @memberof IdNameDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof IdNameDto\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface InterviewCommentDto\n */\nexport interface InterviewCommentDto {\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewCommentDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewCommentDto\n     */\n    'commentToStudent': string | null;\n}\n/**\n * \n * @export\n * @interface InterviewDistributeDto\n */\nexport interface InterviewDistributeDto {\n    /**\n     * \n     * @type {boolean}\n     * @memberof InterviewDistributeDto\n     */\n    'clean': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof InterviewDistributeDto\n     */\n    'registrationEnabled': boolean;\n}\n/**\n * \n * @export\n * @interface InterviewDistributeResponseDto\n */\nexport interface InterviewDistributeResponseDto {\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewDistributeResponseDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewDistributeResponseDto\n     */\n    'courseTaskId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewDistributeResponseDto\n     */\n    'mentorId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewDistributeResponseDto\n     */\n    'studentId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDistributeResponseDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDistributeResponseDto\n     */\n    'updatedDate': string;\n}\n/**\n * \n * @export\n * @interface InterviewDto\n */\nexport interface InterviewDto {\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDto\n     */\n    'description': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof InterviewDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {Attributes}\n     * @memberof InterviewDto\n     */\n    'attributes': Attributes;\n    /**\n     * Date when student can register for the interview\n     * @type {string}\n     * @memberof InterviewDto\n     */\n    'studentRegistrationStartDate': string;\n}\n/**\n * \n * @export\n * @interface InterviewFeedbackDto\n */\nexport interface InterviewFeedbackDto {\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewFeedbackDto\n     */\n    'version'?: number;\n    /**\n     * \n     * @type {object}\n     * @memberof InterviewFeedbackDto\n     */\n    'json'?: object;\n    /**\n     * \n     * @type {boolean}\n     * @memberof InterviewFeedbackDto\n     */\n    'isCompleted': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewFeedbackDto\n     */\n    'maxScore': number;\n}\n/**\n * \n * @export\n * @interface InterviewPairDto\n */\nexport interface InterviewPairDto {\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewPairDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof InterviewPairDto\n     */\n    'result': number | null;\n    /**\n     * \n     * @type {InterviewStatus}\n     * @memberof InterviewPairDto\n     */\n    'status': InterviewStatus;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof InterviewPairDto\n     */\n    'interviewer': PersonDto;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof InterviewPairDto\n     */\n    'student': PersonDto;\n}\n/**\n * \n * @export\n * @enum {string}\n */\n\nexport const InterviewStatus = {\n    NUMBER_1: 1,\n    NUMBER_0: 0\n} as const;\n\nexport type InterviewStatus = typeof InterviewStatus[keyof typeof InterviewStatus];\n\n\n/**\n * \n * @export\n * @interface InviteMentorsDto\n */\nexport interface InviteMentorsDto {\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof InviteMentorsDto\n     */\n    'disciplines': Array<string>;\n    /**\n     * \n     * @type {boolean}\n     * @memberof InviteMentorsDto\n     */\n    'isMentor': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof InviteMentorsDto\n     */\n    'text': string;\n}\n/**\n * \n * @export\n * @interface JoinTeamDto\n */\nexport interface JoinTeamDto {\n    /**\n     * \n     * @type {string}\n     * @memberof JoinTeamDto\n     */\n    'password': string;\n}\n/**\n * \n * @export\n * @interface LeaveCourseRequestDto\n */\nexport interface LeaveCourseRequestDto {\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof LeaveCourseRequestDto\n     */\n    'reasonForLeaving'?: Array<string>;\n    /**\n     * \n     * @type {string}\n     * @memberof LeaveCourseRequestDto\n     */\n    'otherComment'?: string;\n}\n/**\n * \n * @export\n * @interface MentorCourseStatsDto\n */\nexport interface MentorCourseStatsDto {\n    /**\n     * Name of the course\n     * @type {string}\n     * @memberof MentorCourseStatsDto\n     */\n    'courseName': string;\n    /**\n     * Number of certified students mentored in this course\n     * @type {number}\n     * @memberof MentorCourseStatsDto\n     */\n    'studentsCount': number;\n}\n/**\n * \n * @export\n * @interface MentorDashboardDto\n */\nexport interface MentorDashboardDto {\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDashboardDto\n     */\n    'studentGithubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDashboardDto\n     */\n    'studentName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDashboardDto\n     */\n    'taskName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDashboardDto\n     */\n    'taskDescriptionUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorDashboardDto\n     */\n    'courseTaskId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorDashboardDto\n     */\n    'maxScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorDashboardDto\n     */\n    'resultScore': number | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDashboardDto\n     */\n    'solutionUrl': string;\n    /**\n     * \n     * @type {SolutionItemStatusEnum}\n     * @memberof MentorDashboardDto\n     */\n    'status': SolutionItemStatusEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDashboardDto\n     */\n    'endDate': string;\n}\n/**\n * \n * @export\n * @interface MentorDetailsDto\n */\nexport interface MentorDetailsDto {\n    /**\n     * \n     * @type {number}\n     * @memberof MentorDetailsDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDetailsDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDetailsDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof MentorDetailsDto\n     */\n    'isActive': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDetailsDto\n     */\n    'cityName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDetailsDto\n     */\n    'countryName': string;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorDetailsDto\n     */\n    'maxStudentsLimit': number;\n    /**\n     * \n     * @type {Array<StudentId>}\n     * @memberof MentorDetailsDto\n     */\n    'students': Array<StudentId>;\n    /**\n     * \n     * @type {object}\n     * @memberof MentorDetailsDto\n     */\n    'interviews'?: object;\n    /**\n     * \n     * @type {object}\n     * @memberof MentorDetailsDto\n     */\n    'screenings'?: object;\n    /**\n     * \n     * @type {object}\n     * @memberof MentorDetailsDto\n     */\n    'taskResultsStats'?: object;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDetailsDto\n     */\n    'studentsPreference': MentorDetailsDtoStudentsPreferenceEnum;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorDetailsDto\n     */\n    'studentsCount'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDetailsDto\n     */\n    'contactsEpamEmail': string;\n}\n\nexport const MentorDetailsDtoStudentsPreferenceEnum = {\n    Any: 'any',\n    Country: 'country',\n    City: 'city'\n} as const;\n\nexport type MentorDetailsDtoStudentsPreferenceEnum = typeof MentorDetailsDtoStudentsPreferenceEnum[keyof typeof MentorDetailsDtoStudentsPreferenceEnum];\n\n/**\n * \n * @export\n * @interface MentorDto\n */\nexport interface MentorDto {\n    /**\n     * \n     * @type {number}\n     * @memberof MentorDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorDto\n     */\n    'name': string;\n}\n/**\n * \n * @export\n * @interface MentorOptionsDto\n */\nexport interface MentorOptionsDto {\n    /**\n     * \n     * @type {number}\n     * @memberof MentorOptionsDto\n     */\n    'maxStudentsLimit': number;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorOptionsDto\n     */\n    'preferedStudentsLocation': MentorOptionsDtoPreferedStudentsLocationEnum;\n    /**\n     * \n     * @type {Array<StudentsDto>}\n     * @memberof MentorOptionsDto\n     */\n    'students': Array<StudentsDto>;\n}\n\nexport const MentorOptionsDtoPreferedStudentsLocationEnum = {\n    Any: 'any',\n    Country: 'country',\n    City: 'city'\n} as const;\n\nexport type MentorOptionsDtoPreferedStudentsLocationEnum = typeof MentorOptionsDtoPreferedStudentsLocationEnum[keyof typeof MentorOptionsDtoPreferedStudentsLocationEnum];\n\n/**\n * \n * @export\n * @interface MentorRegistryDto\n */\nexport interface MentorRegistryDto {\n    /**\n     * \n     * @type {number}\n     * @memberof MentorRegistryDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'cityName': string | null;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof MentorRegistryDto\n     */\n    'preferedCourses': Array<number>;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof MentorRegistryDto\n     */\n    'preselectedCourses': Array<number>;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorRegistryDto\n     */\n    'maxStudentsLimit': number;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'preferedStudentsLocation': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof MentorRegistryDto\n     */\n    'technicalMentoring': Array<string>;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof MentorRegistryDto\n     */\n    'courses': Array<number>;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'sendDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'receivedDate': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof MentorRegistryDto\n     */\n    'hasCertificate': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof MentorRegistryDto\n     */\n    'englishMentoring': boolean;\n    /**\n     * \n     * @type {object}\n     * @memberof MentorRegistryDto\n     */\n    'primaryEmail': object;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof MentorRegistryDto\n     */\n    'languagesMentoring': Array<string>;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'contactsEpamEmail': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorRegistryDto\n     */\n    'comment': string | null;\n}\n/**\n * \n * @export\n * @interface MentorReviewAssignDto\n */\nexport interface MentorReviewAssignDto {\n    /**\n     * \n     * @type {number}\n     * @memberof MentorReviewAssignDto\n     */\n    'courseTaskId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorReviewAssignDto\n     */\n    'mentorId'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorReviewAssignDto\n     */\n    'studentId': number;\n}\n/**\n * \n * @export\n * @interface MentorReviewDto\n */\nexport interface MentorReviewDto {\n    /**\n     * Task solution id\n     * @type {number}\n     * @memberof MentorReviewDto\n     */\n    'id': number;\n    /**\n     * Course task name\n     * @type {string}\n     * @memberof MentorReviewDto\n     */\n    'taskName': string;\n    /**\n     * Course task id\n     * @type {number}\n     * @memberof MentorReviewDto\n     */\n    'taskId': number;\n    /**\n     * Task solution url\n     * @type {string}\n     * @memberof MentorReviewDto\n     */\n    'solutionUrl': string;\n    /**\n     * Task solution submission date\n     * @type {string}\n     * @memberof MentorReviewDto\n     */\n    'submittedAt': string;\n    /**\n     * Checker github id\n     * @type {string}\n     * @memberof MentorReviewDto\n     */\n    'checker': string;\n    /**\n     * Task solution score\n     * @type {number}\n     * @memberof MentorReviewDto\n     */\n    'score': number;\n    /**\n     * Task max score\n     * @type {number}\n     * @memberof MentorReviewDto\n     */\n    'maxScore': number;\n    /**\n     * Student github id\n     * @type {string}\n     * @memberof MentorReviewDto\n     */\n    'student': string;\n    /**\n     * Student id\n     * @type {number}\n     * @memberof MentorReviewDto\n     */\n    'studentId': number;\n    /**\n     * Task solution review date\n     * @type {string}\n     * @memberof MentorReviewDto\n     */\n    'reviewedAt': string;\n    /**\n     * Task description url\n     * @type {string}\n     * @memberof MentorReviewDto\n     */\n    'taskDescriptionUrl': string;\n}\n/**\n * \n * @export\n * @interface MentorReviewsDto\n */\nexport interface MentorReviewsDto {\n    /**\n     * \n     * @type {Array<MentorReviewDto>}\n     * @memberof MentorReviewsDto\n     */\n    'content': Array<MentorReviewDto>;\n    /**\n     * \n     * @type {PaginationMetaDto}\n     * @memberof MentorReviewsDto\n     */\n    'pagination': PaginationMetaDto;\n}\n/**\n * \n * @export\n * @interface MentorStudentDto\n */\nexport interface MentorStudentDto {\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorStudentDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof MentorStudentDto\n     */\n    'active': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentDto\n     */\n    'cityName': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentDto\n     */\n    'countryName': string | null;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorStudentDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof MentorStudentDto\n     */\n    'rank': number;\n    /**\n     * \n     * @type {Array<StudentFeedbackDto>}\n     * @memberof MentorStudentDto\n     */\n    'feedbacks': Array<StudentFeedbackDto>;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentDto\n     */\n    'repoUrl': string | null;\n}\n/**\n * \n * @export\n * @interface MentorStudentSummaryDto\n */\nexport interface MentorStudentSummaryDto {\n    /**\n     * \n     * @type {number}\n     * @memberof MentorStudentSummaryDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof MentorStudentSummaryDto\n     */\n    'isActive': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'cityName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'countryName': string;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof MentorStudentSummaryDto\n     */\n    'students': Array<string>;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'contactsEmail': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'contactsPhone': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'contactsSkype': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'contactsTelegram': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'contactsNotes': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof MentorStudentSummaryDto\n     */\n    'contactsWhatsApp': string | null;\n}\n/**\n * \n * @export\n * @interface NotificationConnectionDto\n */\nexport interface NotificationConnectionDto {\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationConnectionDto\n     */\n    'channelId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationConnectionDto\n     */\n    'externalId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof NotificationConnectionDto\n     */\n    'userId': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof NotificationConnectionDto\n     */\n    'enabled': boolean;\n}\n/**\n * \n * @export\n * @interface NotificationConnectionExistsDto\n */\nexport interface NotificationConnectionExistsDto {\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationConnectionExistsDto\n     */\n    'channelId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationConnectionExistsDto\n     */\n    'externalId'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof NotificationConnectionExistsDto\n     */\n    'userId'?: number;\n}\n/**\n * \n * @export\n * @interface NotificationDto\n */\nexport interface NotificationDto {\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationDto\n     */\n    'id': string;\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof NotificationDto\n     */\n    'enabled': boolean;\n    /**\n     * \n     * @type {NotificationType}\n     * @memberof NotificationDto\n     */\n    'type': NotificationType;\n    /**\n     * \n     * @type {Array<ChannelSettings>}\n     * @memberof NotificationDto\n     */\n    'channels': Array<ChannelSettings>;\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationDto\n     */\n    'parentId': string;\n}\n/**\n * \n * @export\n * @enum {string}\n */\n\nexport const NotificationType = {\n    Event: 'event',\n    Message: 'message'\n} as const;\n\nexport type NotificationType = typeof NotificationType[keyof typeof NotificationType];\n\n\n/**\n * \n * @export\n * @interface NotificationUserConnectionsDto\n */\nexport interface NotificationUserConnectionsDto {\n    /**\n     * \n     * @type {object}\n     * @memberof NotificationUserConnectionsDto\n     */\n    'connections': object;\n}\n/**\n * \n * @export\n * @interface NotificationUserSettingsDto\n */\nexport interface NotificationUserSettingsDto {\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationUserSettingsDto\n     */\n    'id': string;\n    /**\n     * \n     * @type {string}\n     * @memberof NotificationUserSettingsDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof NotificationUserSettingsDto\n     */\n    'enabled': boolean;\n    /**\n     * \n     * @type {object}\n     * @memberof NotificationUserSettingsDto\n     */\n    'settings': object;\n}\n/**\n * \n * @export\n * @interface Organizer\n */\nexport interface Organizer {\n    /**\n     * \n     * @type {number}\n     * @memberof Organizer\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface PaginationDto\n */\nexport interface PaginationDto {\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationDto\n     */\n    'pageSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationDto\n     */\n    'current': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationDto\n     */\n    'total': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationDto\n     */\n    'totalPages': number;\n}\n/**\n * \n * @export\n * @interface PaginationMetaDto\n */\nexport interface PaginationMetaDto {\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationMetaDto\n     */\n    'itemCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationMetaDto\n     */\n    'total': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationMetaDto\n     */\n    'current': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationMetaDto\n     */\n    'pageSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PaginationMetaDto\n     */\n    'totalPages': number;\n}\n/**\n * \n * @export\n * @interface PersonDto\n */\nexport interface PersonDto {\n    /**\n     * \n     * @type {string}\n     * @memberof PersonDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof PersonDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof PersonDto\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface PersonalProfileDto\n */\nexport interface PersonalProfileDto {\n    /**\n     * \n     * @type {number}\n     * @memberof PersonalProfileDto\n     */\n    'userId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof PersonalProfileDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof PersonalProfileDto\n     */\n    'primaryEmail': string | null;\n    /**\n     * \n     * @type {boolean}\n     * @memberof PersonalProfileDto\n     */\n    'isActiveStudent': boolean;\n}\n/**\n * \n * @export\n * @interface ProfileCourseDto\n */\nexport interface ProfileCourseDto {\n    /**\n     * \n     * @type {number}\n     * @memberof ProfileCourseDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'updatedDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'fullName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'alias': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof ProfileCourseDto\n     */\n    'year': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'registrationEndDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'primarySkillId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'primarySkillName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'locationName': string;\n    /**\n     * \n     * @type {number}\n     * @memberof ProfileCourseDto\n     */\n    'discordServerId': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ProfileCourseDto\n     */\n    'completed': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ProfileCourseDto\n     */\n    'planned': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ProfileCourseDto\n     */\n    'inviteOnly': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'certificateIssuer': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ProfileCourseDto\n     */\n    'usePrivateRepositories': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ProfileCourseDto\n     */\n    'personalMentoring': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'personalMentoringStartDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'personalMentoringEndDate': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'logo': string;\n    /**\n     * \n     * @type {IdNameDto}\n     * @memberof ProfileCourseDto\n     */\n    'discipline': IdNameDto | null;\n    /**\n     * \n     * @type {number}\n     * @memberof ProfileCourseDto\n     */\n    'minStudentsPerMentor': number;\n    /**\n     * \n     * @type {number}\n     * @memberof ProfileCourseDto\n     */\n    'certificateThreshold': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileCourseDto\n     */\n    'wearecommunityUrl': string | null;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof ProfileCourseDto\n     */\n    'certificateDisciplines': Array<number> | null;\n}\n/**\n * \n * @export\n * @interface ProfileDto\n */\nexport interface ProfileDto {\n    /**\n     * \n     * @type {string}\n     * @memberof ProfileDto\n     */\n    'publicCvUrl': string | null;\n}\n/**\n * \n * @export\n * @interface PromptDto\n */\nexport interface PromptDto {\n    /**\n     * \n     * @type {number}\n     * @memberof PromptDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof PromptDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {string}\n     * @memberof PromptDto\n     */\n    'text': string;\n    /**\n     * \n     * @type {number}\n     * @memberof PromptDto\n     */\n    'temperature': number;\n}\n/**\n * \n * @export\n * @interface PublicAttributesDto\n */\nexport interface PublicAttributesDto {\n    /**\n     * \n     * @type {number}\n     * @memberof PublicAttributesDto\n     */\n    'maxAttemptsNumber': number;\n    /**\n     * \n     * @type {number}\n     * @memberof PublicAttributesDto\n     */\n    'numberOfQuestions': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof PublicAttributesDto\n     */\n    'strictAttemptsMode': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof PublicAttributesDto\n     */\n    'tresholdPercentage': number;\n    /**\n     * \n     * @type {Array<QuestionDto>}\n     * @memberof PublicAttributesDto\n     */\n    'questions': Array<QuestionDto>;\n}\n/**\n * \n * @export\n * @interface PutInterviewFeedbackDto\n */\nexport interface PutInterviewFeedbackDto {\n    /**\n     * \n     * @type {number}\n     * @memberof PutInterviewFeedbackDto\n     */\n    'version': number;\n    /**\n     * \n     * @type {object}\n     * @memberof PutInterviewFeedbackDto\n     */\n    'json': object;\n    /**\n     * \n     * @type {string}\n     * @memberof PutInterviewFeedbackDto\n     */\n    'decision'?: string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof PutInterviewFeedbackDto\n     */\n    'isGoodCandidate'?: boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof PutInterviewFeedbackDto\n     */\n    'isCompleted': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof PutInterviewFeedbackDto\n     */\n    'score'?: number;\n}\n/**\n * \n * @export\n * @interface QuestionDto\n */\nexport interface QuestionDto {\n    /**\n     * \n     * @type {string}\n     * @memberof QuestionDto\n     */\n    'question': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof QuestionDto\n     */\n    'multiple': boolean;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof QuestionDto\n     */\n    'answers': Array<string>;\n    /**\n     * \n     * @type {string}\n     * @memberof QuestionDto\n     */\n    'questionImage'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof QuestionDto\n     */\n    'answersType'?: string;\n}\n/**\n * \n * @export\n * @interface ResultDto\n */\nexport interface ResultDto {\n    /**\n     * \n     * @type {number}\n     * @memberof ResultDto\n     */\n    'score'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof ResultDto\n     */\n    'courseTaskId'?: number;\n}\n/**\n * \n * @export\n * @interface ResumeCourseDto\n */\nexport interface ResumeCourseDto {\n    /**\n     * \n     * @type {number}\n     * @memberof ResumeCourseDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeCourseDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeCourseDto\n     */\n    'fullName': string;\n    /**\n     * \n     * @type {number}\n     * @memberof ResumeCourseDto\n     */\n    'rank': number;\n    /**\n     * \n     * @type {number}\n     * @memberof ResumeCourseDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeCourseDto\n     */\n    'certificateId': string | null;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ResumeCourseDto\n     */\n    'completed': boolean;\n    /**\n     * \n     * @type {ResumeCourseMentor}\n     * @memberof ResumeCourseDto\n     */\n    'mentor': ResumeCourseMentor | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeCourseDto\n     */\n    'locationName': string;\n}\n/**\n * \n * @export\n * @interface ResumeCourseMentor\n */\nexport interface ResumeCourseMentor {\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeCourseMentor\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeCourseMentor\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof ResumeCourseMentor\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface ResumeDto\n */\nexport interface ResumeDto {\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'uuid': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'avatarLink': string | null;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof ResumeDto\n     */\n    'visibleCourses': Array<number>;\n    /**\n     * \n     * @type {Array<ResumeCourseDto>}\n     * @memberof ResumeDto\n     */\n    'courses': Array<ResumeCourseDto>;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'desiredPosition': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'email': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'englishLevel': ResumeDtoEnglishLevelEnum;\n    /**\n     * \n     * @type {number}\n     * @memberof ResumeDto\n     */\n    'expires': number | null;\n    /**\n     * \n     * @type {Array<GratitudeDto>}\n     * @memberof ResumeDto\n     */\n    'gratitudes': Array<GratitudeDto>;\n    /**\n     * \n     * @type {Array<FeedbackDto>}\n     * @memberof ResumeDto\n     */\n    'feedbacks': Array<FeedbackDto>;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ResumeDto\n     */\n    'fullTime': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'githubUsername': string | null;\n    /**\n     * \n     * @type {number}\n     * @memberof ResumeDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'linkedin': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'locations': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'militaryService': ResumeDtoMilitaryServiceEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'name': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'notes': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'phone': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'selfIntroLink': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'skype': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'startFrom': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'telegram': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ResumeDto\n     */\n    'website': string | null;\n}\n\nexport const ResumeDtoEnglishLevelEnum = {\n    Unknown: 'unknown',\n    A0: 'a0',\n    A1: 'a1',\n    A2: 'a2',\n    B1: 'b1',\n    B2: 'b2',\n    C1: 'c1',\n    C2: 'c2'\n} as const;\n\nexport type ResumeDtoEnglishLevelEnum = typeof ResumeDtoEnglishLevelEnum[keyof typeof ResumeDtoEnglishLevelEnum];\nexport const ResumeDtoMilitaryServiceEnum = {\n    Served: 'served',\n    Liable: 'liable',\n    NotLiable: 'notLiable'\n} as const;\n\nexport type ResumeDtoMilitaryServiceEnum = typeof ResumeDtoMilitaryServiceEnum[keyof typeof ResumeDtoMilitaryServiceEnum];\n\n/**\n * \n * @export\n * @interface SaveCertificateDto\n */\nexport interface SaveCertificateDto {\n    /**\n     * \n     * @type {string}\n     * @memberof SaveCertificateDto\n     */\n    'publicId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof SaveCertificateDto\n     */\n    'studentId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof SaveCertificateDto\n     */\n    's3Bucket': string;\n    /**\n     * \n     * @type {string}\n     * @memberof SaveCertificateDto\n     */\n    's3Key': string;\n    /**\n     * \n     * @type {string}\n     * @memberof SaveCertificateDto\n     */\n    'issueDate': string;\n}\n/**\n * \n * @export\n * @interface SaveTaskSolutionDto\n */\nexport interface SaveTaskSolutionDto {\n    /**\n     * \n     * @type {string}\n     * @memberof SaveTaskSolutionDto\n     */\n    'url': string;\n}\n/**\n * \n * @export\n * @interface ScoreDto\n */\nexport interface ScoreDto {\n    /**\n     * \n     * @type {Array<ScoreStudentDto>}\n     * @memberof ScoreDto\n     */\n    'content': Array<ScoreStudentDto>;\n    /**\n     * \n     * @type {PaginationMetaDto}\n     * @memberof ScoreDto\n     */\n    'pagination': PaginationMetaDto;\n}\n/**\n * \n * @export\n * @interface ScoreStudentDto\n */\nexport interface ScoreStudentDto {\n    /**\n     * \n     * @type {string}\n     * @memberof ScoreStudentDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof ScoreStudentDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof ScoreStudentDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ScoreStudentDto\n     */\n    'active': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof ScoreStudentDto\n     */\n    'cityName': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ScoreStudentDto\n     */\n    'countryName': string | null;\n    /**\n     * \n     * @type {number}\n     * @memberof ScoreStudentDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof ScoreStudentDto\n     */\n    'rank': number;\n    /**\n     * \n     * @type {MentorDto}\n     * @memberof ScoreStudentDto\n     */\n    'mentor': MentorDto | null;\n    /**\n     * \n     * @type {string}\n     * @memberof ScoreStudentDto\n     */\n    'totalScoreChangeDate': string;\n    /**\n     * \n     * @type {number}\n     * @memberof ScoreStudentDto\n     */\n    'crossCheckScore': number;\n    /**\n     * \n     * @type {string}\n     * @memberof ScoreStudentDto\n     */\n    'repositoryLastActivityDate': string;\n    /**\n     * \n     * @type {Array<TaskResultsDto>}\n     * @memberof ScoreStudentDto\n     */\n    'taskResults': Array<TaskResultsDto>;\n    /**\n     * \n     * @type {boolean}\n     * @memberof ScoreStudentDto\n     */\n    'isActive': boolean;\n    /**\n     * \n     * @type {ContactsDto}\n     * @memberof ScoreStudentDto\n     */\n    'contacts': ContactsDto;\n}\n/**\n * \n * @export\n * @interface SearchMentorDto\n */\nexport interface SearchMentorDto {\n    /**\n     * \n     * @type {number}\n     * @memberof SearchMentorDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof SearchMentorDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof SearchMentorDto\n     */\n    'name': string;\n}\n/**\n * \n * @export\n * @interface SelfEducationQuestionSelectedAnswersDto\n */\nexport interface SelfEducationQuestionSelectedAnswersDto {\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof SelfEducationQuestionSelectedAnswersDto\n     */\n    'answers': Array<string>;\n    /**\n     * \n     * @type {string}\n     * @memberof SelfEducationQuestionSelectedAnswersDto\n     */\n    'question': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof SelfEducationQuestionSelectedAnswersDto\n     */\n    'multiple': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof SelfEducationQuestionSelectedAnswersDto\n     */\n    'questionImage'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof SelfEducationQuestionSelectedAnswersDto\n     */\n    'answersType'?: string;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof SelfEducationQuestionSelectedAnswersDto\n     */\n    'selectedAnswers': Array<number>;\n}\n/**\n * \n * @export\n * @interface SendUserNotificationDto\n */\nexport interface SendUserNotificationDto {\n    /**\n     * \n     * @type {string}\n     * @memberof SendUserNotificationDto\n     */\n    'notificationId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof SendUserNotificationDto\n     */\n    'userId': number;\n    /**\n     * \n     * @type {object}\n     * @memberof SendUserNotificationDto\n     */\n    'data': object;\n    /**\n     * \n     * @type {number}\n     * @memberof SendUserNotificationDto\n     */\n    'expireDate': number;\n}\n/**\n * \n * @export\n * @interface SenderDto\n */\nexport interface SenderDto {\n    /**\n     * \n     * @type {SenderLoginDto}\n     * @memberof SenderDto\n     */\n    'login': SenderLoginDto;\n}\n/**\n * \n * @export\n * @interface SenderLoginDto\n */\nexport interface SenderLoginDto {\n    /**\n     * \n     * @type {string}\n     * @memberof SenderLoginDto\n     */\n    'githubId': string;\n}\n/**\n * \n * @export\n * @interface SoftSkillEntry\n */\nexport interface SoftSkillEntry {\n    /**\n     * \n     * @type {string}\n     * @memberof SoftSkillEntry\n     */\n    'id': SoftSkillEntryIdEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof SoftSkillEntry\n     */\n    'value': SoftSkillEntryValueEnum;\n}\n\nexport const SoftSkillEntryIdEnum = {\n    Responsible: 'skill.soft.responsible',\n    TeamPlayer: 'skill.soft.team-player',\n    Communicable: 'skill.soft.communicable'\n} as const;\n\nexport type SoftSkillEntryIdEnum = typeof SoftSkillEntryIdEnum[keyof typeof SoftSkillEntryIdEnum];\nexport const SoftSkillEntryValueEnum = {\n    None: 'None',\n    Poor: 'Poor',\n    Fair: 'Fair',\n    Good: 'Good',\n    Great: 'Great',\n    Excellent: 'Excellent'\n} as const;\n\nexport type SoftSkillEntryValueEnum = typeof SoftSkillEntryValueEnum[keyof typeof SoftSkillEntryValueEnum];\n\n/**\n * \n * @export\n * @enum {string}\n */\n\nexport const SolutionItemStatusEnum = {\n    InReview: 'in-review',\n    Done: 'done',\n    RandomTask: 'random-task'\n} as const;\n\nexport type SolutionItemStatusEnum = typeof SolutionItemStatusEnum[keyof typeof SolutionItemStatusEnum];\n\n\n/**\n * \n * @export\n * @interface StatusDto\n */\nexport interface StatusDto {\n    /**\n     * \n     * @type {number}\n     * @memberof StatusDto\n     */\n    'expires': number;\n}\n/**\n * \n * @export\n * @interface StudentDto\n */\nexport interface StudentDto {\n    /**\n     * \n     * @type {string}\n     * @memberof StudentDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof StudentDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof StudentDto\n     */\n    'active': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentDto\n     */\n    'cityName': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentDto\n     */\n    'countryName': string | null;\n    /**\n     * \n     * @type {number}\n     * @memberof StudentDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof StudentDto\n     */\n    'rank': number;\n}\n/**\n * \n * @export\n * @interface StudentFeedbackContentDto\n */\nexport interface StudentFeedbackContentDto {\n    /**\n     * \n     * @type {string}\n     * @memberof StudentFeedbackContentDto\n     */\n    'suggestions': string;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentFeedbackContentDto\n     */\n    'recommendationComment': string;\n    /**\n     * \n     * @type {Array<SoftSkillEntry>}\n     * @memberof StudentFeedbackContentDto\n     */\n    'softSkills': Array<SoftSkillEntry>;\n}\n/**\n * \n * @export\n * @interface StudentFeedbackDto\n */\nexport interface StudentFeedbackDto {\n    /**\n     * \n     * @type {number}\n     * @memberof StudentFeedbackDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentFeedbackDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentFeedbackDto\n     */\n    'updatedDate': string;\n    /**\n     * \n     * @type {StudentFeedbackContentDto}\n     * @memberof StudentFeedbackDto\n     */\n    'content': StudentFeedbackContentDto;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentFeedbackDto\n     */\n    'recommendation': StudentFeedbackDtoRecommendationEnum;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof StudentFeedbackDto\n     */\n    'author': PersonDto;\n    /**\n     * \n     * @type {PersonDto}\n     * @memberof StudentFeedbackDto\n     */\n    'mentor': PersonDto | null;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentFeedbackDto\n     */\n    'englishLevel': StudentFeedbackDtoEnglishLevelEnum;\n}\n\nexport const StudentFeedbackDtoRecommendationEnum = {\n    Hire: 'hire',\n    NotHire: 'not-hire'\n} as const;\n\nexport type StudentFeedbackDtoRecommendationEnum = typeof StudentFeedbackDtoRecommendationEnum[keyof typeof StudentFeedbackDtoRecommendationEnum];\nexport const StudentFeedbackDtoEnglishLevelEnum = {\n    Unknown: 'unknown',\n    A0: 'a0',\n    A1: 'a1',\n    A2: 'a2',\n    B1: 'b1',\n    B2: 'b2',\n    C1: 'c1',\n    C2: 'c2'\n} as const;\n\nexport type StudentFeedbackDtoEnglishLevelEnum = typeof StudentFeedbackDtoEnglishLevelEnum[keyof typeof StudentFeedbackDtoEnglishLevelEnum];\n\n/**\n * \n * @export\n * @interface StudentId\n */\nexport interface StudentId {\n    /**\n     * \n     * @type {number}\n     * @memberof StudentId\n     */\n    'id': number;\n}\n/**\n * \n * @export\n * @interface StudentSummaryDto\n */\nexport interface StudentSummaryDto {\n    /**\n     * \n     * @type {number}\n     * @memberof StudentSummaryDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {Array<ResultDto>}\n     * @memberof StudentSummaryDto\n     */\n    'results': Array<ResultDto>;\n    /**\n     * \n     * @type {boolean}\n     * @memberof StudentSummaryDto\n     */\n    'isActive': boolean;\n    /**\n     * \n     * @type {MentorStudentSummaryDto}\n     * @memberof StudentSummaryDto\n     */\n    'mentor': MentorStudentSummaryDto | null;\n    /**\n     * \n     * @type {number}\n     * @memberof StudentSummaryDto\n     */\n    'rank': number;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentSummaryDto\n     */\n    'repository': string | null;\n}\n/**\n * \n * @export\n * @interface StudentsDto\n */\nexport interface StudentsDto {\n    /**\n     * \n     * @type {number}\n     * @memberof StudentsDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentsDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof StudentsDto\n     */\n    'name': string;\n}\n/**\n * \n * @export\n * @interface TaskCriteriaDto\n */\nexport interface TaskCriteriaDto {\n    /**\n     * \n     * @type {Array<CriteriaDto>}\n     * @memberof TaskCriteriaDto\n     */\n    'criteria': Array<CriteriaDto>;\n}\n/**\n * \n * @export\n * @interface TaskDto\n */\nexport interface TaskDto {\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'type': TaskDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof TaskDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'githubRepoName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'sourceGithubRepoUrl': string;\n    /**\n     * \n     * @type {IdNameDto}\n     * @memberof TaskDto\n     */\n    'discipline': IdNameDto;\n    /**\n     * \n     * @type {boolean}\n     * @memberof TaskDto\n     */\n    'githubPrRequired': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'createdDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskDto\n     */\n    'updatedDate': string;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof TaskDto\n     */\n    'tags': Array<string>;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof TaskDto\n     */\n    'skills': Array<string>;\n    /**\n     * \n     * @type {object}\n     * @memberof TaskDto\n     */\n    'attributes': object;\n    /**\n     * \n     * @type {Array<UsedCourseDto>}\n     * @memberof TaskDto\n     */\n    'courses': Array<UsedCourseDto>;\n}\n\nexport const TaskDtoTypeEnum = {\n    Jstask: 'jstask',\n    Kotlintask: 'kotlintask',\n    Objctask: 'objctask',\n    Htmltask: 'htmltask',\n    Ipynb: 'ipynb',\n    Selfeducation: 'selfeducation',\n    Codewars: 'codewars',\n    Test: 'test',\n    Codejam: 'codejam',\n    Interview: 'interview',\n    StageInterview: 'stage-interview',\n    Cvhtml: 'cv:html',\n    Cvmarkdown: 'cv:markdown'\n} as const;\n\nexport type TaskDtoTypeEnum = typeof TaskDtoTypeEnum[keyof typeof TaskDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface TaskPerformanceStatsDto\n */\nexport interface TaskPerformanceStatsDto {\n    /**\n     * Total number of students who submitted the task\n     * @type {number}\n     * @memberof TaskPerformanceStatsDto\n     */\n    'totalAchievement': number;\n    /**\n     * Number of students scoring between 1% and 20% of the maximum points\n     * @type {number}\n     * @memberof TaskPerformanceStatsDto\n     */\n    'minimalAchievement': number;\n    /**\n     * Number of students scoring between 21% and 50% of the maximum points\n     * @type {number}\n     * @memberof TaskPerformanceStatsDto\n     */\n    'lowAchievement': number;\n    /**\n     * Number of students scoring between 51% and 70% of the maximum points\n     * @type {number}\n     * @memberof TaskPerformanceStatsDto\n     */\n    'moderateAchievement': number;\n    /**\n     * Number of students scoring between 71% and 90% of the maximum points\n     * @type {number}\n     * @memberof TaskPerformanceStatsDto\n     */\n    'highAchievement': number;\n    /**\n     * Number of students scoring between 91% and 99% of the maximum points\n     * @type {number}\n     * @memberof TaskPerformanceStatsDto\n     */\n    'exceptionalAchievement': number;\n    /**\n     * Number of students achieving a perfect score of 100%\n     * @type {number}\n     * @memberof TaskPerformanceStatsDto\n     */\n    'perfectScores': number;\n}\n/**\n * \n * @export\n * @interface TaskResultsDto\n */\nexport interface TaskResultsDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TaskResultsDto\n     */\n    'courseTaskId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TaskResultsDto\n     */\n    'score': number;\n}\n/**\n * \n * @export\n * @interface TaskSolutionDto\n */\nexport interface TaskSolutionDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TaskSolutionDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TaskSolutionDto\n     */\n    'courseTaskId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TaskSolutionDto\n     */\n    'url': string;\n}\n/**\n * \n * @export\n * @interface TaskVerificationAttemptDto\n */\nexport interface TaskVerificationAttemptDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TaskVerificationAttemptDto\n     */\n    'createdDate': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TaskVerificationAttemptDto\n     */\n    'courseTaskId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TaskVerificationAttemptDto\n     */\n    'score': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TaskVerificationAttemptDto\n     */\n    'maxScore': number;\n    /**\n     * \n     * @type {Array<SelfEducationQuestionSelectedAnswersDto>}\n     * @memberof TaskVerificationAttemptDto\n     */\n    'questions': Array<SelfEducationQuestionSelectedAnswersDto>;\n}\n/**\n * \n * @export\n * @interface TeamDistributionDetailedDto\n */\nexport interface TeamDistributionDetailedDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'courseId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'studentsWithoutTeamCount': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'teamsCount': number;\n    /**\n     * \n     * @type {TeamDto}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'myTeam': TeamDto;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'minTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'maxTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'strictTeamSize': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof TeamDistributionDetailedDto\n     */\n    'strictTeamSizeMode': boolean;\n}\n/**\n * \n * @export\n * @interface TeamDistributionDto\n */\nexport interface TeamDistributionDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionDto\n     */\n    'registrationStatus': TeamDistributionDtoRegistrationStatusEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDto\n     */\n    'minTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDto\n     */\n    'maxTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDto\n     */\n    'strictTeamSize': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof TeamDistributionDto\n     */\n    'strictTeamSizeMode': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionDto\n     */\n    'minTotalScore': number;\n}\n\nexport const TeamDistributionDtoRegistrationStatusEnum = {\n    Available: 'available',\n    Unavailable: 'unavailable',\n    Future: 'future',\n    Completed: 'completed',\n    Distributed: 'distributed',\n    Closed: 'closed'\n} as const;\n\nexport type TeamDistributionDtoRegistrationStatusEnum = typeof TeamDistributionDtoRegistrationStatusEnum[keyof typeof TeamDistributionDtoRegistrationStatusEnum];\n\n/**\n * \n * @export\n * @interface TeamDistributionStudentDto\n */\nexport interface TeamDistributionStudentDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionStudentDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionStudentDto\n     */\n    'fullName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionStudentDto\n     */\n    'cvLink': string;\n    /**\n     * \n     * @type {Discord}\n     * @memberof TeamDistributionStudentDto\n     */\n    'discord': Discord | null;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionStudentDto\n     */\n    'telegram': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionStudentDto\n     */\n    'email': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionStudentDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionStudentDto\n     */\n    'rank': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDistributionStudentDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionStudentDto\n     */\n    'location': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDistributionStudentDto\n     */\n    'cvUuid': string;\n}\n/**\n * \n * @export\n * @interface TeamDto\n */\nexport interface TeamDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDto\n     */\n    'chatLink': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDto\n     */\n    'teamLeadId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamDto\n     */\n    'teamDistributionId': number;\n    /**\n     * \n     * @type {Array<TeamDistributionStudentDto>}\n     * @memberof TeamDto\n     */\n    'students': Array<TeamDistributionStudentDto>;\n}\n/**\n * \n * @export\n * @interface TeamInfoDto\n */\nexport interface TeamInfoDto {\n    /**\n     * \n     * @type {number}\n     * @memberof TeamInfoDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamInfoDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamInfoDto\n     */\n    'chatLink': string;\n    /**\n     * \n     * @type {string}\n     * @memberof TeamInfoDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamInfoDto\n     */\n    'teamLeadId': number;\n    /**\n     * \n     * @type {number}\n     * @memberof TeamInfoDto\n     */\n    'teamDistributionId': number;\n}\n/**\n * \n * @export\n * @interface TeamPasswordDto\n */\nexport interface TeamPasswordDto {\n    /**\n     * \n     * @type {string}\n     * @memberof TeamPasswordDto\n     */\n    'password': string;\n}\n/**\n * \n * @export\n * @interface TeamsDto\n */\nexport interface TeamsDto {\n    /**\n     * \n     * @type {Array<TeamDto>}\n     * @memberof TeamsDto\n     */\n    'content': Array<TeamDto>;\n    /**\n     * \n     * @type {PaginationMetaDto}\n     * @memberof TeamsDto\n     */\n    'pagination': PaginationMetaDto;\n}\n/**\n * \n * @export\n * @interface TopMentorDto\n */\nexport interface TopMentorDto {\n    /**\n     * Position in the mentors ranking\n     * @type {number}\n     * @memberof TopMentorDto\n     */\n    'rank': number;\n    /**\n     * GitHub username\n     * @type {string}\n     * @memberof TopMentorDto\n     */\n    'githubId': string;\n    /**\n     * Full name of the mentor\n     * @type {string}\n     * @memberof TopMentorDto\n     */\n    'name': string;\n    /**\n     * Total number of certified students mentored\n     * @type {number}\n     * @memberof TopMentorDto\n     */\n    'totalStudents': number;\n    /**\n     * Total number of gratitudes received\n     * @type {number}\n     * @memberof TopMentorDto\n     */\n    'totalGratitudes': number;\n    /**\n     * Student counts per course\n     * @type {Array<MentorCourseStatsDto>}\n     * @memberof TopMentorDto\n     */\n    'courseStats': Array<MentorCourseStatsDto>;\n}\n/**\n * \n * @export\n * @interface UpdateContributorDto\n */\nexport interface UpdateContributorDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateContributorDto\n     */\n    'description'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateContributorDto\n     */\n    'userId'?: number;\n}\n/**\n * \n * @export\n * @interface UpdateCourseDto\n */\nexport interface UpdateCourseDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'name'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'fullName'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'alias'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'description'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'descriptionUrl'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseDto\n     */\n    'year'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'startDate'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'endDate'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'registrationEndDate'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'locationName'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseDto\n     */\n    'discordServerId'?: number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseDto\n     */\n    'completed'?: boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseDto\n     */\n    'planned'?: boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseDto\n     */\n    'inviteOnly'?: boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'certificateIssuer'?: string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseDto\n     */\n    'usePrivateRepositories'?: boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseDto\n     */\n    'personalMentoring'?: boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'personalMentoringStartDate'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'personalMentoringEndDate'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'logo'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseDto\n     */\n    'disciplineId'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseDto\n     */\n    'minStudentsPerMentor'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseDto\n     */\n    'certificateThreshold': number;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseDto\n     */\n    'wearecommunityUrl'?: string | null;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof UpdateCourseDto\n     */\n    'certificateDisciplines'?: Array<string> | null;\n}\n/**\n * \n * @export\n * @interface UpdateCourseEventDto\n */\nexport interface UpdateCourseEventDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseEventDto\n     */\n    'special'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseEventDto\n     */\n    'dateTime'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseEventDto\n     */\n    'endTime'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseEventDto\n     */\n    'duration'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseEventDto\n     */\n    'place'?: string;\n    /**\n     * \n     * @type {Organizer}\n     * @memberof UpdateCourseEventDto\n     */\n    'organizer'?: Organizer;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseEventDto\n     */\n    'organizerId'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseEventDto\n     */\n    'broadcastUrl'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseEventDto\n     */\n    'coordinator'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseEventDto\n     */\n    'comment'?: string;\n}\n/**\n * \n * @export\n * @interface UpdateCourseTaskDto\n */\nexport interface UpdateCourseTaskDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'type'?: UpdateCourseTaskDtoTypeEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'name'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'checker'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'studentStartDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'studentEndDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'descriptionUrl'?: string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseTaskDto\n     */\n    'taskOwnerId'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseTaskDto\n     */\n    'maxScore'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseTaskDto\n     */\n    'scoreWeight'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseTaskDto\n     */\n    'pairsCount'?: number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseTaskDto\n     */\n    'taskId'?: number;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'crossCheckEndDate'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'submitText': string;\n    /**\n     * \n     * @type {object}\n     * @memberof UpdateCourseTaskDto\n     */\n    'validations': object;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateCourseTaskDto\n     */\n    'studentRegistrationStartDate'?: string;\n}\n\nexport const UpdateCourseTaskDtoTypeEnum = {\n    Jstask: 'jstask',\n    Kotlintask: 'kotlintask',\n    Objctask: 'objctask',\n    Htmltask: 'htmltask',\n    Ipynb: 'ipynb',\n    Selfeducation: 'selfeducation',\n    Codewars: 'codewars',\n    Test: 'test',\n    Codejam: 'codejam',\n    Interview: 'interview',\n    StageInterview: 'stage-interview',\n    Cvhtml: 'cv:html',\n    Cvmarkdown: 'cv:markdown'\n} as const;\n\nexport type UpdateCourseTaskDtoTypeEnum = typeof UpdateCourseTaskDtoTypeEnum[keyof typeof UpdateCourseTaskDtoTypeEnum];\n\n/**\n * \n * @export\n * @interface UpdateCourseUserDto\n */\nexport interface UpdateCourseUserDto {\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseUserDto\n     */\n    'isManager': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseUserDto\n     */\n    'isSupervisor': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseUserDto\n     */\n    'isDementor': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateCourseUserDto\n     */\n    'isActivist': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateCourseUserDto\n     */\n    'userId': number;\n}\n/**\n * \n * @export\n * @interface UpdateDisciplineDto\n */\nexport interface UpdateDisciplineDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateDisciplineDto\n     */\n    'name': string;\n}\n/**\n * \n * @export\n * @interface UpdateDiscordServerDto\n */\nexport interface UpdateDiscordServerDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateDiscordServerDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateDiscordServerDto\n     */\n    'gratitudeUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateDiscordServerDto\n     */\n    'mentorsChatUrl': string;\n}\n/**\n * \n * @export\n * @interface UpdateEventDto\n */\nexport interface UpdateEventDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateEventDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateEventDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateEventDto\n     */\n    'disciplineId': number;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateEventDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateEventDto\n     */\n    'description': string;\n}\n/**\n * \n * @export\n * @interface UpdateNotificationDto\n */\nexport interface UpdateNotificationDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateNotificationDto\n     */\n    'id': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateNotificationDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateNotificationDto\n     */\n    'enabled': boolean;\n    /**\n     * \n     * @type {Array<ChannelSettings>}\n     * @memberof UpdateNotificationDto\n     */\n    'channels': Array<ChannelSettings>;\n    /**\n     * \n     * @type {NotificationType}\n     * @memberof UpdateNotificationDto\n     */\n    'type': NotificationType;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateNotificationDto\n     */\n    'parentId': string;\n}\n/**\n * \n * @export\n * @interface UpdateNotificationUserSettingsDto\n */\nexport interface UpdateNotificationUserSettingsDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateNotificationUserSettingsDto\n     */\n    'notificationId': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateNotificationUserSettingsDto\n     */\n    'enabled': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateNotificationUserSettingsDto\n     */\n    'channelId': string;\n}\n/**\n * \n * @export\n * @interface UpdateProfileInfoDto\n */\nexport interface UpdateProfileInfoDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'name'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'githubId'?: string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'aboutMyself'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'cityName'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'countryName'?: string | null;\n    /**\n     * \n     * @type {Array<Education>}\n     * @memberof UpdateProfileInfoDto\n     */\n    'educationHistory'?: Array<Education> | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'englishLevel'?: string | null;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof UpdateProfileInfoDto\n     */\n    'languages'?: Array<string>;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsPhone'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsEmail'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsEpamEmail'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsSkype'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsWhatsApp'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsTelegram'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsNotes'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateProfileInfoDto\n     */\n    'contactsLinkedIn'?: string | null;\n    /**\n     * \n     * @type {Discord}\n     * @memberof UpdateProfileInfoDto\n     */\n    'discord'?: Discord | null;\n}\n/**\n * \n * @export\n * @interface UpdatePromptDto\n */\nexport interface UpdatePromptDto {\n    /**\n     * \n     * @type {number}\n     * @memberof UpdatePromptDto\n     */\n    'temperature': number;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdatePromptDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdatePromptDto\n     */\n    'text': string;\n}\n/**\n * \n * @export\n * @interface UpdateStudentFeedbackDto\n */\nexport interface UpdateStudentFeedbackDto {\n    /**\n     * \n     * @type {StudentFeedbackContentDto}\n     * @memberof UpdateStudentFeedbackDto\n     */\n    'content': StudentFeedbackContentDto;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateStudentFeedbackDto\n     */\n    'recommendation': UpdateStudentFeedbackDtoRecommendationEnum;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateStudentFeedbackDto\n     */\n    'englishLevel': UpdateStudentFeedbackDtoEnglishLevelEnum;\n}\n\nexport const UpdateStudentFeedbackDtoRecommendationEnum = {\n    Hire: 'hire',\n    NotHire: 'not-hire'\n} as const;\n\nexport type UpdateStudentFeedbackDtoRecommendationEnum = typeof UpdateStudentFeedbackDtoRecommendationEnum[keyof typeof UpdateStudentFeedbackDtoRecommendationEnum];\nexport const UpdateStudentFeedbackDtoEnglishLevelEnum = {\n    Unknown: 'unknown',\n    A0: 'a0',\n    A1: 'a1',\n    A2: 'a2',\n    B1: 'b1',\n    B2: 'b2',\n    C1: 'c1',\n    C2: 'c2'\n} as const;\n\nexport type UpdateStudentFeedbackDtoEnglishLevelEnum = typeof UpdateStudentFeedbackDtoEnglishLevelEnum[keyof typeof UpdateStudentFeedbackDtoEnglishLevelEnum];\n\n/**\n * \n * @export\n * @interface UpdateTaskDto\n */\nexport interface UpdateTaskDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTaskDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {object}\n     * @memberof UpdateTaskDto\n     */\n    'attributes': object;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTaskDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTaskDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTaskDto\n     */\n    'githubRepoName': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTaskDto\n     */\n    'sourceGithubRepoUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateTaskDto\n     */\n    'disciplineId': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateTaskDto\n     */\n    'githubPrRequired': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTaskDto\n     */\n    'type': string;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof UpdateTaskDto\n     */\n    'skills': Array<string>;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof UpdateTaskDto\n     */\n    'tags': Array<string>;\n}\n/**\n * \n * @export\n * @interface UpdateTeamDistributionDto\n */\nexport interface UpdateTeamDistributionDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'startDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'endDate': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'descriptionUrl': string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'minTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'maxTeamSize': number;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'strictTeamSize': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'strictTeamSizeMode': boolean;\n    /**\n     * \n     * @type {number}\n     * @memberof UpdateTeamDistributionDto\n     */\n    'minTotalScore': number;\n}\n/**\n * \n * @export\n * @interface UpdateTeamDto\n */\nexport interface UpdateTeamDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDto\n     */\n    'description': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateTeamDto\n     */\n    'chatLink': string;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof UpdateTeamDto\n     */\n    'studentIds'?: Array<number>;\n}\n/**\n * \n * @export\n * @interface UpdateUserDto\n */\nexport interface UpdateUserDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'firstName'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'lastName'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'primaryEmail'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'cityName'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'countryName'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsNotes'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsPhone'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsEmail'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsEpamEmail'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsSkype'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsWhatsApp'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsTelegram'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'contactsLinkedIn'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'notes'?: string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserDto\n     */\n    'aboutMyself'?: string | null;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof UpdateUserDto\n     */\n    'languages'?: Array<UpdateUserDtoLanguagesEnum>;\n}\n\nexport const UpdateUserDtoLanguagesEnum = {\n    En: 'EN',\n    Zh: 'ZH',\n    Hi: 'HI',\n    Es: 'ES',\n    Fr: 'FR',\n    Ar: 'AR',\n    Bn: 'BN',\n    Ru: 'RU',\n    Pt: 'PT',\n    Id: 'ID',\n    Ur: 'UR',\n    Ja: 'JA',\n    De: 'DE',\n    Pa: 'PA',\n    Te: 'TE',\n    Tr: 'TR',\n    Ko: 'KO',\n    Mr: 'MR',\n    Ky: 'KY',\n    Kk: 'KK',\n    Uz: 'UZ',\n    Ka: 'KA',\n    Pl: 'PL',\n    Lt: 'LT',\n    Lv: 'LV',\n    Be: 'BE',\n    Uk: 'UK'\n} as const;\n\nexport type UpdateUserDtoLanguagesEnum = typeof UpdateUserDtoLanguagesEnum[keyof typeof UpdateUserDtoLanguagesEnum];\n\n/**\n * \n * @export\n * @interface UpdateUserGroupDto\n */\nexport interface UpdateUserGroupDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpdateUserGroupDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {Array<number>}\n     * @memberof UpdateUserGroupDto\n     */\n    'users': Array<number>;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof UpdateUserGroupDto\n     */\n    'roles': Array<UpdateUserGroupDtoRolesEnum>;\n}\n\nexport const UpdateUserGroupDtoRolesEnum = {\n    TaskOwner: 'taskOwner',\n    Manager: 'manager',\n    Supervisor: 'supervisor',\n    Student: 'student',\n    Mentor: 'mentor',\n    Dementor: 'dementor',\n    Activist: 'activist'\n} as const;\n\nexport type UpdateUserGroupDtoRolesEnum = typeof UpdateUserGroupDtoRolesEnum[keyof typeof UpdateUserGroupDtoRolesEnum];\n\n/**\n * \n * @export\n * @interface UpsertNotificationConnectionDto\n */\nexport interface UpsertNotificationConnectionDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UpsertNotificationConnectionDto\n     */\n    'channelId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UpsertNotificationConnectionDto\n     */\n    'externalId': string;\n    /**\n     * \n     * @type {number}\n     * @memberof UpsertNotificationConnectionDto\n     */\n    'userId': number;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UpsertNotificationConnectionDto\n     */\n    'enabled': boolean;\n}\n/**\n * \n * @export\n * @interface UsedCourseDto\n */\nexport interface UsedCourseDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UsedCourseDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UsedCourseDto\n     */\n    'isActive': boolean;\n}\n/**\n * \n * @export\n * @interface UserDto\n */\nexport interface UserDto {\n    /**\n     * \n     * @type {number}\n     * @memberof UserDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof UserDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UserDto\n     */\n    'githubId': string;\n}\n/**\n * \n * @export\n * @interface UserGroupDto\n */\nexport interface UserGroupDto {\n    /**\n     * \n     * @type {number}\n     * @memberof UserGroupDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof UserGroupDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {Array<UserDto>}\n     * @memberof UserGroupDto\n     */\n    'users': Array<UserDto>;\n    /**\n     * \n     * @type {Array<string>}\n     * @memberof UserGroupDto\n     */\n    'roles': Array<UserGroupDtoRolesEnum>;\n}\n\nexport const UserGroupDtoRolesEnum = {\n    TaskOwner: 'taskOwner',\n    Manager: 'manager',\n    Supervisor: 'supervisor',\n    Student: 'student',\n    Mentor: 'mentor',\n    Dementor: 'dementor',\n    Activist: 'activist'\n} as const;\n\nexport type UserGroupDtoRolesEnum = typeof UserGroupDtoRolesEnum[keyof typeof UserGroupDtoRolesEnum];\n\n/**\n * \n * @export\n * @interface UserNotificationsDto\n */\nexport interface UserNotificationsDto {\n    /**\n     * \n     * @type {Array<NotificationUserSettingsDto>}\n     * @memberof UserNotificationsDto\n     */\n    'notifications': Array<NotificationUserSettingsDto>;\n    /**\n     * \n     * @type {object}\n     * @memberof UserNotificationsDto\n     */\n    'connections': object;\n}\n/**\n * \n * @export\n * @interface UserSearchDto\n */\nexport interface UserSearchDto {\n    /**\n     * \n     * @type {number}\n     * @memberof UserSearchDto\n     */\n    'id': number;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'githubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'cityName': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'countryName': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'contactsEmail': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'contactsEpamEmail': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'primaryEmail': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'contactsDiscord': string | null;\n    /**\n     * \n     * @type {string}\n     * @memberof UserSearchDto\n     */\n    'contactsTelegram': string | null;\n    /**\n     * \n     * @type {Array<CourseRecord>}\n     * @memberof UserSearchDto\n     */\n    'mentors': Array<CourseRecord> | null;\n    /**\n     * \n     * @type {Array<CourseRecord>}\n     * @memberof UserSearchDto\n     */\n    'students': Array<CourseRecord> | null;\n}\n/**\n * \n * @export\n * @interface UserStudentCourseDto\n */\nexport interface UserStudentCourseDto {\n    /**\n     * \n     * @type {string}\n     * @memberof UserStudentCourseDto\n     */\n    'alias': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UserStudentCourseDto\n     */\n    'name': string;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UserStudentCourseDto\n     */\n    'hasCertificate': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UserStudentCourseDto\n     */\n    'completed': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof UserStudentCourseDto\n     */\n    'studentIsExpelled': boolean;\n    /**\n     * \n     * @type {string}\n     * @memberof UserStudentCourseDto\n     */\n    'certificateId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UserStudentCourseDto\n     */\n    'mentorGithubId': string;\n    /**\n     * \n     * @type {string}\n     * @memberof UserStudentCourseDto\n     */\n    'mentorFullName': string;\n    /**\n     * \n     * @type {number}\n     * @memberof UserStudentCourseDto\n     */\n    'totalScore': number;\n    /**\n     * \n     * @type {number}\n     * @memberof UserStudentCourseDto\n     */\n    'rank': number;\n}\n/**\n * \n * @export\n * @interface UserStudentDto\n */\nexport interface UserStudentDto {\n    /**\n     * User id\n     * @type {number}\n     * @memberof UserStudentDto\n     */\n    'id': number;\n    /**\n     * User github id\n     * @type {string}\n     * @memberof UserStudentDto\n     */\n    'githubId': string;\n    /**\n     * User full name\n     * @type {string}\n     * @memberof UserStudentDto\n     */\n    'fullName': string;\n    /**\n     * User country\n     * @type {object}\n     * @memberof UserStudentDto\n     */\n    'country': object;\n    /**\n     * User city\n     * @type {object}\n     * @memberof UserStudentDto\n     */\n    'city': object;\n    /**\n     * User email\n     * @type {string}\n     * @memberof UserStudentDto\n     */\n    'contactsEmail': string;\n    /**\n     * User telegram\n     * @type {string}\n     * @memberof UserStudentDto\n     */\n    'contactsTelegram': string;\n    /**\n     * User linkedIn\n     * @type {string}\n     * @memberof UserStudentDto\n     */\n    'contactsLinkedIn': string;\n    /**\n     * User skype\n     * @type {string}\n     * @memberof UserStudentDto\n     */\n    'contactsSkype': string;\n    /**\n     * User phone\n     * @type {string}\n     * @memberof UserStudentDto\n     */\n    'contactsPhone': string;\n    /**\n     * User discord\n     * @type {Discord}\n     * @memberof UserStudentDto\n     */\n    'discord': Discord;\n    /**\n     * User on going courses\n     * @type {Array<UserStudentCourseDto>}\n     * @memberof UserStudentDto\n     */\n    'onGoingCourses': Array<UserStudentCourseDto>;\n    /**\n     * User previous courses\n     * @type {Array<UserStudentCourseDto>}\n     * @memberof UserStudentDto\n     */\n    'previousCourses': Array<UserStudentCourseDto>;\n    /**\n     * User languages\n     * @type {Array<string>}\n     * @memberof UserStudentDto\n     */\n    'languages': Array<string>;\n}\n/**\n * \n * @export\n * @interface UserStudentsDto\n */\nexport interface UserStudentsDto {\n    /**\n     * \n     * @type {Array<UserStudentDto>}\n     * @memberof UserStudentsDto\n     */\n    'content': Array<UserStudentDto>;\n    /**\n     * \n     * @type {PaginationMetaDto}\n     * @memberof UserStudentsDto\n     */\n    'pagination': PaginationMetaDto;\n}\n/**\n * \n * @export\n * @interface Validations\n */\nexport interface Validations {\n    /**\n     * \n     * @type {boolean}\n     * @memberof Validations\n     */\n    'githubIdInUrl': boolean;\n    /**\n     * \n     * @type {boolean}\n     * @memberof Validations\n     */\n    'githubPrInUrl': boolean;\n}\n/**\n * \n * @export\n * @interface VisibilityDto\n */\nexport interface VisibilityDto {\n    /**\n     * \n     * @type {boolean}\n     * @memberof VisibilityDto\n     */\n    'isHidden': boolean;\n}\n\n/**\n * ActivityApi - axios parameter creator\n * @export\n */\nexport const ActivityApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateActivityDto} createActivityDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createActivity: async (createActivityDto: CreateActivityDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createActivityDto' is not null or undefined\n            assertParamExists('createActivity', 'createActivityDto', createActivityDto)\n            const localVarPath = `/activity`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createActivityDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {CreateActivityWebhookDto} createActivityWebhookDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createActivityWebhook: async (createActivityWebhookDto: CreateActivityWebhookDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createActivityWebhookDto' is not null or undefined\n            assertParamExists('createActivityWebhook', 'createActivityWebhookDto', createActivityWebhookDto)\n            const localVarPath = `/activity/webhook`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createActivityWebhookDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getActivity: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/activity`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ActivityApi - functional programming interface\n * @export\n */\nexport const ActivityApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ActivityApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateActivityDto} createActivityDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createActivity(createActivityDto: CreateActivityDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createActivity(createActivityDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {CreateActivityWebhookDto} createActivityWebhookDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createActivityWebhook(createActivityWebhookDto: CreateActivityWebhookDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createActivityWebhook(createActivityWebhookDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getActivity(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ActivityDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getActivity(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * ActivityApi - factory interface\n * @export\n */\nexport const ActivityApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ActivityApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateActivityDto} createActivityDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createActivity(createActivityDto: CreateActivityDto, options?: any): AxiosPromise<ActivityDto> {\n            return localVarFp.createActivity(createActivityDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {CreateActivityWebhookDto} createActivityWebhookDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createActivityWebhook(createActivityWebhookDto: CreateActivityWebhookDto, options?: any): AxiosPromise<ActivityDto> {\n            return localVarFp.createActivityWebhook(createActivityWebhookDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getActivity(options?: any): AxiosPromise<ActivityDto> {\n            return localVarFp.getActivity(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * ActivityApi - object-oriented interface\n * @export\n * @class ActivityApi\n * @extends {BaseAPI}\n */\nexport class ActivityApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateActivityDto} createActivityDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ActivityApi\n     */\n    public createActivity(createActivityDto: CreateActivityDto, options?: AxiosRequestConfig) {\n        return ActivityApiFp(this.configuration).createActivity(createActivityDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {CreateActivityWebhookDto} createActivityWebhookDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ActivityApi\n     */\n    public createActivityWebhook(createActivityWebhookDto: CreateActivityWebhookDto, options?: AxiosRequestConfig) {\n        return ActivityApiFp(this.configuration).createActivityWebhook(createActivityWebhookDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ActivityApi\n     */\n    public getActivity(options?: AxiosRequestConfig) {\n        return ActivityApiFp(this.configuration).getActivity(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * AlertsApi - axios parameter creator\n * @export\n */\nexport const AlertsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateAlertDto} createAlertDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAlert: async (createAlertDto: CreateAlertDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createAlertDto' is not null or undefined\n            assertParamExists('createAlert', 'createAlertDto', createAlertDto)\n            const localVarPath = `/alerts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createAlertDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAlert: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteAlert', 'id', id)\n            const localVarPath = `/alerts/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {boolean} enabled \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAlerts: async (enabled: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'enabled' is not null or undefined\n            assertParamExists('getAlerts', 'enabled', enabled)\n            const localVarPath = `/alerts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (enabled !== undefined) {\n                localVarQueryParameter['enabled'] = enabled;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAlert: async (id: number, body: object, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateAlert', 'id', id)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('updateAlert', 'body', body)\n            const localVarPath = `/alerts/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AlertsApi - functional programming interface\n * @export\n */\nexport const AlertsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AlertsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateAlertDto} createAlertDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createAlert(createAlertDto: CreateAlertDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlertDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createAlert(createAlertDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteAlert(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteAlert(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {boolean} enabled \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAlerts(enabled: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AlertDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAlerts(enabled, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateAlert(id: number, body: object, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AlertDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateAlert(id, body, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * AlertsApi - factory interface\n * @export\n */\nexport const AlertsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AlertsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateAlertDto} createAlertDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createAlert(createAlertDto: CreateAlertDto, options?: any): AxiosPromise<AlertDto> {\n            return localVarFp.createAlert(createAlertDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteAlert(id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteAlert(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {boolean} enabled \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAlerts(enabled: boolean, options?: any): AxiosPromise<Array<AlertDto>> {\n            return localVarFp.getAlerts(enabled, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateAlert(id: number, body: object, options?: any): AxiosPromise<AlertDto> {\n            return localVarFp.updateAlert(id, body, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * AlertsApi - object-oriented interface\n * @export\n * @class AlertsApi\n * @extends {BaseAPI}\n */\nexport class AlertsApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateAlertDto} createAlertDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AlertsApi\n     */\n    public createAlert(createAlertDto: CreateAlertDto, options?: AxiosRequestConfig) {\n        return AlertsApiFp(this.configuration).createAlert(createAlertDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AlertsApi\n     */\n    public deleteAlert(id: number, options?: AxiosRequestConfig) {\n        return AlertsApiFp(this.configuration).deleteAlert(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {boolean} enabled \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AlertsApi\n     */\n    public getAlerts(enabled: boolean, options?: AxiosRequestConfig) {\n        return AlertsApiFp(this.configuration).getAlerts(enabled, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {object} body \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AlertsApi\n     */\n    public updateAlert(id: number, body: object, options?: AxiosRequestConfig) {\n        return AlertsApiFp(this.configuration).updateAlert(id, body, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * AuthApi - axios parameter creator\n * @export\n */\nexport const AuthApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {AuthConnectionDto} authConnectionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        authControllerCreateConnectLinkViaGithub: async (authConnectionDto: AuthConnectionDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'authConnectionDto' is not null or undefined\n            assertParamExists('authControllerCreateConnectLinkViaGithub', 'authConnectionDto', authConnectionDto)\n            const localVarPath = `/auth/github/connect`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(authConnectionDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} userId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        clearAuthUserSessionCache: async (userId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'userId' is not null or undefined\n            assertParamExists('clearAuthUserSessionCache', 'userId', userId)\n            const localVarPath = `/auth/cache/{userId}`\n                .replace(`{${\"userId\"}}`, encodeURIComponent(String(userId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        githubCallback: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/auth/github/callback`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        githubLogin: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/auth/github/login`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        githubLogout: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/auth/github/logout`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AuthApi - functional programming interface\n * @export\n */\nexport const AuthApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AuthApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {AuthConnectionDto} authConnectionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async authControllerCreateConnectLinkViaGithub(authConnectionDto: AuthConnectionDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.authControllerCreateConnectLinkViaGithub(authConnectionDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} userId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async clearAuthUserSessionCache(userId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.clearAuthUserSessionCache(userId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async githubCallback(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.githubCallback(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async githubLogin(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.githubLogin(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async githubLogout(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.githubLogout(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * AuthApi - factory interface\n * @export\n */\nexport const AuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AuthApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {AuthConnectionDto} authConnectionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        authControllerCreateConnectLinkViaGithub(authConnectionDto: AuthConnectionDto, options?: any): AxiosPromise<void> {\n            return localVarFp.authControllerCreateConnectLinkViaGithub(authConnectionDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} userId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        clearAuthUserSessionCache(userId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.clearAuthUserSessionCache(userId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        githubCallback(options?: any): AxiosPromise<void> {\n            return localVarFp.githubCallback(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        githubLogin(options?: any): AxiosPromise<void> {\n            return localVarFp.githubLogin(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        githubLogout(options?: any): AxiosPromise<void> {\n            return localVarFp.githubLogout(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * AuthApi - object-oriented interface\n * @export\n * @class AuthApi\n * @extends {BaseAPI}\n */\nexport class AuthApi extends BaseAPI {\n    /**\n     * \n     * @param {AuthConnectionDto} authConnectionDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AuthApi\n     */\n    public authControllerCreateConnectLinkViaGithub(authConnectionDto: AuthConnectionDto, options?: AxiosRequestConfig) {\n        return AuthApiFp(this.configuration).authControllerCreateConnectLinkViaGithub(authConnectionDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} userId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AuthApi\n     */\n    public clearAuthUserSessionCache(userId: number, options?: AxiosRequestConfig) {\n        return AuthApiFp(this.configuration).clearAuthUserSessionCache(userId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AuthApi\n     */\n    public githubCallback(options?: AxiosRequestConfig) {\n        return AuthApiFp(this.configuration).githubCallback(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AuthApi\n     */\n    public githubLogin(options?: AxiosRequestConfig) {\n        return AuthApiFp(this.configuration).githubLogin(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AuthApi\n     */\n    public githubLogout(options?: AxiosRequestConfig) {\n        return AuthApiFp(this.configuration).githubLogout(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * AutoTestsApi - axios parameter creator\n * @export\n */\nexport const AutoTestsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAutoTest: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('getAutoTest', 'id', id)\n            const localVarPath = `/auto-test/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBasicAutoTests: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/auto-test`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * AutoTestsApi - functional programming interface\n * @export\n */\nexport const AutoTestsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = AutoTestsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAutoTest(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AutoTestTaskDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAutoTest(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getBasicAutoTests(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BasicAutoTestTaskDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getBasicAutoTests(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * AutoTestsApi - factory interface\n * @export\n */\nexport const AutoTestsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = AutoTestsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAutoTest(id: number, options?: any): AxiosPromise<AutoTestTaskDto> {\n            return localVarFp.getAutoTest(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBasicAutoTests(options?: any): AxiosPromise<Array<BasicAutoTestTaskDto>> {\n            return localVarFp.getBasicAutoTests(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * AutoTestsApi - object-oriented interface\n * @export\n * @class AutoTestsApi\n * @extends {BaseAPI}\n */\nexport class AutoTestsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AutoTestsApi\n     */\n    public getAutoTest(id: number, options?: AxiosRequestConfig) {\n        return AutoTestsApiFp(this.configuration).getAutoTest(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof AutoTestsApi\n     */\n    public getBasicAutoTests(options?: AxiosRequestConfig) {\n        return AutoTestsApiFp(this.configuration).getBasicAutoTests(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CertificateApi - axios parameter creator\n * @export\n */\nexport const CertificateApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} publicId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCertificate: async (publicId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'publicId' is not null or undefined\n            assertParamExists('getCertificate', 'publicId', publicId)\n            const localVarPath = `/certificate/{publicId}`\n                .replace(`{${\"publicId\"}}`, encodeURIComponent(String(publicId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        removeCertificate: async (studentId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'studentId' is not null or undefined\n            assertParamExists('removeCertificate', 'studentId', studentId)\n            const localVarPath = `/certificate/{studentId}`\n                .replace(`{${\"studentId\"}}`, encodeURIComponent(String(studentId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {SaveCertificateDto} saveCertificateDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveCertificate: async (saveCertificateDto: SaveCertificateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'saveCertificateDto' is not null or undefined\n            assertParamExists('saveCertificate', 'saveCertificateDto', saveCertificateDto)\n            const localVarPath = `/certificate`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(saveCertificateDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CertificateApi - functional programming interface\n * @export\n */\nexport const CertificateApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CertificateApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} publicId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCertificate(publicId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCertificate(publicId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async removeCertificate(studentId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.removeCertificate(studentId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {SaveCertificateDto} saveCertificateDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async saveCertificate(saveCertificateDto: SaveCertificateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.saveCertificate(saveCertificateDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CertificateApi - factory interface\n * @export\n */\nexport const CertificateApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CertificateApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {string} publicId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCertificate(publicId: string, options?: any): AxiosPromise<void> {\n            return localVarFp.getCertificate(publicId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        removeCertificate(studentId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.removeCertificate(studentId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {SaveCertificateDto} saveCertificateDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveCertificate(saveCertificateDto: SaveCertificateDto, options?: any): AxiosPromise<void> {\n            return localVarFp.saveCertificate(saveCertificateDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CertificateApi - object-oriented interface\n * @export\n * @class CertificateApi\n * @extends {BaseAPI}\n */\nexport class CertificateApi extends BaseAPI {\n    /**\n     * \n     * @param {string} publicId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CertificateApi\n     */\n    public getCertificate(publicId: string, options?: AxiosRequestConfig) {\n        return CertificateApiFp(this.configuration).getCertificate(publicId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} studentId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CertificateApi\n     */\n    public removeCertificate(studentId: number, options?: AxiosRequestConfig) {\n        return CertificateApiFp(this.configuration).removeCertificate(studentId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {SaveCertificateDto} saveCertificateDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CertificateApi\n     */\n    public saveCertificate(saveCertificateDto: SaveCertificateDto, options?: AxiosRequestConfig) {\n        return CertificateApiFp(this.configuration).saveCertificate(saveCertificateDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * ContributorsApi - axios parameter creator\n * @export\n */\nexport const ContributorsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateContributorDto} createContributorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createContributor: async (createContributorDto: CreateContributorDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createContributorDto' is not null or undefined\n            assertParamExists('createContributor', 'createContributorDto', createContributorDto)\n            const localVarPath = `/contributors`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createContributorDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteContributor: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteContributor', 'id', id)\n            const localVarPath = `/contributors/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getContributor: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('getContributor', 'id', id)\n            const localVarPath = `/contributors/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getContributors: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/contributors`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateContributorDto} updateContributorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateContributor: async (id: number, updateContributorDto: UpdateContributorDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateContributor', 'id', id)\n            // verify required parameter 'updateContributorDto' is not null or undefined\n            assertParamExists('updateContributor', 'updateContributorDto', updateContributorDto)\n            const localVarPath = `/contributors/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateContributorDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ContributorsApi - functional programming interface\n * @export\n */\nexport const ContributorsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ContributorsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateContributorDto} createContributorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createContributor(createContributorDto: CreateContributorDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContributorDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createContributor(createContributorDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteContributor(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteContributor(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getContributor(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContributorDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getContributor(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getContributors(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ContributorDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getContributors(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateContributorDto} updateContributorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateContributor(id: number, updateContributorDto: UpdateContributorDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ContributorDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateContributor(id, updateContributorDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * ContributorsApi - factory interface\n * @export\n */\nexport const ContributorsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ContributorsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateContributorDto} createContributorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createContributor(createContributorDto: CreateContributorDto, options?: any): AxiosPromise<ContributorDto> {\n            return localVarFp.createContributor(createContributorDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteContributor(id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteContributor(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getContributor(id: number, options?: any): AxiosPromise<ContributorDto> {\n            return localVarFp.getContributor(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getContributors(options?: any): AxiosPromise<Array<ContributorDto>> {\n            return localVarFp.getContributors(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateContributorDto} updateContributorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateContributor(id: number, updateContributorDto: UpdateContributorDto, options?: any): AxiosPromise<ContributorDto> {\n            return localVarFp.updateContributor(id, updateContributorDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * ContributorsApi - object-oriented interface\n * @export\n * @class ContributorsApi\n * @extends {BaseAPI}\n */\nexport class ContributorsApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateContributorDto} createContributorDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ContributorsApi\n     */\n    public createContributor(createContributorDto: CreateContributorDto, options?: AxiosRequestConfig) {\n        return ContributorsApiFp(this.configuration).createContributor(createContributorDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ContributorsApi\n     */\n    public deleteContributor(id: number, options?: AxiosRequestConfig) {\n        return ContributorsApiFp(this.configuration).deleteContributor(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ContributorsApi\n     */\n    public getContributor(id: number, options?: AxiosRequestConfig) {\n        return ContributorsApiFp(this.configuration).getContributor(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ContributorsApi\n     */\n    public getContributors(options?: AxiosRequestConfig) {\n        return ContributorsApiFp(this.configuration).getContributors(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {UpdateContributorDto} updateContributorDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ContributorsApi\n     */\n    public updateContributor(id: number, updateContributorDto: UpdateContributorDto, options?: AxiosRequestConfig) {\n        return ContributorsApiFp(this.configuration).updateContributor(id, updateContributorDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CourseMentorsApi - axios parameter creator\n * @export\n */\nexport const CourseMentorsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorsDetails: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getMentorsDetails', 'courseId', courseId)\n            const localVarPath = `/course/{courseId}/mentors/details`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorsDetailsCsv: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getMentorsDetailsCsv', 'courseId', courseId)\n            const localVarPath = `/course/{courseId}/mentors/details/csv`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} searchText \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        searchMentors: async (courseId: number, searchText: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('searchMentors', 'courseId', courseId)\n            // verify required parameter 'searchText' is not null or undefined\n            assertParamExists('searchMentors', 'searchText', searchText)\n            const localVarPath = `/course/{courseId}/mentors/search/{searchText}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"searchText\"}}`, encodeURIComponent(String(searchText)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CourseMentorsApi - functional programming interface\n * @export\n */\nexport const CourseMentorsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CourseMentorsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMentorsDetails(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MentorDetailsDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorsDetails(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMentorsDetailsCsv(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorsDetailsCsv(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} searchText \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async searchMentors(courseId: number, searchText: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<SearchMentorDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.searchMentors(courseId, searchText, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CourseMentorsApi - factory interface\n * @export\n */\nexport const CourseMentorsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CourseMentorsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorsDetails(courseId: number, options?: any): AxiosPromise<Array<MentorDetailsDto>> {\n            return localVarFp.getMentorsDetails(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorsDetailsCsv(courseId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.getMentorsDetailsCsv(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} searchText \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        searchMentors(courseId: number, searchText: string, options?: any): AxiosPromise<Array<SearchMentorDto>> {\n            return localVarFp.searchMentors(courseId, searchText, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CourseMentorsApi - object-oriented interface\n * @export\n * @class CourseMentorsApi\n * @extends {BaseAPI}\n */\nexport class CourseMentorsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseMentorsApi\n     */\n    public getMentorsDetails(courseId: number, options?: AxiosRequestConfig) {\n        return CourseMentorsApiFp(this.configuration).getMentorsDetails(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseMentorsApi\n     */\n    public getMentorsDetailsCsv(courseId: number, options?: AxiosRequestConfig) {\n        return CourseMentorsApiFp(this.configuration).getMentorsDetailsCsv(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {string} searchText \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseMentorsApi\n     */\n    public searchMentors(courseId: number, searchText: string, options?: AxiosRequestConfig) {\n        return CourseMentorsApiFp(this.configuration).searchMentors(courseId, searchText, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CourseStatsApi - axios parameter creator\n * @export\n */\nexport const CourseStatsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteExpelledStat: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteExpelledStat', 'id', id)\n            const localVarPath = `/courses/stats/expelled/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseExpelledStats: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseExpelledStats', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/stats/expelled`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseMentorCountries: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseMentorCountries', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/stats/mentors/countries`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseMentors: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseMentors', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/stats/mentors`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStats: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseStats', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/stats`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStudentCertificatesCountries: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseStudentCertificatesCountries', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/stats/students/certificates/countries`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStudentCountries: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseStudentCountries', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/stats/students/countries`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {Array<number>} ids List of course IDs\n         * @param {number} year Year for which stats are fetched\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCoursesStats: async (ids: Array<number>, year: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'ids' is not null or undefined\n            assertParamExists('getCoursesStats', 'ids', ids)\n            // verify required parameter 'year' is not null or undefined\n            assertParamExists('getCoursesStats', 'year', year)\n            const localVarPath = `/courses/aggregate/stats`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (ids) {\n                localVarQueryParameter['ids'] = ids;\n            }\n\n            if (year !== undefined) {\n                localVarQueryParameter['year'] = year;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getExpelledStats: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/courses/stats/expelled`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTaskPerformance: async (courseId: number, taskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getTaskPerformance', 'courseId', courseId)\n            // verify required parameter 'taskId' is not null or undefined\n            assertParamExists('getTaskPerformance', 'taskId', taskId)\n            const localVarPath = `/courses/{courseId}/stats/task/{taskId}/performance`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"taskId\"}}`, encodeURIComponent(String(taskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CourseStatsApi - functional programming interface\n * @export\n */\nexport const CourseStatsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CourseStatsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteExpelledStat(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteExpelledStat(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseExpelledStats(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ExpelledStatsDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseExpelledStats(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseMentorCountries(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CountriesStatsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseMentorCountries(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseMentors(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseMentorsStatsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseMentors(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseStats(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseStatsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStats(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseStudentCertificatesCountries(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CountriesStatsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStudentCertificatesCountries(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseStudentCountries(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CountriesStatsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStudentCountries(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {Array<number>} ids List of course IDs\n         * @param {number} year Year for which stats are fetched\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCoursesStats(ids: Array<number>, year: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseAggregateStatsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCoursesStats(ids, year, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getExpelledStats(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ExpelledStatsDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getExpelledStats(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTaskPerformance(courseId: number, taskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskPerformanceStatsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTaskPerformance(courseId, taskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CourseStatsApi - factory interface\n * @export\n */\nexport const CourseStatsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CourseStatsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {string} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteExpelledStat(id: string, options?: any): AxiosPromise<string> {\n            return localVarFp.deleteExpelledStat(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseExpelledStats(courseId: number, options?: any): AxiosPromise<Array<ExpelledStatsDto>> {\n            return localVarFp.getCourseExpelledStats(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseMentorCountries(courseId: number, options?: any): AxiosPromise<CountriesStatsDto> {\n            return localVarFp.getCourseMentorCountries(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseMentors(courseId: number, options?: any): AxiosPromise<CourseMentorsStatsDto> {\n            return localVarFp.getCourseMentors(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStats(courseId: number, options?: any): AxiosPromise<CourseStatsDto> {\n            return localVarFp.getCourseStats(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStudentCertificatesCountries(courseId: number, options?: any): AxiosPromise<CountriesStatsDto> {\n            return localVarFp.getCourseStudentCertificatesCountries(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStudentCountries(courseId: number, options?: any): AxiosPromise<CountriesStatsDto> {\n            return localVarFp.getCourseStudentCountries(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {Array<number>} ids List of course IDs\n         * @param {number} year Year for which stats are fetched\n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCoursesStats(ids: Array<number>, year: number, options?: any): AxiosPromise<CourseAggregateStatsDto> {\n            return localVarFp.getCoursesStats(ids, year, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getExpelledStats(options?: any): AxiosPromise<Array<ExpelledStatsDto>> {\n            return localVarFp.getExpelledStats(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTaskPerformance(courseId: number, taskId: number, options?: any): AxiosPromise<TaskPerformanceStatsDto> {\n            return localVarFp.getTaskPerformance(courseId, taskId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CourseStatsApi - object-oriented interface\n * @export\n * @class CourseStatsApi\n * @extends {BaseAPI}\n */\nexport class CourseStatsApi extends BaseAPI {\n    /**\n     * \n     * @param {string} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public deleteExpelledStat(id: string, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).deleteExpelledStat(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getCourseExpelledStats(courseId: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getCourseExpelledStats(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getCourseMentorCountries(courseId: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getCourseMentorCountries(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getCourseMentors(courseId: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getCourseMentors(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getCourseStats(courseId: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getCourseStats(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getCourseStudentCertificatesCountries(courseId: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getCourseStudentCertificatesCountries(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getCourseStudentCountries(courseId: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getCourseStudentCountries(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {Array<number>} ids List of course IDs\n     * @param {number} year Year for which stats are fetched\n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getCoursesStats(ids: Array<number>, year: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getCoursesStats(ids, year, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getExpelledStats(options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getExpelledStats(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} taskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseStatsApi\n     */\n    public getTaskPerformance(courseId: number, taskId: number, options?: AxiosRequestConfig) {\n        return CourseStatsApiFp(this.configuration).getTaskPerformance(courseId, taskId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CourseTaskVerificationsApi - axios parameter creator\n * @export\n */\nexport const CourseTaskVerificationsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTaskVerification: async (courseId: number, courseTaskId: number, body: object, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('createTaskVerification', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('createTaskVerification', 'courseTaskId', courseTaskId)\n            // verify required parameter 'body' is not null or undefined\n            assertParamExists('createTaskVerification', 'body', body)\n            const localVarPath = `/courses/{courseId}/tasks/{courseTaskId}/verifications`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(body, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAnswers: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getAnswers', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('getAnswers', 'courseTaskId', courseTaskId)\n            const localVarPath = `/courses/{courseId}/tasks/{courseTaskId}/verifications/answers`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CourseTaskVerificationsApi - functional programming interface\n * @export\n */\nexport const CourseTaskVerificationsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CourseTaskVerificationsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTaskVerification(courseId: number, courseTaskId: number, body: object, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CreateTaskVerificationDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTaskVerification(courseId, courseTaskId, body, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAnswers(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TaskVerificationAttemptDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAnswers(courseId, courseTaskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CourseTaskVerificationsApi - factory interface\n * @export\n */\nexport const CourseTaskVerificationsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CourseTaskVerificationsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {object} body \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTaskVerification(courseId: number, courseTaskId: number, body: object, options?: any): AxiosPromise<CreateTaskVerificationDto> {\n            return localVarFp.createTaskVerification(courseId, courseTaskId, body, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAnswers(courseId: number, courseTaskId: number, options?: any): AxiosPromise<Array<TaskVerificationAttemptDto>> {\n            return localVarFp.getAnswers(courseId, courseTaskId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CourseTaskVerificationsApi - object-oriented interface\n * @export\n * @class CourseTaskVerificationsApi\n * @extends {BaseAPI}\n */\nexport class CourseTaskVerificationsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {object} body \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseTaskVerificationsApi\n     */\n    public createTaskVerification(courseId: number, courseTaskId: number, body: object, options?: AxiosRequestConfig) {\n        return CourseTaskVerificationsApiFp(this.configuration).createTaskVerification(courseId, courseTaskId, body, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseTaskVerificationsApi\n     */\n    public getAnswers(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) {\n        return CourseTaskVerificationsApiFp(this.configuration).getAnswers(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CourseUsersApi - axios parameter creator\n * @export\n */\nexport const CourseUsersApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseUsers: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseUsers', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/users`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {CourseRolesDto} courseRolesDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        putCourseUser: async (courseId: number, githubId: string, courseRolesDto: CourseRolesDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('putCourseUser', 'courseId', courseId)\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('putCourseUser', 'githubId', githubId)\n            // verify required parameter 'courseRolesDto' is not null or undefined\n            assertParamExists('putCourseUser', 'courseRolesDto', courseRolesDto)\n            const localVarPath = `/courses/{courseId}/users/{githubId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(courseRolesDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {Array<UpdateCourseUserDto>} updateCourseUserDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        putCourseUsers: async (courseId: number, updateCourseUserDto: Array<UpdateCourseUserDto>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('putCourseUsers', 'courseId', courseId)\n            // verify required parameter 'updateCourseUserDto' is not null or undefined\n            assertParamExists('putCourseUsers', 'updateCourseUserDto', updateCourseUserDto)\n            const localVarPath = `/courses/{courseId}/users`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateCourseUserDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CourseUsersApi - functional programming interface\n * @export\n */\nexport const CourseUsersApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CourseUsersApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseUsers(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CourseUserDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseUsers(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {CourseRolesDto} courseRolesDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async putCourseUser(courseId: number, githubId: string, courseRolesDto: CourseRolesDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.putCourseUser(courseId, githubId, courseRolesDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {Array<UpdateCourseUserDto>} updateCourseUserDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async putCourseUsers(courseId: number, updateCourseUserDto: Array<UpdateCourseUserDto>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.putCourseUsers(courseId, updateCourseUserDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CourseUsersApi - factory interface\n * @export\n */\nexport const CourseUsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CourseUsersApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseUsers(courseId: number, options?: any): AxiosPromise<Array<CourseUserDto>> {\n            return localVarFp.getCourseUsers(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {CourseRolesDto} courseRolesDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        putCourseUser(courseId: number, githubId: string, courseRolesDto: CourseRolesDto, options?: any): AxiosPromise<void> {\n            return localVarFp.putCourseUser(courseId, githubId, courseRolesDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {Array<UpdateCourseUserDto>} updateCourseUserDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        putCourseUsers(courseId: number, updateCourseUserDto: Array<UpdateCourseUserDto>, options?: any): AxiosPromise<void> {\n            return localVarFp.putCourseUsers(courseId, updateCourseUserDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CourseUsersApi - object-oriented interface\n * @export\n * @class CourseUsersApi\n * @extends {BaseAPI}\n */\nexport class CourseUsersApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseUsersApi\n     */\n    public getCourseUsers(courseId: number, options?: AxiosRequestConfig) {\n        return CourseUsersApiFp(this.configuration).getCourseUsers(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {string} githubId \n     * @param {CourseRolesDto} courseRolesDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseUsersApi\n     */\n    public putCourseUser(courseId: number, githubId: string, courseRolesDto: CourseRolesDto, options?: AxiosRequestConfig) {\n        return CourseUsersApiFp(this.configuration).putCourseUser(courseId, githubId, courseRolesDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {Array<UpdateCourseUserDto>} updateCourseUserDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CourseUsersApi\n     */\n    public putCourseUsers(courseId: number, updateCourseUserDto: Array<UpdateCourseUserDto>, options?: AxiosRequestConfig) {\n        return CourseUsersApiFp(this.configuration).putCourseUsers(courseId, updateCourseUserDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CoursesApi - axios parameter creator\n * @export\n */\nexport const CoursesApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseDto} createCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        copyCourse: async (courseId: number, createCourseDto: CreateCourseDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('copyCourse', 'courseId', courseId)\n            // verify required parameter 'createCourseDto' is not null or undefined\n            assertParamExists('copyCourse', 'createCourseDto', createCourseDto)\n            const localVarPath = `/courses/{courseId}/copy`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createCourseDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {CreateCourseDto} createCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCourse: async (createCourseDto: CreateCourseDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createCourseDto' is not null or undefined\n            assertParamExists('createCourse', 'createCourseDto', createCourseDto)\n            const localVarPath = `/courses`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createCourseDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourse: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourse', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourses: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/courses`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {LeaveCourseRequestDto} [leaveCourseRequestDto] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        leaveCourse: async (courseId: number, leaveCourseRequestDto?: LeaveCourseRequestDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('leaveCourse', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/leave`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(leaveCourseRequestDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        rejoinCourse: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('rejoinCourse', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/rejoin`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {UpdateCourseDto} updateCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCourse: async (courseId: number, updateCourseDto: UpdateCourseDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('updateCourse', 'courseId', courseId)\n            // verify required parameter 'updateCourseDto' is not null or undefined\n            assertParamExists('updateCourse', 'updateCourseDto', updateCourseDto)\n            const localVarPath = `/courses/{courseId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateCourseDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CoursesApi - functional programming interface\n * @export\n */\nexport const CoursesApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CoursesApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseDto} createCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async copyCourse(courseId: number, createCourseDto: CreateCourseDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.copyCourse(courseId, createCourseDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {CreateCourseDto} createCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createCourse(createCourseDto: CreateCourseDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createCourse(createCourseDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourse(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourse(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourses(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CourseDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourses(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {LeaveCourseRequestDto} [leaveCourseRequestDto] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async leaveCourse(courseId: number, leaveCourseRequestDto?: LeaveCourseRequestDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.leaveCourse(courseId, leaveCourseRequestDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async rejoinCourse(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.rejoinCourse(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {UpdateCourseDto} updateCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateCourse(courseId: number, updateCourseDto: UpdateCourseDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateCourse(courseId, updateCourseDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CoursesApi - factory interface\n * @export\n */\nexport const CoursesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CoursesApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseDto} createCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        copyCourse(courseId: number, createCourseDto: CreateCourseDto, options?: any): AxiosPromise<CourseDto> {\n            return localVarFp.copyCourse(courseId, createCourseDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {CreateCourseDto} createCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCourse(createCourseDto: CreateCourseDto, options?: any): AxiosPromise<CourseDto> {\n            return localVarFp.createCourse(createCourseDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourse(courseId: number, options?: any): AxiosPromise<CourseDto> {\n            return localVarFp.getCourse(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourses(options?: any): AxiosPromise<Array<CourseDto>> {\n            return localVarFp.getCourses(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {LeaveCourseRequestDto} [leaveCourseRequestDto] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        leaveCourse(courseId: number, leaveCourseRequestDto?: LeaveCourseRequestDto, options?: any): AxiosPromise<void> {\n            return localVarFp.leaveCourse(courseId, leaveCourseRequestDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        rejoinCourse(courseId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.rejoinCourse(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {UpdateCourseDto} updateCourseDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCourse(courseId: number, updateCourseDto: UpdateCourseDto, options?: any): AxiosPromise<CourseDto> {\n            return localVarFp.updateCourse(courseId, updateCourseDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CoursesApi - object-oriented interface\n * @export\n * @class CoursesApi\n * @extends {BaseAPI}\n */\nexport class CoursesApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {CreateCourseDto} createCourseDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesApi\n     */\n    public copyCourse(courseId: number, createCourseDto: CreateCourseDto, options?: AxiosRequestConfig) {\n        return CoursesApiFp(this.configuration).copyCourse(courseId, createCourseDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {CreateCourseDto} createCourseDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesApi\n     */\n    public createCourse(createCourseDto: CreateCourseDto, options?: AxiosRequestConfig) {\n        return CoursesApiFp(this.configuration).createCourse(createCourseDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesApi\n     */\n    public getCourse(courseId: number, options?: AxiosRequestConfig) {\n        return CoursesApiFp(this.configuration).getCourse(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesApi\n     */\n    public getCourses(options?: AxiosRequestConfig) {\n        return CoursesApiFp(this.configuration).getCourses(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {LeaveCourseRequestDto} [leaveCourseRequestDto] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesApi\n     */\n    public leaveCourse(courseId: number, leaveCourseRequestDto?: LeaveCourseRequestDto, options?: AxiosRequestConfig) {\n        return CoursesApiFp(this.configuration).leaveCourse(courseId, leaveCourseRequestDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesApi\n     */\n    public rejoinCourse(courseId: number, options?: AxiosRequestConfig) {\n        return CoursesApiFp(this.configuration).rejoinCourse(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {UpdateCourseDto} updateCourseDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesApi\n     */\n    public updateCourse(courseId: number, updateCourseDto: UpdateCourseDto, options?: AxiosRequestConfig) {\n        return CoursesApiFp(this.configuration).updateCourse(courseId, updateCourseDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CoursesEventsApi - axios parameter creator\n * @export\n */\nexport const CoursesEventsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseEventDto} createCourseEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCourseEvent: async (courseId: number, createCourseEventDto: CreateCourseEventDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('createCourseEvent', 'courseId', courseId)\n            // verify required parameter 'createCourseEventDto' is not null or undefined\n            assertParamExists('createCourseEvent', 'createCourseEventDto', createCourseEventDto)\n            const localVarPath = `/courses/{courseId}/events`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createCourseEventDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseEventId \n         * @param {any} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCourseEvent: async (courseEventId: number, courseId: any, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseEventId' is not null or undefined\n            assertParamExists('deleteCourseEvent', 'courseEventId', courseEventId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('deleteCourseEvent', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/events/{courseEventId}`\n                .replace(`{${\"courseEventId\"}}`, encodeURIComponent(String(courseEventId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseEventId \n         * @param {UpdateCourseEventDto} updateCourseEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCourseEvent: async (courseId: number, courseEventId: number, updateCourseEventDto: UpdateCourseEventDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('updateCourseEvent', 'courseId', courseId)\n            // verify required parameter 'courseEventId' is not null or undefined\n            assertParamExists('updateCourseEvent', 'courseEventId', courseEventId)\n            // verify required parameter 'updateCourseEventDto' is not null or undefined\n            assertParamExists('updateCourseEvent', 'updateCourseEventDto', updateCourseEventDto)\n            const localVarPath = `/courses/{courseId}/events/{courseEventId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseEventId\"}}`, encodeURIComponent(String(courseEventId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateCourseEventDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CoursesEventsApi - functional programming interface\n * @export\n */\nexport const CoursesEventsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CoursesEventsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseEventDto} createCourseEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createCourseEvent(courseId: number, createCourseEventDto: CreateCourseEventDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CourseEventDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createCourseEvent(courseId, createCourseEventDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseEventId \n         * @param {any} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteCourseEvent(courseEventId: number, courseId: any, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteCourseEvent(courseEventId, courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseEventId \n         * @param {UpdateCourseEventDto} updateCourseEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateCourseEvent(courseId: number, courseEventId: number, updateCourseEventDto: UpdateCourseEventDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateCourseEvent(courseId, courseEventId, updateCourseEventDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CoursesEventsApi - factory interface\n * @export\n */\nexport const CoursesEventsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CoursesEventsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseEventDto} createCourseEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCourseEvent(courseId: number, createCourseEventDto: CreateCourseEventDto, options?: any): AxiosPromise<Array<CourseEventDto>> {\n            return localVarFp.createCourseEvent(courseId, createCourseEventDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseEventId \n         * @param {any} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCourseEvent(courseEventId: number, courseId: any, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteCourseEvent(courseEventId, courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseEventId \n         * @param {UpdateCourseEventDto} updateCourseEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCourseEvent(courseId: number, courseEventId: number, updateCourseEventDto: UpdateCourseEventDto, options?: any): AxiosPromise<void> {\n            return localVarFp.updateCourseEvent(courseId, courseEventId, updateCourseEventDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CoursesEventsApi - object-oriented interface\n * @export\n * @class CoursesEventsApi\n * @extends {BaseAPI}\n */\nexport class CoursesEventsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {CreateCourseEventDto} createCourseEventDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesEventsApi\n     */\n    public createCourseEvent(courseId: number, createCourseEventDto: CreateCourseEventDto, options?: AxiosRequestConfig) {\n        return CoursesEventsApiFp(this.configuration).createCourseEvent(courseId, createCourseEventDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseEventId \n     * @param {any} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesEventsApi\n     */\n    public deleteCourseEvent(courseEventId: number, courseId: any, options?: AxiosRequestConfig) {\n        return CoursesEventsApiFp(this.configuration).deleteCourseEvent(courseEventId, courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseEventId \n     * @param {UpdateCourseEventDto} updateCourseEventDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesEventsApi\n     */\n    public updateCourseEvent(courseId: number, courseEventId: number, updateCourseEventDto: UpdateCourseEventDto, options?: AxiosRequestConfig) {\n        return CoursesEventsApiFp(this.configuration).updateCourseEvent(courseId, courseEventId, updateCourseEventDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CoursesInterviewsApi - axios parameter creator\n * @export\n */\nexport const CoursesInterviewsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {string} type \n         * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createInterviewFeedback: async (courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('createInterviewFeedback', 'courseId', courseId)\n            // verify required parameter 'interviewId' is not null or undefined\n            assertParamExists('createInterviewFeedback', 'interviewId', interviewId)\n            // verify required parameter 'type' is not null or undefined\n            assertParamExists('createInterviewFeedback', 'type', type)\n            // verify required parameter 'putInterviewFeedbackDto' is not null or undefined\n            assertParamExists('createInterviewFeedback', 'putInterviewFeedbackDto', putInterviewFeedbackDto)\n            const localVarPath = `/courses/{courseId}/interviews/{interviewId}/{type}/feedback`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"interviewId\"}}`, encodeURIComponent(String(interviewId)))\n                .replace(`{${\"type\"}}`, encodeURIComponent(String(type)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(putInterviewFeedbackDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {InterviewDistributeDto} interviewDistributeDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        distributeInterviewPairs: async (courseId: number, courseTaskId: number, interviewDistributeDto: InterviewDistributeDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('distributeInterviewPairs', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('distributeInterviewPairs', 'courseTaskId', courseTaskId)\n            // verify required parameter 'interviewDistributeDto' is not null or undefined\n            assertParamExists('distributeInterviewPairs', 'interviewDistributeDto', interviewDistributeDto)\n            const localVarPath = `/courses/{courseId}/interviews/{courseTaskId}/auto-distribute`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(interviewDistributeDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAvailableStudents: async (courseId: number, interviewId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getAvailableStudents', 'courseId', courseId)\n            // verify required parameter 'interviewId' is not null or undefined\n            assertParamExists('getAvailableStudents', 'interviewId', interviewId)\n            const localVarPath = `/courses/{courseId}/interviews/{interviewId}/students/available`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"interviewId\"}}`, encodeURIComponent(String(interviewId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} interviewId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterview: async (interviewId: number, courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'interviewId' is not null or undefined\n            assertParamExists('getInterview', 'interviewId', interviewId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getInterview', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/interviews/{interviewId}`\n                .replace(`{${\"interviewId\"}}`, encodeURIComponent(String(interviewId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {string} type \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterviewFeedback: async (courseId: number, interviewId: number, type: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getInterviewFeedback', 'courseId', courseId)\n            // verify required parameter 'interviewId' is not null or undefined\n            assertParamExists('getInterviewFeedback', 'interviewId', interviewId)\n            // verify required parameter 'type' is not null or undefined\n            assertParamExists('getInterviewFeedback', 'type', type)\n            const localVarPath = `/courses/{courseId}/interviews/{interviewId}/{type}/feedback`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"interviewId\"}}`, encodeURIComponent(String(interviewId)))\n                .replace(`{${\"type\"}}`, encodeURIComponent(String(type)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} interviewId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterviewPairs: async (interviewId: number, courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'interviewId' is not null or undefined\n            assertParamExists('getInterviewPairs', 'interviewId', interviewId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getInterviewPairs', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/interviews/{interviewId}/pairs`\n                .replace(`{${\"interviewId\"}}`, encodeURIComponent(String(interviewId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {boolean} [disabled] \n         * @param {Array<string>} [types] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterviews: async (courseId: number, disabled?: boolean, types?: Array<string>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getInterviews', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/interviews`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (disabled !== undefined) {\n                localVarQueryParameter['disabled'] = disabled;\n            }\n\n            if (types) {\n                localVarQueryParameter['types'] = types;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStageInterviewsCommentToStudent: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getStageInterviewsCommentToStudent', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/interviews/comments`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        registerToInterview: async (courseId: number, interviewId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('registerToInterview', 'courseId', courseId)\n            // verify required parameter 'interviewId' is not null or undefined\n            assertParamExists('registerToInterview', 'interviewId', interviewId)\n            const localVarPath = `/courses/{courseId}/interviews/{interviewId}/register`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"interviewId\"}}`, encodeURIComponent(String(interviewId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CoursesInterviewsApi - functional programming interface\n * @export\n */\nexport const CoursesInterviewsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CoursesInterviewsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {string} type \n         * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createInterviewFeedback(courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createInterviewFeedback(courseId, interviewId, type, putInterviewFeedbackDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {InterviewDistributeDto} interviewDistributeDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async distributeInterviewPairs(courseId: number, courseTaskId: number, interviewDistributeDto: InterviewDistributeDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<InterviewDistributeResponseDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.distributeInterviewPairs(courseId, courseTaskId, interviewDistributeDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAvailableStudents(courseId: number, interviewId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AvailableStudentDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAvailableStudents(courseId, interviewId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} interviewId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getInterview(interviewId: number, courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InterviewDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getInterview(interviewId, courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {string} type \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getInterviewFeedback(courseId: number, interviewId: number, type: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InterviewFeedbackDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getInterviewFeedback(courseId, interviewId, type, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} interviewId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getInterviewPairs(interviewId: number, courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<InterviewPairDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getInterviewPairs(interviewId, courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {boolean} [disabled] \n         * @param {Array<string>} [types] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getInterviews(courseId: number, disabled?: boolean, types?: Array<string>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<InterviewDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getInterviews(courseId, disabled, types, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getStageInterviewsCommentToStudent(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<InterviewCommentDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getStageInterviewsCommentToStudent(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async registerToInterview(courseId: number, interviewId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.registerToInterview(courseId, interviewId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CoursesInterviewsApi - factory interface\n * @export\n */\nexport const CoursesInterviewsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CoursesInterviewsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {string} type \n         * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createInterviewFeedback(courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options?: any): AxiosPromise<void> {\n            return localVarFp.createInterviewFeedback(courseId, interviewId, type, putInterviewFeedbackDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {InterviewDistributeDto} interviewDistributeDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        distributeInterviewPairs(courseId: number, courseTaskId: number, interviewDistributeDto: InterviewDistributeDto, options?: any): AxiosPromise<Array<InterviewDistributeResponseDto>> {\n            return localVarFp.distributeInterviewPairs(courseId, courseTaskId, interviewDistributeDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAvailableStudents(courseId: number, interviewId: number, options?: any): AxiosPromise<Array<AvailableStudentDto>> {\n            return localVarFp.getAvailableStudents(courseId, interviewId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} interviewId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterview(interviewId: number, courseId: number, options?: any): AxiosPromise<InterviewDto> {\n            return localVarFp.getInterview(interviewId, courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {string} type \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterviewFeedback(courseId: number, interviewId: number, type: string, options?: any): AxiosPromise<InterviewFeedbackDto> {\n            return localVarFp.getInterviewFeedback(courseId, interviewId, type, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} interviewId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterviewPairs(interviewId: number, courseId: number, options?: any): AxiosPromise<Array<InterviewPairDto>> {\n            return localVarFp.getInterviewPairs(interviewId, courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {boolean} [disabled] \n         * @param {Array<string>} [types] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInterviews(courseId: number, disabled?: boolean, types?: Array<string>, options?: any): AxiosPromise<Array<InterviewDto>> {\n            return localVarFp.getInterviews(courseId, disabled, types, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStageInterviewsCommentToStudent(courseId: number, options?: any): AxiosPromise<Array<InterviewCommentDto>> {\n            return localVarFp.getStageInterviewsCommentToStudent(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} interviewId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        registerToInterview(courseId: number, interviewId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.registerToInterview(courseId, interviewId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CoursesInterviewsApi - object-oriented interface\n * @export\n * @class CoursesInterviewsApi\n * @extends {BaseAPI}\n */\nexport class CoursesInterviewsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} interviewId \n     * @param {string} type \n     * @param {PutInterviewFeedbackDto} putInterviewFeedbackDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public createInterviewFeedback(courseId: number, interviewId: number, type: string, putInterviewFeedbackDto: PutInterviewFeedbackDto, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).createInterviewFeedback(courseId, interviewId, type, putInterviewFeedbackDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {InterviewDistributeDto} interviewDistributeDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public distributeInterviewPairs(courseId: number, courseTaskId: number, interviewDistributeDto: InterviewDistributeDto, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).distributeInterviewPairs(courseId, courseTaskId, interviewDistributeDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} interviewId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public getAvailableStudents(courseId: number, interviewId: number, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).getAvailableStudents(courseId, interviewId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} interviewId \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public getInterview(interviewId: number, courseId: number, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).getInterview(interviewId, courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} interviewId \n     * @param {string} type \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public getInterviewFeedback(courseId: number, interviewId: number, type: string, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).getInterviewFeedback(courseId, interviewId, type, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} interviewId \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public getInterviewPairs(interviewId: number, courseId: number, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).getInterviewPairs(interviewId, courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {boolean} [disabled] \n     * @param {Array<string>} [types] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public getInterviews(courseId: number, disabled?: boolean, types?: Array<string>, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).getInterviews(courseId, disabled, types, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public getStageInterviewsCommentToStudent(courseId: number, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).getStageInterviewsCommentToStudent(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} interviewId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesInterviewsApi\n     */\n    public registerToInterview(courseId: number, interviewId: number, options?: AxiosRequestConfig) {\n        return CoursesInterviewsApiFp(this.configuration).registerToInterview(courseId, interviewId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CoursesScheduleApi - axios parameter creator\n * @export\n */\nexport const CoursesScheduleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CourseCopyFromDto} courseCopyFromDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        copySchedule: async (courseId: number, courseCopyFromDto: CourseCopyFromDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('copySchedule', 'courseId', courseId)\n            // verify required parameter 'courseCopyFromDto' is not null or undefined\n            assertParamExists('copySchedule', 'courseCopyFromDto', courseCopyFromDto)\n            const localVarPath = `/courses/{courseId}/schedule/copy`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(courseCopyFromDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSchedule: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getSchedule', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/schedule`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CoursesScheduleApi - functional programming interface\n * @export\n */\nexport const CoursesScheduleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CoursesScheduleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CourseCopyFromDto} courseCopyFromDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async copySchedule(courseId: number, courseCopyFromDto: CourseCopyFromDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.copySchedule(courseId, courseCopyFromDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSchedule(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CourseScheduleItemDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSchedule(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CoursesScheduleApi - factory interface\n * @export\n */\nexport const CoursesScheduleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CoursesScheduleApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CourseCopyFromDto} courseCopyFromDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        copySchedule(courseId: number, courseCopyFromDto: CourseCopyFromDto, options?: any): AxiosPromise<void> {\n            return localVarFp.copySchedule(courseId, courseCopyFromDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSchedule(courseId: number, options?: any): AxiosPromise<Array<CourseScheduleItemDto>> {\n            return localVarFp.getSchedule(courseId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CoursesScheduleApi - object-oriented interface\n * @export\n * @class CoursesScheduleApi\n * @extends {BaseAPI}\n */\nexport class CoursesScheduleApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {CourseCopyFromDto} courseCopyFromDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesScheduleApi\n     */\n    public copySchedule(courseId: number, courseCopyFromDto: CourseCopyFromDto, options?: AxiosRequestConfig) {\n        return CoursesScheduleApiFp(this.configuration).copySchedule(courseId, courseCopyFromDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesScheduleApi\n     */\n    public getSchedule(courseId: number, options?: AxiosRequestConfig) {\n        return CoursesScheduleApiFp(this.configuration).getSchedule(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CoursesScheduleIcalApi - axios parameter creator\n * @export\n */\nexport const CoursesScheduleIcalApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} token \n         * @param {string} timezone \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getScheduleICalendar: async (courseId: number, token: string, timezone: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getScheduleICalendar', 'courseId', courseId)\n            // verify required parameter 'token' is not null or undefined\n            assertParamExists('getScheduleICalendar', 'token', token)\n            // verify required parameter 'timezone' is not null or undefined\n            assertParamExists('getScheduleICalendar', 'timezone', timezone)\n            const localVarPath = `/courses/{courseId}/icalendar/{token}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"token\"}}`, encodeURIComponent(String(token)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (timezone !== undefined) {\n                localVarQueryParameter['timezone'] = timezone;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getScheduleICalendarToken: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getScheduleICalendarToken', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/icalendar/token`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CoursesScheduleIcalApi - functional programming interface\n * @export\n */\nexport const CoursesScheduleIcalApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CoursesScheduleIcalApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} token \n         * @param {string} timezone \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getScheduleICalendar(courseId: number, token: string, timezone: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getScheduleICalendar(courseId, token, timezone, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getScheduleICalendarToken(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseScheduleTokenDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getScheduleICalendarToken(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CoursesScheduleIcalApi - factory interface\n * @export\n */\nexport const CoursesScheduleIcalApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CoursesScheduleIcalApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} token \n         * @param {string} timezone \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getScheduleICalendar(courseId: number, token: string, timezone: string, options?: any): AxiosPromise<string> {\n            return localVarFp.getScheduleICalendar(courseId, token, timezone, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getScheduleICalendarToken(courseId: number, options?: any): AxiosPromise<CourseScheduleTokenDto> {\n            return localVarFp.getScheduleICalendarToken(courseId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CoursesScheduleIcalApi - object-oriented interface\n * @export\n * @class CoursesScheduleIcalApi\n * @extends {BaseAPI}\n */\nexport class CoursesScheduleIcalApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {string} token \n     * @param {string} timezone \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesScheduleIcalApi\n     */\n    public getScheduleICalendar(courseId: number, token: string, timezone: string, options?: AxiosRequestConfig) {\n        return CoursesScheduleIcalApiFp(this.configuration).getScheduleICalendar(courseId, token, timezone, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesScheduleIcalApi\n     */\n    public getScheduleICalendarToken(courseId: number, options?: AxiosRequestConfig) {\n        return CoursesScheduleIcalApiFp(this.configuration).getScheduleICalendarToken(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CoursesTaskSolutionsApi - axios parameter creator\n * @export\n */\nexport const CoursesTaskSolutionsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {SaveTaskSolutionDto} saveTaskSolutionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTaskSolution: async (courseId: number, courseTaskId: number, saveTaskSolutionDto: SaveTaskSolutionDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('createTaskSolution', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('createTaskSolution', 'courseTaskId', courseTaskId)\n            // verify required parameter 'saveTaskSolutionDto' is not null or undefined\n            assertParamExists('createTaskSolution', 'saveTaskSolutionDto', saveTaskSolutionDto)\n            const localVarPath = `/courses/{courseId}/tasks/{courseTaskId}/solutions`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(saveTaskSolutionDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CoursesTaskSolutionsApi - functional programming interface\n * @export\n */\nexport const CoursesTaskSolutionsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CoursesTaskSolutionsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {SaveTaskSolutionDto} saveTaskSolutionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTaskSolution(courseId: number, courseTaskId: number, saveTaskSolutionDto: SaveTaskSolutionDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskSolutionDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTaskSolution(courseId, courseTaskId, saveTaskSolutionDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CoursesTaskSolutionsApi - factory interface\n * @export\n */\nexport const CoursesTaskSolutionsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CoursesTaskSolutionsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {SaveTaskSolutionDto} saveTaskSolutionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTaskSolution(courseId: number, courseTaskId: number, saveTaskSolutionDto: SaveTaskSolutionDto, options?: any): AxiosPromise<TaskSolutionDto> {\n            return localVarFp.createTaskSolution(courseId, courseTaskId, saveTaskSolutionDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CoursesTaskSolutionsApi - object-oriented interface\n * @export\n * @class CoursesTaskSolutionsApi\n * @extends {BaseAPI}\n */\nexport class CoursesTaskSolutionsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {SaveTaskSolutionDto} saveTaskSolutionDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTaskSolutionsApi\n     */\n    public createTaskSolution(courseId: number, courseTaskId: number, saveTaskSolutionDto: SaveTaskSolutionDto, options?: AxiosRequestConfig) {\n        return CoursesTaskSolutionsApiFp(this.configuration).createTaskSolution(courseId, courseTaskId, saveTaskSolutionDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * CoursesTasksApi - axios parameter creator\n * @export\n */\nexport const CoursesTasksApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseTaskDto} createCourseTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCourseTask: async (courseId: number, createCourseTaskDto: CreateCourseTaskDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('createCourseTask', 'courseId', courseId)\n            // verify required parameter 'createCourseTaskDto' is not null or undefined\n            assertParamExists('createCourseTask', 'createCourseTaskDto', createCourseTaskDto)\n            const localVarPath = `/courses/{courseId}/tasks`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createCourseTaskDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCourseTask: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('deleteCourseTask', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('deleteCourseTask', 'courseTaskId', courseTaskId)\n            const localVarPath = `/courses/{courseId}/tasks/{courseTaskId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAvailableCrossCheckReviewStats: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getAvailableCrossCheckReviewStats', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/cross-checks/available-review-stats`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTask: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseTask', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('getCourseTask', 'courseTaskId', courseTaskId)\n            const localVarPath = `/courses/{courseId}/tasks/{courseTaskId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {'started' | 'inprogress' | 'finished'} [status] \n         * @param {'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck'} [checker] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTasks: async (courseId: number, status?: 'started' | 'inprogress' | 'finished', checker?: 'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck', options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseTasks', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/tasks`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (status !== undefined) {\n                localVarQueryParameter['status'] = status;\n            }\n\n            if (checker !== undefined) {\n                localVarQueryParameter['checker'] = checker;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTasksDetailed: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseTasksDetailed', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/tasks/detailed`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {'started' | 'inprogress' | 'finished'} [status] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTasksWithStudentSolution: async (courseId: number, status?: 'started' | 'inprogress' | 'finished', options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseTasksWithStudentSolution', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/tasks/solutions`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (status !== undefined) {\n                localVarQueryParameter['status'] = status;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCrossCheckCsv: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCrossCheckCsv', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('getCrossCheckCsv', 'courseTaskId', courseTaskId)\n            const localVarPath = `/courses/{courseId}/cross-checks/{courseTaskId}/csv`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} [orderBy] \n         * @param {string} [orderDirection] \n         * @param {string} [checker] \n         * @param {string} [student] \n         * @param {string} [url] \n         * @param {string} [task] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCrossCheckPairs: async (courseId: number, pageSize: number, current: number, orderBy?: string, orderDirection?: string, checker?: string, student?: string, url?: string, task?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCrossCheckPairs', 'courseId', courseId)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getCrossCheckPairs', 'pageSize', pageSize)\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getCrossCheckPairs', 'current', current)\n            const localVarPath = `/courses/{courseId}/cross-checks/pairs`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (orderBy !== undefined) {\n                localVarQueryParameter['orderBy'] = orderBy;\n            }\n\n            if (orderDirection !== undefined) {\n                localVarQueryParameter['orderDirection'] = orderDirection;\n            }\n\n            if (checker !== undefined) {\n                localVarQueryParameter['checker'] = checker;\n            }\n\n            if (student !== undefined) {\n                localVarQueryParameter['student'] = student;\n            }\n\n            if (url !== undefined) {\n                localVarQueryParameter['url'] = url;\n            }\n\n            if (task !== undefined) {\n                localVarQueryParameter['task'] = task;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyCrossCheckFeedbacks: async (courseId: number, courseTaskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getMyCrossCheckFeedbacks', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('getMyCrossCheckFeedbacks', 'courseTaskId', courseTaskId)\n            const localVarPath = `/courses/{courseId}/cross-checks/{courseTaskId}/feedbacks/my`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {CheckTasksDeadlineDto} checkTasksDeadlineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        notifyTasksDeadlines: async (checkTasksDeadlineDto: CheckTasksDeadlineDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'checkTasksDeadlineDto' is not null or undefined\n            assertParamExists('notifyTasksDeadlines', 'checkTasksDeadlineDto', checkTasksDeadlineDto)\n            const localVarPath = `/tasks/notify/changes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(checkTasksDeadlineDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {UpdateCourseTaskDto} updateCourseTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCourseTask: async (courseId: number, courseTaskId: number, updateCourseTaskDto: UpdateCourseTaskDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('updateCourseTask', 'courseId', courseId)\n            // verify required parameter 'courseTaskId' is not null or undefined\n            assertParamExists('updateCourseTask', 'courseTaskId', courseTaskId)\n            // verify required parameter 'updateCourseTaskDto' is not null or undefined\n            assertParamExists('updateCourseTask', 'updateCourseTaskDto', updateCourseTaskDto)\n            const localVarPath = `/courses/{courseId}/tasks/{courseTaskId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"courseTaskId\"}}`, encodeURIComponent(String(courseTaskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateCourseTaskDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * CoursesTasksApi - functional programming interface\n * @export\n */\nexport const CoursesTasksApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = CoursesTasksApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseTaskDto} createCourseTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createCourseTask(courseId: number, createCourseTaskDto: CreateCourseTaskDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseTaskDetailedDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createCourseTask(courseId, createCourseTaskDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteCourseTask(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteCourseTask(courseId, courseTaskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getAvailableCrossCheckReviewStats(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AvailableReviewStatsDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getAvailableCrossCheckReviewStats(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseTask(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseTaskDetailedDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseTask(courseId, courseTaskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {'started' | 'inprogress' | 'finished'} [status] \n         * @param {'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck'} [checker] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseTasks(courseId: number, status?: 'started' | 'inprogress' | 'finished', checker?: 'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CourseTaskDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseTasks(courseId, status, checker, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseTasksDetailed(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CourseTaskDetailedDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseTasksDetailed(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {'started' | 'inprogress' | 'finished'} [status] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseTasksWithStudentSolution(courseId: number, status?: 'started' | 'inprogress' | 'finished', options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CourseTaskDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseTasksWithStudentSolution(courseId, status, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCrossCheckCsv(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCrossCheckCsv(courseId, courseTaskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} [orderBy] \n         * @param {string} [orderDirection] \n         * @param {string} [checker] \n         * @param {string} [student] \n         * @param {string} [url] \n         * @param {string} [task] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCrossCheckPairs(courseId: number, pageSize: number, current: number, orderBy?: string, orderDirection?: string, checker?: string, student?: string, url?: string, task?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CrossCheckPairResponseDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCrossCheckPairs(courseId, pageSize, current, orderBy, orderDirection, checker, student, url, task, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMyCrossCheckFeedbacks(courseId: number, courseTaskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CrossCheckFeedbackDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMyCrossCheckFeedbacks(courseId, courseTaskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {CheckTasksDeadlineDto} checkTasksDeadlineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async notifyTasksDeadlines(checkTasksDeadlineDto: CheckTasksDeadlineDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.notifyTasksDeadlines(checkTasksDeadlineDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {UpdateCourseTaskDto} updateCourseTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateCourseTask(courseId: number, courseTaskId: number, updateCourseTaskDto: UpdateCourseTaskDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<CourseTaskDetailedDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateCourseTask(courseId, courseTaskId, updateCourseTaskDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * CoursesTasksApi - factory interface\n * @export\n */\nexport const CoursesTasksApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = CoursesTasksApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateCourseTaskDto} createCourseTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createCourseTask(courseId: number, createCourseTaskDto: CreateCourseTaskDto, options?: any): AxiosPromise<CourseTaskDetailedDto> {\n            return localVarFp.createCourseTask(courseId, createCourseTaskDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteCourseTask(courseId: number, courseTaskId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteCourseTask(courseId, courseTaskId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getAvailableCrossCheckReviewStats(courseId: number, options?: any): AxiosPromise<Array<AvailableReviewStatsDto>> {\n            return localVarFp.getAvailableCrossCheckReviewStats(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTask(courseId: number, courseTaskId: number, options?: any): AxiosPromise<CourseTaskDetailedDto> {\n            return localVarFp.getCourseTask(courseId, courseTaskId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {'started' | 'inprogress' | 'finished'} [status] \n         * @param {'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck'} [checker] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTasks(courseId: number, status?: 'started' | 'inprogress' | 'finished', checker?: 'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck', options?: any): AxiosPromise<Array<CourseTaskDto>> {\n            return localVarFp.getCourseTasks(courseId, status, checker, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTasksDetailed(courseId: number, options?: any): AxiosPromise<Array<CourseTaskDetailedDto>> {\n            return localVarFp.getCourseTasksDetailed(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {'started' | 'inprogress' | 'finished'} [status] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTasksWithStudentSolution(courseId: number, status?: 'started' | 'inprogress' | 'finished', options?: any): AxiosPromise<Array<CourseTaskDto>> {\n            return localVarFp.getCourseTasksWithStudentSolution(courseId, status, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCrossCheckCsv(courseId: number, courseTaskId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.getCrossCheckCsv(courseId, courseTaskId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} [orderBy] \n         * @param {string} [orderDirection] \n         * @param {string} [checker] \n         * @param {string} [student] \n         * @param {string} [url] \n         * @param {string} [task] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCrossCheckPairs(courseId: number, pageSize: number, current: number, orderBy?: string, orderDirection?: string, checker?: string, student?: string, url?: string, task?: string, options?: any): AxiosPromise<CrossCheckPairResponseDto> {\n            return localVarFp.getCrossCheckPairs(courseId, pageSize, current, orderBy, orderDirection, checker, student, url, task, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMyCrossCheckFeedbacks(courseId: number, courseTaskId: number, options?: any): AxiosPromise<CrossCheckFeedbackDto> {\n            return localVarFp.getMyCrossCheckFeedbacks(courseId, courseTaskId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {CheckTasksDeadlineDto} checkTasksDeadlineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        notifyTasksDeadlines(checkTasksDeadlineDto: CheckTasksDeadlineDto, options?: any): AxiosPromise<void> {\n            return localVarFp.notifyTasksDeadlines(checkTasksDeadlineDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} courseTaskId \n         * @param {UpdateCourseTaskDto} updateCourseTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateCourseTask(courseId: number, courseTaskId: number, updateCourseTaskDto: UpdateCourseTaskDto, options?: any): AxiosPromise<CourseTaskDetailedDto> {\n            return localVarFp.updateCourseTask(courseId, courseTaskId, updateCourseTaskDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * CoursesTasksApi - object-oriented interface\n * @export\n * @class CoursesTasksApi\n * @extends {BaseAPI}\n */\nexport class CoursesTasksApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {CreateCourseTaskDto} createCourseTaskDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public createCourseTask(courseId: number, createCourseTaskDto: CreateCourseTaskDto, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).createCourseTask(courseId, createCourseTaskDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public deleteCourseTask(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).deleteCourseTask(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getAvailableCrossCheckReviewStats(courseId: number, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getAvailableCrossCheckReviewStats(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getCourseTask(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getCourseTask(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {'started' | 'inprogress' | 'finished'} [status] \n     * @param {'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck'} [checker] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getCourseTasks(courseId: number, status?: 'started' | 'inprogress' | 'finished', checker?: 'auto-test' | 'assigned' | 'mentor' | 'taskOwner' | 'crossCheck', options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getCourseTasks(courseId, status, checker, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getCourseTasksDetailed(courseId: number, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getCourseTasksDetailed(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {'started' | 'inprogress' | 'finished'} [status] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getCourseTasksWithStudentSolution(courseId: number, status?: 'started' | 'inprogress' | 'finished', options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getCourseTasksWithStudentSolution(courseId, status, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getCrossCheckCsv(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getCrossCheckCsv(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} pageSize \n     * @param {number} current \n     * @param {string} [orderBy] \n     * @param {string} [orderDirection] \n     * @param {string} [checker] \n     * @param {string} [student] \n     * @param {string} [url] \n     * @param {string} [task] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getCrossCheckPairs(courseId: number, pageSize: number, current: number, orderBy?: string, orderDirection?: string, checker?: string, student?: string, url?: string, task?: string, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getCrossCheckPairs(courseId, pageSize, current, orderBy, orderDirection, checker, student, url, task, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public getMyCrossCheckFeedbacks(courseId: number, courseTaskId: number, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).getMyCrossCheckFeedbacks(courseId, courseTaskId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {CheckTasksDeadlineDto} checkTasksDeadlineDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public notifyTasksDeadlines(checkTasksDeadlineDto: CheckTasksDeadlineDto, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).notifyTasksDeadlines(checkTasksDeadlineDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} courseTaskId \n     * @param {UpdateCourseTaskDto} updateCourseTaskDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof CoursesTasksApi\n     */\n    public updateCourseTask(courseId: number, courseTaskId: number, updateCourseTaskDto: UpdateCourseTaskDto, options?: AxiosRequestConfig) {\n        return CoursesTasksApiFp(this.configuration).updateCourseTask(courseId, courseTaskId, updateCourseTaskDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * DevtoolsApi - axios parameter creator\n * @export\n */\nexport const DevtoolsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDevUserLogin: async (githubId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('getDevUserLogin', 'githubId', githubId)\n            const localVarPath = `/devtools/user/{githubId}/login`\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDevUsers: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/devtools/users`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * DevtoolsApi - functional programming interface\n * @export\n */\nexport const DevtoolsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = DevtoolsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getDevUserLogin(githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getDevUserLogin(githubId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getDevUsers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DevtoolsUserDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getDevUsers(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * DevtoolsApi - factory interface\n * @export\n */\nexport const DevtoolsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = DevtoolsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDevUserLogin(githubId: string, options?: any): AxiosPromise<void> {\n            return localVarFp.getDevUserLogin(githubId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDevUsers(options?: any): AxiosPromise<Array<DevtoolsUserDto>> {\n            return localVarFp.getDevUsers(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * DevtoolsApi - object-oriented interface\n * @export\n * @class DevtoolsApi\n * @extends {BaseAPI}\n */\nexport class DevtoolsApi extends BaseAPI {\n    /**\n     * \n     * @param {string} githubId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DevtoolsApi\n     */\n    public getDevUserLogin(githubId: string, options?: AxiosRequestConfig) {\n        return DevtoolsApiFp(this.configuration).getDevUserLogin(githubId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DevtoolsApi\n     */\n    public getDevUsers(options?: AxiosRequestConfig) {\n        return DevtoolsApiFp(this.configuration).getDevUsers(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * DisciplinesApi - axios parameter creator\n * @export\n */\nexport const DisciplinesApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateDisciplineDto} createDisciplineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createDiscipline: async (createDisciplineDto: CreateDisciplineDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createDisciplineDto' is not null or undefined\n            assertParamExists('createDiscipline', 'createDisciplineDto', createDisciplineDto)\n            const localVarPath = `/disciplines`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createDisciplineDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteDiscipline: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteDiscipline', 'id', id)\n            const localVarPath = `/disciplines/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDisciplines: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/disciplines`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {DisciplineIdsDto} disciplineIdsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDisciplinesByIds: async (disciplineIdsDto: DisciplineIdsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'disciplineIdsDto' is not null or undefined\n            assertParamExists('getDisciplinesByIds', 'disciplineIdsDto', disciplineIdsDto)\n            const localVarPath = `/disciplines/ids`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(disciplineIdsDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateDisciplineDto} updateDisciplineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDiscipline: async (id: number, updateDisciplineDto: UpdateDisciplineDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateDiscipline', 'id', id)\n            // verify required parameter 'updateDisciplineDto' is not null or undefined\n            assertParamExists('updateDiscipline', 'updateDisciplineDto', updateDisciplineDto)\n            const localVarPath = `/disciplines/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateDisciplineDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * DisciplinesApi - functional programming interface\n * @export\n */\nexport const DisciplinesApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = DisciplinesApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateDisciplineDto} createDisciplineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createDiscipline(createDisciplineDto: CreateDisciplineDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DisciplineDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createDiscipline(createDisciplineDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteDiscipline(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteDiscipline(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getDisciplines(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DisciplineDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getDisciplines(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {DisciplineIdsDto} disciplineIdsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getDisciplinesByIds(disciplineIdsDto: DisciplineIdsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DisciplineDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getDisciplinesByIds(disciplineIdsDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateDisciplineDto} updateDisciplineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateDiscipline(id: number, updateDisciplineDto: UpdateDisciplineDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DisciplineDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateDiscipline(id, updateDisciplineDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * DisciplinesApi - factory interface\n * @export\n */\nexport const DisciplinesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = DisciplinesApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateDisciplineDto} createDisciplineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createDiscipline(createDisciplineDto: CreateDisciplineDto, options?: any): AxiosPromise<DisciplineDto> {\n            return localVarFp.createDiscipline(createDisciplineDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteDiscipline(id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteDiscipline(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDisciplines(options?: any): AxiosPromise<Array<DisciplineDto>> {\n            return localVarFp.getDisciplines(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {DisciplineIdsDto} disciplineIdsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDisciplinesByIds(disciplineIdsDto: DisciplineIdsDto, options?: any): AxiosPromise<Array<DisciplineDto>> {\n            return localVarFp.getDisciplinesByIds(disciplineIdsDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateDisciplineDto} updateDisciplineDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDiscipline(id: number, updateDisciplineDto: UpdateDisciplineDto, options?: any): AxiosPromise<DisciplineDto> {\n            return localVarFp.updateDiscipline(id, updateDisciplineDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * DisciplinesApi - object-oriented interface\n * @export\n * @class DisciplinesApi\n * @extends {BaseAPI}\n */\nexport class DisciplinesApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateDisciplineDto} createDisciplineDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DisciplinesApi\n     */\n    public createDiscipline(createDisciplineDto: CreateDisciplineDto, options?: AxiosRequestConfig) {\n        return DisciplinesApiFp(this.configuration).createDiscipline(createDisciplineDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DisciplinesApi\n     */\n    public deleteDiscipline(id: number, options?: AxiosRequestConfig) {\n        return DisciplinesApiFp(this.configuration).deleteDiscipline(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DisciplinesApi\n     */\n    public getDisciplines(options?: AxiosRequestConfig) {\n        return DisciplinesApiFp(this.configuration).getDisciplines(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {DisciplineIdsDto} disciplineIdsDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DisciplinesApi\n     */\n    public getDisciplinesByIds(disciplineIdsDto: DisciplineIdsDto, options?: AxiosRequestConfig) {\n        return DisciplinesApiFp(this.configuration).getDisciplinesByIds(disciplineIdsDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {UpdateDisciplineDto} updateDisciplineDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DisciplinesApi\n     */\n    public updateDiscipline(id: number, updateDisciplineDto: UpdateDisciplineDto, options?: AxiosRequestConfig) {\n        return DisciplinesApiFp(this.configuration).updateDiscipline(id, updateDisciplineDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * DiscordServersApi - axios parameter creator\n * @export\n */\nexport const DiscordServersApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateDiscordServerDto} createDiscordServerDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createDiscordServer: async (createDiscordServerDto: CreateDiscordServerDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createDiscordServerDto' is not null or undefined\n            assertParamExists('createDiscordServer', 'createDiscordServerDto', createDiscordServerDto)\n            const localVarPath = `/discord-servers`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createDiscordServerDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteDiscordServer: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteDiscordServer', 'id', id)\n            const localVarPath = `/discord-servers/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDiscordServers: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/discord-servers`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInviteLinkByDiscordServerId: async (courseId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getInviteLinkByDiscordServerId', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('getInviteLinkByDiscordServerId', 'id', id)\n            const localVarPath = `/discord-servers/{courseId}/invite/{id}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReducedDiscordServers: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/discord-servers/reduced`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateDiscordServerDto} updateDiscordServerDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDiscordServer: async (id: number, updateDiscordServerDto: UpdateDiscordServerDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateDiscordServer', 'id', id)\n            // verify required parameter 'updateDiscordServerDto' is not null or undefined\n            assertParamExists('updateDiscordServer', 'updateDiscordServerDto', updateDiscordServerDto)\n            const localVarPath = `/discord-servers/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateDiscordServerDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * DiscordServersApi - functional programming interface\n * @export\n */\nexport const DiscordServersApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = DiscordServersApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateDiscordServerDto} createDiscordServerDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createDiscordServer(createDiscordServerDto: CreateDiscordServerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DiscordServerDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createDiscordServer(createDiscordServerDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteDiscordServer(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DiscordServerDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteDiscordServer(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getDiscordServers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<DiscordServerDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getDiscordServers(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getInviteLinkByDiscordServerId(courseId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getInviteLinkByDiscordServerId(courseId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getReducedDiscordServers(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<IdNameDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getReducedDiscordServers(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateDiscordServerDto} updateDiscordServerDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateDiscordServer(id: number, updateDiscordServerDto: UpdateDiscordServerDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DiscordServerDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateDiscordServer(id, updateDiscordServerDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * DiscordServersApi - factory interface\n * @export\n */\nexport const DiscordServersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = DiscordServersApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateDiscordServerDto} createDiscordServerDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createDiscordServer(createDiscordServerDto: CreateDiscordServerDto, options?: any): AxiosPromise<DiscordServerDto> {\n            return localVarFp.createDiscordServer(createDiscordServerDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteDiscordServer(id: number, options?: any): AxiosPromise<DiscordServerDto> {\n            return localVarFp.deleteDiscordServer(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getDiscordServers(options?: any): AxiosPromise<Array<DiscordServerDto>> {\n            return localVarFp.getDiscordServers(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getInviteLinkByDiscordServerId(courseId: number, id: number, options?: any): AxiosPromise<string> {\n            return localVarFp.getInviteLinkByDiscordServerId(courseId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getReducedDiscordServers(options?: any): AxiosPromise<Array<IdNameDto>> {\n            return localVarFp.getReducedDiscordServers(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateDiscordServerDto} updateDiscordServerDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateDiscordServer(id: number, updateDiscordServerDto: UpdateDiscordServerDto, options?: any): AxiosPromise<DiscordServerDto> {\n            return localVarFp.updateDiscordServer(id, updateDiscordServerDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * DiscordServersApi - object-oriented interface\n * @export\n * @class DiscordServersApi\n * @extends {BaseAPI}\n */\nexport class DiscordServersApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateDiscordServerDto} createDiscordServerDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DiscordServersApi\n     */\n    public createDiscordServer(createDiscordServerDto: CreateDiscordServerDto, options?: AxiosRequestConfig) {\n        return DiscordServersApiFp(this.configuration).createDiscordServer(createDiscordServerDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DiscordServersApi\n     */\n    public deleteDiscordServer(id: number, options?: AxiosRequestConfig) {\n        return DiscordServersApiFp(this.configuration).deleteDiscordServer(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DiscordServersApi\n     */\n    public getDiscordServers(options?: AxiosRequestConfig) {\n        return DiscordServersApiFp(this.configuration).getDiscordServers(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DiscordServersApi\n     */\n    public getInviteLinkByDiscordServerId(courseId: number, id: number, options?: AxiosRequestConfig) {\n        return DiscordServersApiFp(this.configuration).getInviteLinkByDiscordServerId(courseId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DiscordServersApi\n     */\n    public getReducedDiscordServers(options?: AxiosRequestConfig) {\n        return DiscordServersApiFp(this.configuration).getReducedDiscordServers(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {UpdateDiscordServerDto} updateDiscordServerDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof DiscordServersApi\n     */\n    public updateDiscordServer(id: number, updateDiscordServerDto: UpdateDiscordServerDto, options?: AxiosRequestConfig) {\n        return DiscordServersApiFp(this.configuration).updateDiscordServer(id, updateDiscordServerDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * EventsApi - axios parameter creator\n * @export\n */\nexport const EventsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateEventDto} createEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createEvent: async (createEventDto: CreateEventDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createEventDto' is not null or undefined\n            assertParamExists('createEvent', 'createEventDto', createEventDto)\n            const localVarPath = `/events`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createEventDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteEvent: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteEvent', 'id', id)\n            const localVarPath = `/events/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getEvents: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/events`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateEventDto} updateEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateEvent: async (id: number, updateEventDto: UpdateEventDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateEvent', 'id', id)\n            // verify required parameter 'updateEventDto' is not null or undefined\n            assertParamExists('updateEvent', 'updateEventDto', updateEventDto)\n            const localVarPath = `/events/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateEventDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * EventsApi - functional programming interface\n * @export\n */\nexport const EventsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = EventsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateEventDto} createEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createEvent(createEventDto: CreateEventDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<EventDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createEvent(createEventDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteEvent(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteEvent(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getEvents(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<EventDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getEvents(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateEventDto} updateEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateEvent(id: number, updateEventDto: UpdateEventDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<EventDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateEvent(id, updateEventDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * EventsApi - factory interface\n * @export\n */\nexport const EventsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = EventsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateEventDto} createEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createEvent(createEventDto: CreateEventDto, options?: any): AxiosPromise<EventDto> {\n            return localVarFp.createEvent(createEventDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteEvent(id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteEvent(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getEvents(options?: any): AxiosPromise<Array<EventDto>> {\n            return localVarFp.getEvents(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateEventDto} updateEventDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateEvent(id: number, updateEventDto: UpdateEventDto, options?: any): AxiosPromise<EventDto> {\n            return localVarFp.updateEvent(id, updateEventDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * EventsApi - object-oriented interface\n * @export\n * @class EventsApi\n * @extends {BaseAPI}\n */\nexport class EventsApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateEventDto} createEventDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof EventsApi\n     */\n    public createEvent(createEventDto: CreateEventDto, options?: AxiosRequestConfig) {\n        return EventsApiFp(this.configuration).createEvent(createEventDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof EventsApi\n     */\n    public deleteEvent(id: number, options?: AxiosRequestConfig) {\n        return EventsApiFp(this.configuration).deleteEvent(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof EventsApi\n     */\n    public getEvents(options?: AxiosRequestConfig) {\n        return EventsApiFp(this.configuration).getEvents(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {UpdateEventDto} updateEventDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof EventsApi\n     */\n    public updateEvent(id: number, updateEventDto: UpdateEventDto, options?: AxiosRequestConfig) {\n        return EventsApiFp(this.configuration).updateEvent(id, updateEventDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * GratitudesApi - axios parameter creator\n * @export\n */\nexport const GratitudesApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateGratitudeDto} createGratitudeDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createGratitude: async (createGratitudeDto: CreateGratitudeDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createGratitudeDto' is not null or undefined\n            assertParamExists('createGratitude', 'createGratitudeDto', createGratitudeDto)\n            const localVarPath = `/gratitudes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createGratitudeDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBadges: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getBadges', 'courseId', courseId)\n            const localVarPath = `/gratitudes/badges/{courseId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getHeroesCountries: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/gratitudes/heroes/countries`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} current \n         * @param {number} pageSize \n         * @param {number} [courseId] \n         * @param {boolean} [notActivist] \n         * @param {string} [countryName] \n         * @param {string} [startDate] \n         * @param {string} [endDate] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getHeroesRadar: async (current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getHeroesRadar', 'current', current)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getHeroesRadar', 'pageSize', pageSize)\n            const localVarPath = `/gratitudes/heroes/radar`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (courseId !== undefined) {\n                localVarQueryParameter['courseId'] = courseId;\n            }\n\n            if (notActivist !== undefined) {\n                localVarQueryParameter['notActivist'] = notActivist;\n            }\n\n            if (countryName !== undefined) {\n                localVarQueryParameter['countryName'] = countryName;\n            }\n\n            if (startDate !== undefined) {\n                localVarQueryParameter['startDate'] = (startDate as any instanceof Date) ?\n                    (startDate as any).toISOString() :\n                    startDate;\n            }\n\n            if (endDate !== undefined) {\n                localVarQueryParameter['endDate'] = (endDate as any instanceof Date) ?\n                    (endDate as any).toISOString() :\n                    endDate;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} current \n         * @param {number} pageSize \n         * @param {number} [courseId] \n         * @param {boolean} [notActivist] \n         * @param {string} [countryName] \n         * @param {string} [startDate] \n         * @param {string} [endDate] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getHeroesRadarCsv: async (current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getHeroesRadarCsv', 'current', current)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getHeroesRadarCsv', 'pageSize', pageSize)\n            const localVarPath = `/gratitudes/heroes/radar/csv`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (courseId !== undefined) {\n                localVarQueryParameter['courseId'] = courseId;\n            }\n\n            if (notActivist !== undefined) {\n                localVarQueryParameter['notActivist'] = notActivist;\n            }\n\n            if (countryName !== undefined) {\n                localVarQueryParameter['countryName'] = countryName;\n            }\n\n            if (startDate !== undefined) {\n                localVarQueryParameter['startDate'] = (startDate as any instanceof Date) ?\n                    (startDate as any).toISOString() :\n                    startDate;\n            }\n\n            if (endDate !== undefined) {\n                localVarQueryParameter['endDate'] = (endDate as any instanceof Date) ?\n                    (endDate as any).toISOString() :\n                    endDate;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * GratitudesApi - functional programming interface\n * @export\n */\nexport const GratitudesApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = GratitudesApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateGratitudeDto} createGratitudeDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createGratitude(createGratitudeDto: CreateGratitudeDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GratitudeDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createGratitude(createGratitudeDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getBadges(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BadgeDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getBadges(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getHeroesCountries(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<CountryDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getHeroesCountries(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} current \n         * @param {number} pageSize \n         * @param {number} [courseId] \n         * @param {boolean} [notActivist] \n         * @param {string} [countryName] \n         * @param {string} [startDate] \n         * @param {string} [endDate] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<HeroesRadarDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getHeroesRadar(current, pageSize, courseId, notActivist, countryName, startDate, endDate, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} current \n         * @param {number} pageSize \n         * @param {number} [courseId] \n         * @param {boolean} [notActivist] \n         * @param {string} [countryName] \n         * @param {string} [startDate] \n         * @param {string} [endDate] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getHeroesRadarCsv(current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getHeroesRadarCsv(current, pageSize, courseId, notActivist, countryName, startDate, endDate, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * GratitudesApi - factory interface\n * @export\n */\nexport const GratitudesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = GratitudesApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateGratitudeDto} createGratitudeDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createGratitude(createGratitudeDto: CreateGratitudeDto, options?: any): AxiosPromise<GratitudeDto> {\n            return localVarFp.createGratitude(createGratitudeDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getBadges(courseId: number, options?: any): AxiosPromise<Array<BadgeDto>> {\n            return localVarFp.getBadges(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getHeroesCountries(options?: any): AxiosPromise<Array<CountryDto>> {\n            return localVarFp.getHeroesCountries(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} current \n         * @param {number} pageSize \n         * @param {number} [courseId] \n         * @param {boolean} [notActivist] \n         * @param {string} [countryName] \n         * @param {string} [startDate] \n         * @param {string} [endDate] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options?: any): AxiosPromise<HeroesRadarDto> {\n            return localVarFp.getHeroesRadar(current, pageSize, courseId, notActivist, countryName, startDate, endDate, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} current \n         * @param {number} pageSize \n         * @param {number} [courseId] \n         * @param {boolean} [notActivist] \n         * @param {string} [countryName] \n         * @param {string} [startDate] \n         * @param {string} [endDate] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getHeroesRadarCsv(current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options?: any): AxiosPromise<void> {\n            return localVarFp.getHeroesRadarCsv(current, pageSize, courseId, notActivist, countryName, startDate, endDate, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * GratitudesApi - object-oriented interface\n * @export\n * @class GratitudesApi\n * @extends {BaseAPI}\n */\nexport class GratitudesApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateGratitudeDto} createGratitudeDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof GratitudesApi\n     */\n    public createGratitude(createGratitudeDto: CreateGratitudeDto, options?: AxiosRequestConfig) {\n        return GratitudesApiFp(this.configuration).createGratitude(createGratitudeDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof GratitudesApi\n     */\n    public getBadges(courseId: number, options?: AxiosRequestConfig) {\n        return GratitudesApiFp(this.configuration).getBadges(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof GratitudesApi\n     */\n    public getHeroesCountries(options?: AxiosRequestConfig) {\n        return GratitudesApiFp(this.configuration).getHeroesCountries(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} current \n     * @param {number} pageSize \n     * @param {number} [courseId] \n     * @param {boolean} [notActivist] \n     * @param {string} [countryName] \n     * @param {string} [startDate] \n     * @param {string} [endDate] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof GratitudesApi\n     */\n    public getHeroesRadar(current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options?: AxiosRequestConfig) {\n        return GratitudesApiFp(this.configuration).getHeroesRadar(current, pageSize, courseId, notActivist, countryName, startDate, endDate, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} current \n     * @param {number} pageSize \n     * @param {number} [courseId] \n     * @param {boolean} [notActivist] \n     * @param {string} [countryName] \n     * @param {string} [startDate] \n     * @param {string} [endDate] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof GratitudesApi\n     */\n    public getHeroesRadarCsv(current: number, pageSize: number, courseId?: number, notActivist?: boolean, countryName?: string, startDate?: string, endDate?: string, options?: AxiosRequestConfig) {\n        return GratitudesApiFp(this.configuration).getHeroesRadarCsv(current, pageSize, courseId, notActivist, countryName, startDate, endDate, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * MentorReviewsApi - axios parameter creator\n * @export\n */\nexport const MentorReviewsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {MentorReviewAssignDto} mentorReviewAssignDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        assignReviewer: async (courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('assignReviewer', 'courseId', courseId)\n            // verify required parameter 'mentorReviewAssignDto' is not null or undefined\n            assertParamExists('assignReviewer', 'mentorReviewAssignDto', mentorReviewAssignDto)\n            const localVarPath = `/course/{courseId}/mentor-reviews`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(mentorReviewAssignDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {number} courseId \n         * @param {string} [tasks] \n         * @param {string} [student] \n         * @param {string} [checker] \n         * @param {string} [sortField] \n         * @param {string} [sortOrder] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorReviews: async (current: string, pageSize: string, courseId: number, tasks?: string, student?: string, checker?: string, sortField?: string, sortOrder?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getMentorReviews', 'current', current)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getMentorReviews', 'pageSize', pageSize)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getMentorReviews', 'courseId', courseId)\n            const localVarPath = `/course/{courseId}/mentor-reviews`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (tasks !== undefined) {\n                localVarQueryParameter['tasks'] = tasks;\n            }\n\n            if (student !== undefined) {\n                localVarQueryParameter['student'] = student;\n            }\n\n            if (checker !== undefined) {\n                localVarQueryParameter['checker'] = checker;\n            }\n\n            if (sortField !== undefined) {\n                localVarQueryParameter['sortField'] = sortField;\n            }\n\n            if (sortOrder !== undefined) {\n                localVarQueryParameter['sortOrder'] = sortOrder;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MentorReviewsApi - functional programming interface\n * @export\n */\nexport const MentorReviewsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MentorReviewsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {MentorReviewAssignDto} mentorReviewAssignDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async assignReviewer(courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.assignReviewer(courseId, mentorReviewAssignDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {number} courseId \n         * @param {string} [tasks] \n         * @param {string} [student] \n         * @param {string} [checker] \n         * @param {string} [sortField] \n         * @param {string} [sortOrder] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMentorReviews(current: string, pageSize: string, courseId: number, tasks?: string, student?: string, checker?: string, sortField?: string, sortOrder?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MentorReviewsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorReviews(current, pageSize, courseId, tasks, student, checker, sortField, sortOrder, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * MentorReviewsApi - factory interface\n * @export\n */\nexport const MentorReviewsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MentorReviewsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {MentorReviewAssignDto} mentorReviewAssignDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        assignReviewer(courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options?: any): AxiosPromise<void> {\n            return localVarFp.assignReviewer(courseId, mentorReviewAssignDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {number} courseId \n         * @param {string} [tasks] \n         * @param {string} [student] \n         * @param {string} [checker] \n         * @param {string} [sortField] \n         * @param {string} [sortOrder] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorReviews(current: string, pageSize: string, courseId: number, tasks?: string, student?: string, checker?: string, sortField?: string, sortOrder?: string, options?: any): AxiosPromise<MentorReviewsDto> {\n            return localVarFp.getMentorReviews(current, pageSize, courseId, tasks, student, checker, sortField, sortOrder, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * MentorReviewsApi - object-oriented interface\n * @export\n * @class MentorReviewsApi\n * @extends {BaseAPI}\n */\nexport class MentorReviewsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {MentorReviewAssignDto} mentorReviewAssignDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorReviewsApi\n     */\n    public assignReviewer(courseId: number, mentorReviewAssignDto: MentorReviewAssignDto, options?: AxiosRequestConfig) {\n        return MentorReviewsApiFp(this.configuration).assignReviewer(courseId, mentorReviewAssignDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} current \n     * @param {string} pageSize \n     * @param {number} courseId \n     * @param {string} [tasks] \n     * @param {string} [student] \n     * @param {string} [checker] \n     * @param {string} [sortField] \n     * @param {string} [sortOrder] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorReviewsApi\n     */\n    public getMentorReviews(current: string, pageSize: string, courseId: number, tasks?: string, student?: string, checker?: string, sortField?: string, sortOrder?: string, options?: AxiosRequestConfig) {\n        return MentorReviewsApiFp(this.configuration).getMentorReviews(current, pageSize, courseId, tasks, student, checker, sortField, sortOrder, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * MentorsApi - axios parameter creator\n * @export\n */\nexport const MentorsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStudentsCount: async (mentorId: number, courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'mentorId' is not null or undefined\n            assertParamExists('getCourseStudentsCount', 'mentorId', mentorId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseStudentsCount', 'courseId', courseId)\n            const localVarPath = `/mentors/{mentorId}/course/{courseId}/students`\n                .replace(`{${\"mentorId\"}}`, encodeURIComponent(String(mentorId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorDashboardData: async (mentorId: number, courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'mentorId' is not null or undefined\n            assertParamExists('getMentorDashboardData', 'mentorId', mentorId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getMentorDashboardData', 'courseId', courseId)\n            const localVarPath = `/mentors/{mentorId}/course/{courseId}/dashboard`\n                .replace(`{${\"mentorId\"}}`, encodeURIComponent(String(mentorId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorOptions: async (mentorId: number, courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'mentorId' is not null or undefined\n            assertParamExists('getMentorOptions', 'mentorId', mentorId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getMentorOptions', 'courseId', courseId)\n            const localVarPath = `/mentors/{mentorId}/course/{courseId}/options`\n                .replace(`{${\"mentorId\"}}`, encodeURIComponent(String(mentorId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorStudents: async (mentorId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'mentorId' is not null or undefined\n            assertParamExists('getMentorStudents', 'mentorId', mentorId)\n            const localVarPath = `/mentors/{mentorId}/students`\n                .replace(`{${\"mentorId\"}}`, encodeURIComponent(String(mentorId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRandomTask: async (mentorId: number, courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'mentorId' is not null or undefined\n            assertParamExists('getRandomTask', 'mentorId', mentorId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getRandomTask', 'courseId', courseId)\n            const localVarPath = `/mentors/{mentorId}/course/{courseId}/random-task`\n                .replace(`{${\"mentorId\"}}`, encodeURIComponent(String(mentorId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MentorsApi - functional programming interface\n * @export\n */\nexport const MentorsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MentorsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseStudentsCount(mentorId: number, courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<number>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseStudentsCount(mentorId, courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMentorDashboardData(mentorId: number, courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MentorDashboardDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorDashboardData(mentorId, courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMentorOptions(mentorId: number, courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<MentorOptionsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorOptions(mentorId, courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMentorStudents(mentorId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MentorStudentDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorStudents(mentorId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getRandomTask(mentorId: number, courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getRandomTask(mentorId, courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * MentorsApi - factory interface\n * @export\n */\nexport const MentorsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MentorsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseStudentsCount(mentorId: number, courseId: number, options?: any): AxiosPromise<number> {\n            return localVarFp.getCourseStudentsCount(mentorId, courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorDashboardData(mentorId: number, courseId: number, options?: any): AxiosPromise<Array<MentorDashboardDto>> {\n            return localVarFp.getMentorDashboardData(mentorId, courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorOptions(mentorId: number, courseId: number, options?: any): AxiosPromise<MentorOptionsDto> {\n            return localVarFp.getMentorOptions(mentorId, courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorStudents(mentorId: number, options?: any): AxiosPromise<Array<MentorStudentDto>> {\n            return localVarFp.getMentorStudents(mentorId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} mentorId \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getRandomTask(mentorId: number, courseId: number, options?: any): AxiosPromise<void> {\n            return localVarFp.getRandomTask(mentorId, courseId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * MentorsApi - object-oriented interface\n * @export\n * @class MentorsApi\n * @extends {BaseAPI}\n */\nexport class MentorsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} mentorId \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorsApi\n     */\n    public getCourseStudentsCount(mentorId: number, courseId: number, options?: AxiosRequestConfig) {\n        return MentorsApiFp(this.configuration).getCourseStudentsCount(mentorId, courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} mentorId \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorsApi\n     */\n    public getMentorDashboardData(mentorId: number, courseId: number, options?: AxiosRequestConfig) {\n        return MentorsApiFp(this.configuration).getMentorDashboardData(mentorId, courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} mentorId \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorsApi\n     */\n    public getMentorOptions(mentorId: number, courseId: number, options?: AxiosRequestConfig) {\n        return MentorsApiFp(this.configuration).getMentorOptions(mentorId, courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} mentorId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorsApi\n     */\n    public getMentorStudents(mentorId: number, options?: AxiosRequestConfig) {\n        return MentorsApiFp(this.configuration).getMentorStudents(mentorId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} mentorId \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorsApi\n     */\n    public getRandomTask(mentorId: number, courseId: number, options?: AxiosRequestConfig) {\n        return MentorsApiFp(this.configuration).getRandomTask(mentorId, courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * MentorsHallOfFameApi - axios parameter creator\n * @export\n */\nexport const MentorsHallOfFameApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {boolean} [allTime] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTopMentors: async (allTime?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/mentors-hall-of-fame`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (allTime !== undefined) {\n                localVarQueryParameter['allTime'] = allTime;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * MentorsHallOfFameApi - functional programming interface\n * @export\n */\nexport const MentorsHallOfFameApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = MentorsHallOfFameApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {boolean} [allTime] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTopMentors(allTime?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TopMentorDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTopMentors(allTime, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * MentorsHallOfFameApi - factory interface\n * @export\n */\nexport const MentorsHallOfFameApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = MentorsHallOfFameApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {boolean} [allTime] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTopMentors(allTime?: boolean, options?: any): AxiosPromise<Array<TopMentorDto>> {\n            return localVarFp.getTopMentors(allTime, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * MentorsHallOfFameApi - object-oriented interface\n * @export\n * @class MentorsHallOfFameApi\n * @extends {BaseAPI}\n */\nexport class MentorsHallOfFameApi extends BaseAPI {\n    /**\n     * \n     * @param {boolean} [allTime] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof MentorsHallOfFameApi\n     */\n    public getTopMentors(allTime?: boolean, options?: AxiosRequestConfig) {\n        return MentorsHallOfFameApiFp(this.configuration).getTopMentors(allTime, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * NotificationsApi - axios parameter creator\n * @export\n */\nexport const NotificationsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {UpdateNotificationDto} updateNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotification: async (updateNotificationDto: UpdateNotificationDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'updateNotificationDto' is not null or undefined\n            assertParamExists('createNotification', 'updateNotificationDto', updateNotificationDto)\n            const localVarPath = `/notifications`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateNotificationDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotification: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteNotification', 'id', id)\n            const localVarPath = `/notifications/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotifications: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/notifications`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {UpdateNotificationDto} updateNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotification: async (updateNotificationDto: UpdateNotificationDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'updateNotificationDto' is not null or undefined\n            assertParamExists('updateNotification', 'updateNotificationDto', updateNotificationDto)\n            const localVarPath = `/notifications`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateNotificationDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * NotificationsApi - functional programming interface\n * @export\n */\nexport const NotificationsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = NotificationsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {UpdateNotificationDto} updateNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createNotification(updateNotificationDto: UpdateNotificationDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createNotification(updateNotificationDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteNotification(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteNotification(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getNotifications(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<NotificationDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getNotifications(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {UpdateNotificationDto} updateNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateNotification(updateNotificationDto: UpdateNotificationDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateNotification(updateNotificationDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * NotificationsApi - factory interface\n * @export\n */\nexport const NotificationsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = NotificationsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {UpdateNotificationDto} updateNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createNotification(updateNotificationDto: UpdateNotificationDto, options?: any): AxiosPromise<NotificationDto> {\n            return localVarFp.createNotification(updateNotificationDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteNotification(id: string, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteNotification(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getNotifications(options?: any): AxiosPromise<Array<NotificationDto>> {\n            return localVarFp.getNotifications(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {UpdateNotificationDto} updateNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateNotification(updateNotificationDto: UpdateNotificationDto, options?: any): AxiosPromise<NotificationDto> {\n            return localVarFp.updateNotification(updateNotificationDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * NotificationsApi - object-oriented interface\n * @export\n * @class NotificationsApi\n * @extends {BaseAPI}\n */\nexport class NotificationsApi extends BaseAPI {\n    /**\n     * \n     * @param {UpdateNotificationDto} updateNotificationDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof NotificationsApi\n     */\n    public createNotification(updateNotificationDto: UpdateNotificationDto, options?: AxiosRequestConfig) {\n        return NotificationsApiFp(this.configuration).createNotification(updateNotificationDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof NotificationsApi\n     */\n    public deleteNotification(id: string, options?: AxiosRequestConfig) {\n        return NotificationsApiFp(this.configuration).deleteNotification(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof NotificationsApi\n     */\n    public getNotifications(options?: AxiosRequestConfig) {\n        return NotificationsApiFp(this.configuration).getNotifications(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {UpdateNotificationDto} updateNotificationDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof NotificationsApi\n     */\n    public updateNotification(updateNotificationDto: UpdateNotificationDto, options?: AxiosRequestConfig) {\n        return NotificationsApiFp(this.configuration).updateNotification(updateNotificationDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * OpportunitiesApi - axios parameter creator\n * @export\n */\nexport const OpportunitiesApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createConsent: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/opportunities/consent`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteConsent: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/opportunities/consent`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getApplicants: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/opportunities/applicants`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getConsent: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/opportunities/consent`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} uuid \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPublicResume: async (uuid: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'uuid' is not null or undefined\n            assertParamExists('getPublicResume', 'uuid', uuid)\n            const localVarPath = `/opportunities/public/{uuid}`\n                .replace(`{${\"uuid\"}}`, encodeURIComponent(String(uuid)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getResume: async (githubId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('getResume', 'githubId', githubId)\n            const localVarPath = `/opportunities/{githubId}/resume`\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        prolong: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/opportunities/prolong`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {FormDataDto} formDataDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveResume: async (githubId: string, formDataDto: FormDataDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('saveResume', 'githubId', githubId)\n            // verify required parameter 'formDataDto' is not null or undefined\n            assertParamExists('saveResume', 'formDataDto', formDataDto)\n            const localVarPath = `/opportunities/{githubId}/resume`\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(formDataDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        setVisibility: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/opportunities/visibility`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * OpportunitiesApi - functional programming interface\n * @export\n */\nexport const OpportunitiesApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = OpportunitiesApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createConsent(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<GiveConsentDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createConsent(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteConsent(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConsentDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteConsent(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getApplicants(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ApplicantResumeDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getApplicants(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getConsent(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ConsentDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getConsent(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} uuid \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPublicResume(uuid: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ResumeDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPublicResume(uuid, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getResume(githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ResumeDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getResume(githubId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async prolong(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StatusDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.prolong(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {FormDataDto} formDataDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async saveResume(githubId: string, formDataDto: FormDataDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.saveResume(githubId, formDataDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async setVisibility(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<VisibilityDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.setVisibility(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * OpportunitiesApi - factory interface\n * @export\n */\nexport const OpportunitiesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = OpportunitiesApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createConsent(options?: any): AxiosPromise<GiveConsentDto> {\n            return localVarFp.createConsent(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteConsent(options?: any): AxiosPromise<ConsentDto> {\n            return localVarFp.deleteConsent(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getApplicants(options?: any): AxiosPromise<Array<ApplicantResumeDto>> {\n            return localVarFp.getApplicants(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getConsent(options?: any): AxiosPromise<ConsentDto> {\n            return localVarFp.getConsent(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} uuid \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPublicResume(uuid: string, options?: any): AxiosPromise<ResumeDto> {\n            return localVarFp.getPublicResume(uuid, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getResume(githubId: string, options?: any): AxiosPromise<ResumeDto> {\n            return localVarFp.getResume(githubId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        prolong(options?: any): AxiosPromise<StatusDto> {\n            return localVarFp.prolong(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {FormDataDto} formDataDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        saveResume(githubId: string, formDataDto: FormDataDto, options?: any): AxiosPromise<object> {\n            return localVarFp.saveResume(githubId, formDataDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        setVisibility(options?: any): AxiosPromise<VisibilityDto> {\n            return localVarFp.setVisibility(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * OpportunitiesApi - object-oriented interface\n * @export\n * @class OpportunitiesApi\n * @extends {BaseAPI}\n */\nexport class OpportunitiesApi extends BaseAPI {\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public createConsent(options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).createConsent(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public deleteConsent(options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).deleteConsent(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public getApplicants(options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).getApplicants(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public getConsent(options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).getConsent(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} uuid \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public getPublicResume(uuid: string, options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).getPublicResume(uuid, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} githubId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public getResume(githubId: string, options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).getResume(githubId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public prolong(options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).prolong(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} githubId \n     * @param {FormDataDto} formDataDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public saveResume(githubId: string, formDataDto: FormDataDto, options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).saveResume(githubId, formDataDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof OpportunitiesApi\n     */\n    public setVisibility(options?: AxiosRequestConfig) {\n        return OpportunitiesApiFp(this.configuration).setVisibility(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * ProfileApi - axios parameter creator\n * @export\n */\nexport const ProfileApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getEndorsement: async (username: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('getEndorsement', 'username', username)\n            const localVarPath = `/profile/{username}/endorsement`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getEndorsementData: async (username: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('getEndorsementData', 'username', username)\n            const localVarPath = `/profile/{username}/endorsement-data`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPersonalProfile: async (username: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('getPersonalProfile', 'username', username)\n            const localVarPath = `/profile/{username}/personal`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getProfile: async (username: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('getProfile', 'username', username)\n            const localVarPath = `/profile/{username}`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserCourses: async (username: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('getUserCourses', 'username', username)\n            const localVarPath = `/profile/{username}/courses`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        obfuscateProfile: async (username: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'username' is not null or undefined\n            assertParamExists('obfuscateProfile', 'username', username)\n            const localVarPath = `/profile/{username}`\n                .replace(`{${\"username\"}}`, encodeURIComponent(String(username)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {UpdateProfileInfoDto} updateProfileInfoDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateProfileInfoFlat: async (updateProfileInfoDto: UpdateProfileInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'updateProfileInfoDto' is not null or undefined\n            assertParamExists('updateProfileInfoFlat', 'updateProfileInfoDto', updateProfileInfoDto)\n            const localVarPath = `/profile/info`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateProfileInfoDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {UpdateUserDto} updateUserDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUser: async (updateUserDto: UpdateUserDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'updateUserDto' is not null or undefined\n            assertParamExists('updateUser', 'updateUserDto', updateUserDto)\n            const localVarPath = `/profile/user`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateUserDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ProfileApi - functional programming interface\n * @export\n */\nexport const ProfileApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ProfileApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getEndorsement(username: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<EndorsementDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getEndorsement(username, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getEndorsementData(username: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<EndorsementDataDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getEndorsementData(username, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPersonalProfile(username: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonalProfileDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPersonalProfile(username, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getProfile(username: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ProfileDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getProfile(username, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUserCourses(username: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ProfileCourseDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserCourses(username, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async obfuscateProfile(username: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.obfuscateProfile(username, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {UpdateProfileInfoDto} updateProfileInfoDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateProfileInfoFlat(updateProfileInfoDto: UpdateProfileInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateProfileInfoFlat(updateProfileInfoDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {UpdateUserDto} updateUserDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateUser(updateUserDto: UpdateUserDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateUser(updateUserDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * ProfileApi - factory interface\n * @export\n */\nexport const ProfileApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ProfileApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getEndorsement(username: string, options?: any): AxiosPromise<EndorsementDto> {\n            return localVarFp.getEndorsement(username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getEndorsementData(username: string, options?: any): AxiosPromise<EndorsementDataDto> {\n            return localVarFp.getEndorsementData(username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPersonalProfile(username: string, options?: any): AxiosPromise<PersonalProfileDto> {\n            return localVarFp.getPersonalProfile(username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getProfile(username: string, options?: any): AxiosPromise<ProfileDto> {\n            return localVarFp.getProfile(username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserCourses(username: string, options?: any): AxiosPromise<Array<ProfileCourseDto>> {\n            return localVarFp.getUserCourses(username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} username \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        obfuscateProfile(username: string, options?: any): AxiosPromise<void> {\n            return localVarFp.obfuscateProfile(username, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {UpdateProfileInfoDto} updateProfileInfoDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateProfileInfoFlat(updateProfileInfoDto: UpdateProfileInfoDto, options?: any): AxiosPromise<void> {\n            return localVarFp.updateProfileInfoFlat(updateProfileInfoDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {UpdateUserDto} updateUserDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUser(updateUserDto: UpdateUserDto, options?: any): AxiosPromise<void> {\n            return localVarFp.updateUser(updateUserDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * ProfileApi - object-oriented interface\n * @export\n * @class ProfileApi\n * @extends {BaseAPI}\n */\nexport class ProfileApi extends BaseAPI {\n    /**\n     * \n     * @param {string} username \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public getEndorsement(username: string, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).getEndorsement(username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} username \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public getEndorsementData(username: string, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).getEndorsementData(username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} username \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public getPersonalProfile(username: string, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).getPersonalProfile(username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} username \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public getProfile(username: string, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).getProfile(username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} username \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public getUserCourses(username: string, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).getUserCourses(username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} username \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public obfuscateProfile(username: string, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).obfuscateProfile(username, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {UpdateProfileInfoDto} updateProfileInfoDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public updateProfileInfoFlat(updateProfileInfoDto: UpdateProfileInfoDto, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).updateProfileInfoFlat(updateProfileInfoDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {UpdateUserDto} updateUserDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ProfileApi\n     */\n    public updateUser(updateUserDto: UpdateUserDto, options?: AxiosRequestConfig) {\n        return ProfileApiFp(this.configuration).updateUser(updateUserDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * PromptsApi - axios parameter creator\n * @export\n */\nexport const PromptsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreatePromptDto} createPromptDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPrompt: async (createPromptDto: CreatePromptDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createPromptDto' is not null or undefined\n            assertParamExists('createPrompt', 'createPromptDto', createPromptDto)\n            const localVarPath = `/prompts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createPromptDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePrompt: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deletePrompt', 'id', id)\n            const localVarPath = `/prompts/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPrompts: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/prompts`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdatePromptDto} updatePromptDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePrompt: async (id: number, updatePromptDto: UpdatePromptDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updatePrompt', 'id', id)\n            // verify required parameter 'updatePromptDto' is not null or undefined\n            assertParamExists('updatePrompt', 'updatePromptDto', updatePromptDto)\n            const localVarPath = `/prompts/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updatePromptDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * PromptsApi - functional programming interface\n * @export\n */\nexport const PromptsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = PromptsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreatePromptDto} createPromptDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createPrompt(createPromptDto: CreatePromptDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PromptDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createPrompt(createPromptDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deletePrompt(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deletePrompt(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getPrompts(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PromptDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getPrompts(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdatePromptDto} updatePromptDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updatePrompt(id: number, updatePromptDto: UpdatePromptDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PromptDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePrompt(id, updatePromptDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * PromptsApi - factory interface\n * @export\n */\nexport const PromptsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = PromptsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreatePromptDto} createPromptDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createPrompt(createPromptDto: CreatePromptDto, options?: any): AxiosPromise<PromptDto> {\n            return localVarFp.createPrompt(createPromptDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deletePrompt(id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deletePrompt(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getPrompts(options?: any): AxiosPromise<Array<PromptDto>> {\n            return localVarFp.getPrompts(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdatePromptDto} updatePromptDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updatePrompt(id: number, updatePromptDto: UpdatePromptDto, options?: any): AxiosPromise<PromptDto> {\n            return localVarFp.updatePrompt(id, updatePromptDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * PromptsApi - object-oriented interface\n * @export\n * @class PromptsApi\n * @extends {BaseAPI}\n */\nexport class PromptsApi extends BaseAPI {\n    /**\n     * \n     * @param {CreatePromptDto} createPromptDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof PromptsApi\n     */\n    public createPrompt(createPromptDto: CreatePromptDto, options?: AxiosRequestConfig) {\n        return PromptsApiFp(this.configuration).createPrompt(createPromptDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof PromptsApi\n     */\n    public deletePrompt(id: number, options?: AxiosRequestConfig) {\n        return PromptsApiFp(this.configuration).deletePrompt(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof PromptsApi\n     */\n    public getPrompts(options?: AxiosRequestConfig) {\n        return PromptsApiFp(this.configuration).getPrompts(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {UpdatePromptDto} updatePromptDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof PromptsApi\n     */\n    public updatePrompt(id: number, updatePromptDto: UpdatePromptDto, options?: AxiosRequestConfig) {\n        return PromptsApiFp(this.configuration).updatePrompt(id, updatePromptDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * RegistryApi - axios parameter creator\n * @export\n */\nexport const RegistryApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} githubId \n         * @param {ApproveMentorDto} approveMentorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        approveMentor: async (githubId: string, approveMentorDto: ApproveMentorDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('approveMentor', 'githubId', githubId)\n            // verify required parameter 'approveMentorDto' is not null or undefined\n            assertParamExists('approveMentor', 'approveMentorDto', approveMentorDto)\n            const localVarPath = `/registry/mentor/{githubId}`\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(approveMentorDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        cancelMentorRegistry: async (githubId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('cancelMentorRegistry', 'githubId', githubId)\n            const localVarPath = `/registry/mentor/{githubId}`\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {CommentMentorRegistryDto} commentMentorRegistryDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        commentMentorRegistry: async (githubId: string, commentMentorRegistryDto: CommentMentorRegistryDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('commentMentorRegistry', 'githubId', githubId)\n            // verify required parameter 'commentMentorRegistryDto' is not null or undefined\n            assertParamExists('commentMentorRegistry', 'commentMentorRegistryDto', commentMentorRegistryDto)\n            const localVarPath = `/registry/mentor/{githubId}/comment`\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(commentMentorRegistryDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {'new' | 'all'} [status] \n         * @param {number} [pageSize] \n         * @param {number} [currentPage] \n         * @param {string} [githubId] \n         * @param {string} [cityName] \n         * @param {Array<number>} [preferedCourses] \n         * @param {Array<number>} [preselectedCourses] \n         * @param {Array<string>} [technicalMentoring] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorRegistries: async (status?: 'new' | 'all', pageSize?: number, currentPage?: number, githubId?: string, cityName?: string, preferedCourses?: Array<number>, preselectedCourses?: Array<number>, technicalMentoring?: Array<string>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/registry/mentors`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (status !== undefined) {\n                localVarQueryParameter['status'] = status;\n            }\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (currentPage !== undefined) {\n                localVarQueryParameter['currentPage'] = currentPage;\n            }\n\n            if (githubId !== undefined) {\n                localVarQueryParameter['githubId'] = githubId;\n            }\n\n            if (cityName !== undefined) {\n                localVarQueryParameter['cityName'] = cityName;\n            }\n\n            if (preferedCourses) {\n                localVarQueryParameter['preferedCourses'] = preferedCourses;\n            }\n\n            if (preselectedCourses) {\n                localVarQueryParameter['preselectedCourses'] = preselectedCourses;\n            }\n\n            if (technicalMentoring) {\n                localVarQueryParameter['technicalMentoring'] = technicalMentoring;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {InviteMentorsDto} inviteMentorsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        inviteMentors: async (inviteMentorsDto: InviteMentorsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'inviteMentorsDto' is not null or undefined\n            assertParamExists('inviteMentors', 'inviteMentorsDto', inviteMentorsDto)\n            const localVarPath = `/registry/mentors/invite`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(inviteMentorsDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * RegistryApi - functional programming interface\n * @export\n */\nexport const RegistryApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = RegistryApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} githubId \n         * @param {ApproveMentorDto} approveMentorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async approveMentor(githubId: string, approveMentorDto: ApproveMentorDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.approveMentor(githubId, approveMentorDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async cancelMentorRegistry(githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.cancelMentorRegistry(githubId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {CommentMentorRegistryDto} commentMentorRegistryDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async commentMentorRegistry(githubId: string, commentMentorRegistryDto: CommentMentorRegistryDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.commentMentorRegistry(githubId, commentMentorRegistryDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {'new' | 'all'} [status] \n         * @param {number} [pageSize] \n         * @param {number} [currentPage] \n         * @param {string} [githubId] \n         * @param {string} [cityName] \n         * @param {Array<number>} [preferedCourses] \n         * @param {Array<number>} [preselectedCourses] \n         * @param {Array<string>} [technicalMentoring] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getMentorRegistries(status?: 'new' | 'all', pageSize?: number, currentPage?: number, githubId?: string, cityName?: string, preferedCourses?: Array<number>, preselectedCourses?: Array<number>, technicalMentoring?: Array<string>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<FilterMentorRegistryResponse>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getMentorRegistries(status, pageSize, currentPage, githubId, cityName, preferedCourses, preselectedCourses, technicalMentoring, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {InviteMentorsDto} inviteMentorsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async inviteMentors(inviteMentorsDto: InviteMentorsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.inviteMentors(inviteMentorsDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * RegistryApi - factory interface\n * @export\n */\nexport const RegistryApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = RegistryApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {string} githubId \n         * @param {ApproveMentorDto} approveMentorDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        approveMentor(githubId: string, approveMentorDto: ApproveMentorDto, options?: any): AxiosPromise<void> {\n            return localVarFp.approveMentor(githubId, approveMentorDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        cancelMentorRegistry(githubId: string, options?: any): AxiosPromise<void> {\n            return localVarFp.cancelMentorRegistry(githubId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} githubId \n         * @param {CommentMentorRegistryDto} commentMentorRegistryDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        commentMentorRegistry(githubId: string, commentMentorRegistryDto: CommentMentorRegistryDto, options?: any): AxiosPromise<void> {\n            return localVarFp.commentMentorRegistry(githubId, commentMentorRegistryDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {'new' | 'all'} [status] \n         * @param {number} [pageSize] \n         * @param {number} [currentPage] \n         * @param {string} [githubId] \n         * @param {string} [cityName] \n         * @param {Array<number>} [preferedCourses] \n         * @param {Array<number>} [preselectedCourses] \n         * @param {Array<string>} [technicalMentoring] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getMentorRegistries(status?: 'new' | 'all', pageSize?: number, currentPage?: number, githubId?: string, cityName?: string, preferedCourses?: Array<number>, preselectedCourses?: Array<number>, technicalMentoring?: Array<string>, options?: any): AxiosPromise<FilterMentorRegistryResponse> {\n            return localVarFp.getMentorRegistries(status, pageSize, currentPage, githubId, cityName, preferedCourses, preselectedCourses, technicalMentoring, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {InviteMentorsDto} inviteMentorsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        inviteMentors(inviteMentorsDto: InviteMentorsDto, options?: any): AxiosPromise<void> {\n            return localVarFp.inviteMentors(inviteMentorsDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * RegistryApi - object-oriented interface\n * @export\n * @class RegistryApi\n * @extends {BaseAPI}\n */\nexport class RegistryApi extends BaseAPI {\n    /**\n     * \n     * @param {string} githubId \n     * @param {ApproveMentorDto} approveMentorDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof RegistryApi\n     */\n    public approveMentor(githubId: string, approveMentorDto: ApproveMentorDto, options?: AxiosRequestConfig) {\n        return RegistryApiFp(this.configuration).approveMentor(githubId, approveMentorDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} githubId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof RegistryApi\n     */\n    public cancelMentorRegistry(githubId: string, options?: AxiosRequestConfig) {\n        return RegistryApiFp(this.configuration).cancelMentorRegistry(githubId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} githubId \n     * @param {CommentMentorRegistryDto} commentMentorRegistryDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof RegistryApi\n     */\n    public commentMentorRegistry(githubId: string, commentMentorRegistryDto: CommentMentorRegistryDto, options?: AxiosRequestConfig) {\n        return RegistryApiFp(this.configuration).commentMentorRegistry(githubId, commentMentorRegistryDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {'new' | 'all'} [status] \n     * @param {number} [pageSize] \n     * @param {number} [currentPage] \n     * @param {string} [githubId] \n     * @param {string} [cityName] \n     * @param {Array<number>} [preferedCourses] \n     * @param {Array<number>} [preselectedCourses] \n     * @param {Array<string>} [technicalMentoring] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof RegistryApi\n     */\n    public getMentorRegistries(status?: 'new' | 'all', pageSize?: number, currentPage?: number, githubId?: string, cityName?: string, preferedCourses?: Array<number>, preselectedCourses?: Array<number>, technicalMentoring?: Array<string>, options?: AxiosRequestConfig) {\n        return RegistryApiFp(this.configuration).getMentorRegistries(status, pageSize, currentPage, githubId, cityName, preferedCourses, preselectedCourses, technicalMentoring, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {InviteMentorsDto} inviteMentorsDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof RegistryApi\n     */\n    public inviteMentors(inviteMentorsDto: InviteMentorsDto, options?: AxiosRequestConfig) {\n        return RegistryApiFp(this.configuration).inviteMentors(inviteMentorsDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * ScheduleApi - axios parameter creator\n * @export\n */\nexport const ScheduleApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CheckScheduleChangesDto} checkScheduleChangesDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        notifyScheduleChanges: async (checkScheduleChangesDto: CheckScheduleChangesDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'checkScheduleChangesDto' is not null or undefined\n            assertParamExists('notifyScheduleChanges', 'checkScheduleChangesDto', checkScheduleChangesDto)\n            const localVarPath = `/schedule/notify/changes`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(checkScheduleChangesDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * ScheduleApi - functional programming interface\n * @export\n */\nexport const ScheduleApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = ScheduleApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CheckScheduleChangesDto} checkScheduleChangesDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async notifyScheduleChanges(checkScheduleChangesDto: CheckScheduleChangesDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.notifyScheduleChanges(checkScheduleChangesDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * ScheduleApi - factory interface\n * @export\n */\nexport const ScheduleApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = ScheduleApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CheckScheduleChangesDto} checkScheduleChangesDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        notifyScheduleChanges(checkScheduleChangesDto: CheckScheduleChangesDto, options?: any): AxiosPromise<void> {\n            return localVarFp.notifyScheduleChanges(checkScheduleChangesDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * ScheduleApi - object-oriented interface\n * @export\n * @class ScheduleApi\n * @extends {BaseAPI}\n */\nexport class ScheduleApi extends BaseAPI {\n    /**\n     * \n     * @param {CheckScheduleChangesDto} checkScheduleChangesDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof ScheduleApi\n     */\n    public notifyScheduleChanges(checkScheduleChangesDto: CheckScheduleChangesDto, options?: AxiosRequestConfig) {\n        return ScheduleApiFp(this.configuration).notifyScheduleChanges(checkScheduleChangesDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * SessionApi - axios parameter creator\n * @export\n */\nexport const SessionApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSession: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/session`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * SessionApi - functional programming interface\n * @export\n */\nexport const SessionApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = SessionApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getSession(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AuthUserDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getSession(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * SessionApi - factory interface\n * @export\n */\nexport const SessionApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = SessionApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getSession(options?: any): AxiosPromise<AuthUserDto> {\n            return localVarFp.getSession(options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * SessionApi - object-oriented interface\n * @export\n * @class SessionApi\n * @extends {BaseAPI}\n */\nexport class SessionApi extends BaseAPI {\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof SessionApi\n     */\n    public getSession(options?: AxiosRequestConfig) {\n        return SessionApiFp(this.configuration).getSession(options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * StudentsApi - axios parameter creator\n * @export\n */\nexport const StudentsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {ExpelStatusDto} expelStatusDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        expelStudents: async (courseId: number, expelStatusDto: ExpelStatusDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('expelStudents', 'courseId', courseId)\n            // verify required parameter 'expelStatusDto' is not null or undefined\n            assertParamExists('expelStudents', 'expelStatusDto', expelStatusDto)\n            const localVarPath = `/courses/{courseId}/students/expel`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(expelStatusDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudent: async (studentId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'studentId' is not null or undefined\n            assertParamExists('getStudent', 'studentId', studentId)\n            const localVarPath = `/students/{studentId}`\n                .replace(`{${\"studentId\"}}`, encodeURIComponent(String(studentId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentSummary: async (courseId: number, githubId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getStudentSummary', 'courseId', courseId)\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('getStudentSummary', 'githubId', githubId)\n            const localVarPath = `/courses/{courseId}/students/{githubId}/summary`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {string} [student] \n         * @param {string} [country] \n         * @param {string} [city] \n         * @param {string} [ongoingCourses] \n         * @param {string} [previousCourses] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserStudents: async (current: string, pageSize: string, student?: string, country?: string, city?: string, ongoingCourses?: string, previousCourses?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getUserStudents', 'current', current)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getUserStudents', 'pageSize', pageSize)\n            const localVarPath = `/students`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (student !== undefined) {\n                localVarQueryParameter['student'] = student;\n            }\n\n            if (country !== undefined) {\n                localVarQueryParameter['country'] = country;\n            }\n\n            if (city !== undefined) {\n                localVarQueryParameter['city'] = city;\n            }\n\n            if (ongoingCourses !== undefined) {\n                localVarQueryParameter['ongoingCourses'] = ongoingCourses;\n            }\n\n            if (previousCourses !== undefined) {\n                localVarQueryParameter['previousCourses'] = previousCourses;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * StudentsApi - functional programming interface\n * @export\n */\nexport const StudentsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = StudentsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {ExpelStatusDto} expelStatusDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async expelStudents(courseId: number, expelStatusDto: ExpelStatusDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.expelStudents(courseId, expelStatusDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getStudent(studentId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StudentDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getStudent(studentId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getStudentSummary(courseId: number, githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StudentSummaryDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentSummary(courseId, githubId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {string} [student] \n         * @param {string} [country] \n         * @param {string} [city] \n         * @param {string} [ongoingCourses] \n         * @param {string} [previousCourses] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUserStudents(current: string, pageSize: string, student?: string, country?: string, city?: string, ongoingCourses?: string, previousCourses?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserStudentsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserStudents(current, pageSize, student, country, city, ongoingCourses, previousCourses, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * StudentsApi - factory interface\n * @export\n */\nexport const StudentsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = StudentsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {ExpelStatusDto} expelStatusDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        expelStudents(courseId: number, expelStatusDto: ExpelStatusDto, options?: any): AxiosPromise<void> {\n            return localVarFp.expelStudents(courseId, expelStatusDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudent(studentId: number, options?: any): AxiosPromise<StudentDto> {\n            return localVarFp.getStudent(studentId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentSummary(courseId: number, githubId: string, options?: any): AxiosPromise<StudentSummaryDto> {\n            return localVarFp.getStudentSummary(courseId, githubId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {string} [student] \n         * @param {string} [country] \n         * @param {string} [city] \n         * @param {string} [ongoingCourses] \n         * @param {string} [previousCourses] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserStudents(current: string, pageSize: string, student?: string, country?: string, city?: string, ongoingCourses?: string, previousCourses?: string, options?: any): AxiosPromise<UserStudentsDto> {\n            return localVarFp.getUserStudents(current, pageSize, student, country, city, ongoingCourses, previousCourses, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * StudentsApi - object-oriented interface\n * @export\n * @class StudentsApi\n * @extends {BaseAPI}\n */\nexport class StudentsApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {ExpelStatusDto} expelStatusDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsApi\n     */\n    public expelStudents(courseId: number, expelStatusDto: ExpelStatusDto, options?: AxiosRequestConfig) {\n        return StudentsApiFp(this.configuration).expelStudents(courseId, expelStatusDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} studentId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsApi\n     */\n    public getStudent(studentId: number, options?: AxiosRequestConfig) {\n        return StudentsApiFp(this.configuration).getStudent(studentId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {string} githubId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsApi\n     */\n    public getStudentSummary(courseId: number, githubId: string, options?: AxiosRequestConfig) {\n        return StudentsApiFp(this.configuration).getStudentSummary(courseId, githubId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {string} current \n     * @param {string} pageSize \n     * @param {string} [student] \n     * @param {string} [country] \n     * @param {string} [city] \n     * @param {string} [ongoingCourses] \n     * @param {string} [previousCourses] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsApi\n     */\n    public getUserStudents(current: string, pageSize: string, student?: string, country?: string, city?: string, ongoingCourses?: string, previousCourses?: string, options?: AxiosRequestConfig) {\n        return StudentsApiFp(this.configuration).getUserStudents(current, pageSize, student, country, city, ongoingCourses, previousCourses, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * StudentsFeedbacksApi - axios parameter creator\n * @export\n */\nexport const StudentsFeedbacksApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} studentId \n         * @param {CreateStudentFeedbackDto} createStudentFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createStudentFeedback: async (studentId: number, createStudentFeedbackDto: CreateStudentFeedbackDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'studentId' is not null or undefined\n            assertParamExists('createStudentFeedback', 'studentId', studentId)\n            // verify required parameter 'createStudentFeedbackDto' is not null or undefined\n            assertParamExists('createStudentFeedback', 'createStudentFeedbackDto', createStudentFeedbackDto)\n            const localVarPath = `/students/{studentId}/feedbacks`\n                .replace(`{${\"studentId\"}}`, encodeURIComponent(String(studentId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createStudentFeedbackDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentFeedback: async (studentId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'studentId' is not null or undefined\n            assertParamExists('getStudentFeedback', 'studentId', studentId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('getStudentFeedback', 'id', id)\n            const localVarPath = `/students/{studentId}/feedbacks/{id}`\n                .replace(`{${\"studentId\"}}`, encodeURIComponent(String(studentId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} id \n         * @param {UpdateStudentFeedbackDto} updateStudentFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateStudentFeedback: async (studentId: number, id: number, updateStudentFeedbackDto: UpdateStudentFeedbackDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'studentId' is not null or undefined\n            assertParamExists('updateStudentFeedback', 'studentId', studentId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateStudentFeedback', 'id', id)\n            // verify required parameter 'updateStudentFeedbackDto' is not null or undefined\n            assertParamExists('updateStudentFeedback', 'updateStudentFeedbackDto', updateStudentFeedbackDto)\n            const localVarPath = `/students/{studentId}/feedbacks/{id}`\n                .replace(`{${\"studentId\"}}`, encodeURIComponent(String(studentId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateStudentFeedbackDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * StudentsFeedbacksApi - functional programming interface\n * @export\n */\nexport const StudentsFeedbacksApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = StudentsFeedbacksApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} studentId \n         * @param {CreateStudentFeedbackDto} createStudentFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createStudentFeedback(studentId: number, createStudentFeedbackDto: CreateStudentFeedbackDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StudentFeedbackDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createStudentFeedback(studentId, createStudentFeedbackDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getStudentFeedback(studentId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StudentFeedbackDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentFeedback(studentId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} id \n         * @param {UpdateStudentFeedbackDto} updateStudentFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateStudentFeedback(studentId: number, id: number, updateStudentFeedbackDto: UpdateStudentFeedbackDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<StudentFeedbackDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateStudentFeedback(studentId, id, updateStudentFeedbackDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * StudentsFeedbacksApi - factory interface\n * @export\n */\nexport const StudentsFeedbacksApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = StudentsFeedbacksApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} studentId \n         * @param {CreateStudentFeedbackDto} createStudentFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createStudentFeedback(studentId: number, createStudentFeedbackDto: CreateStudentFeedbackDto, options?: any): AxiosPromise<StudentFeedbackDto> {\n            return localVarFp.createStudentFeedback(studentId, createStudentFeedbackDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentFeedback(studentId: number, id: number, options?: any): AxiosPromise<StudentFeedbackDto> {\n            return localVarFp.getStudentFeedback(studentId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} id \n         * @param {UpdateStudentFeedbackDto} updateStudentFeedbackDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateStudentFeedback(studentId: number, id: number, updateStudentFeedbackDto: UpdateStudentFeedbackDto, options?: any): AxiosPromise<StudentFeedbackDto> {\n            return localVarFp.updateStudentFeedback(studentId, id, updateStudentFeedbackDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * StudentsFeedbacksApi - object-oriented interface\n * @export\n * @class StudentsFeedbacksApi\n * @extends {BaseAPI}\n */\nexport class StudentsFeedbacksApi extends BaseAPI {\n    /**\n     * \n     * @param {number} studentId \n     * @param {CreateStudentFeedbackDto} createStudentFeedbackDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsFeedbacksApi\n     */\n    public createStudentFeedback(studentId: number, createStudentFeedbackDto: CreateStudentFeedbackDto, options?: AxiosRequestConfig) {\n        return StudentsFeedbacksApiFp(this.configuration).createStudentFeedback(studentId, createStudentFeedbackDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} studentId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsFeedbacksApi\n     */\n    public getStudentFeedback(studentId: number, id: number, options?: AxiosRequestConfig) {\n        return StudentsFeedbacksApiFp(this.configuration).getStudentFeedback(studentId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} studentId \n     * @param {number} id \n     * @param {UpdateStudentFeedbackDto} updateStudentFeedbackDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsFeedbacksApi\n     */\n    public updateStudentFeedback(studentId: number, id: number, updateStudentFeedbackDto: UpdateStudentFeedbackDto, options?: AxiosRequestConfig) {\n        return StudentsFeedbacksApiFp(this.configuration).updateStudentFeedback(studentId, id, updateStudentFeedbackDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * StudentsScoreApi - axios parameter creator\n * @export\n */\nexport const StudentsScoreApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} activeOnly \n         * @param {'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate'} orderBy \n         * @param {'asc' | 'desc'} orderDirection \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {number} courseId \n         * @param {string} [githubId] \n         * @param {string} [name] \n         * @param {string} [mentorGithubId] \n         * @param {string} [cityName] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getScore: async (activeOnly: string, orderBy: 'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate', orderDirection: 'asc' | 'desc', current: string, pageSize: string, courseId: number, githubId?: string, name?: string, mentorGithubId?: string, cityName?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'activeOnly' is not null or undefined\n            assertParamExists('getScore', 'activeOnly', activeOnly)\n            // verify required parameter 'orderBy' is not null or undefined\n            assertParamExists('getScore', 'orderBy', orderBy)\n            // verify required parameter 'orderDirection' is not null or undefined\n            assertParamExists('getScore', 'orderDirection', orderDirection)\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getScore', 'current', current)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getScore', 'pageSize', pageSize)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getScore', 'courseId', courseId)\n            const localVarPath = `/course/{courseId}/students/score`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (activeOnly !== undefined) {\n                localVarQueryParameter['activeOnly'] = activeOnly;\n            }\n\n            if (orderBy !== undefined) {\n                localVarQueryParameter['orderBy'] = orderBy;\n            }\n\n            if (orderDirection !== undefined) {\n                localVarQueryParameter['orderDirection'] = orderDirection;\n            }\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (githubId !== undefined) {\n                localVarQueryParameter['githubId'] = githubId;\n            }\n\n            if (name !== undefined) {\n                localVarQueryParameter['name'] = name;\n            }\n\n            if (mentorGithubId !== undefined) {\n                localVarQueryParameter['mentor.githubId'] = mentorGithubId;\n            }\n\n            if (cityName !== undefined) {\n                localVarQueryParameter['cityName'] = cityName;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentScore: async (courseId: number, githubId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getStudentScore', 'courseId', courseId)\n            // verify required parameter 'githubId' is not null or undefined\n            assertParamExists('getStudentScore', 'githubId', githubId)\n            const localVarPath = `/course/{courseId}/students/score/{githubId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"githubId\"}}`, encodeURIComponent(String(githubId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * StudentsScoreApi - functional programming interface\n * @export\n */\nexport const StudentsScoreApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = StudentsScoreApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} activeOnly \n         * @param {'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate'} orderBy \n         * @param {'asc' | 'desc'} orderDirection \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {number} courseId \n         * @param {string} [githubId] \n         * @param {string} [name] \n         * @param {string} [mentorGithubId] \n         * @param {string} [cityName] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getScore(activeOnly: string, orderBy: 'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate', orderDirection: 'asc' | 'desc', current: string, pageSize: string, courseId: number, githubId?: string, name?: string, mentorGithubId?: string, cityName?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ScoreDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getScore(activeOnly, orderBy, orderDirection, current, pageSize, courseId, githubId, name, mentorGithubId, cityName, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getStudentScore(courseId: number, githubId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<ScoreStudentDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentScore(courseId, githubId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * StudentsScoreApi - factory interface\n * @export\n */\nexport const StudentsScoreApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = StudentsScoreApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {string} activeOnly \n         * @param {'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate'} orderBy \n         * @param {'asc' | 'desc'} orderDirection \n         * @param {string} current \n         * @param {string} pageSize \n         * @param {number} courseId \n         * @param {string} [githubId] \n         * @param {string} [name] \n         * @param {string} [mentorGithubId] \n         * @param {string} [cityName] \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getScore(activeOnly: string, orderBy: 'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate', orderDirection: 'asc' | 'desc', current: string, pageSize: string, courseId: number, githubId?: string, name?: string, mentorGithubId?: string, cityName?: string, options?: any): AxiosPromise<ScoreDto> {\n            return localVarFp.getScore(activeOnly, orderBy, orderDirection, current, pageSize, courseId, githubId, name, mentorGithubId, cityName, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {string} githubId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentScore(courseId: number, githubId: string, options?: any): AxiosPromise<ScoreStudentDto> {\n            return localVarFp.getStudentScore(courseId, githubId, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * StudentsScoreApi - object-oriented interface\n * @export\n * @class StudentsScoreApi\n * @extends {BaseAPI}\n */\nexport class StudentsScoreApi extends BaseAPI {\n    /**\n     * \n     * @param {string} activeOnly \n     * @param {'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate'} orderBy \n     * @param {'asc' | 'desc'} orderDirection \n     * @param {string} current \n     * @param {string} pageSize \n     * @param {number} courseId \n     * @param {string} [githubId] \n     * @param {string} [name] \n     * @param {string} [mentorGithubId] \n     * @param {string} [cityName] \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsScoreApi\n     */\n    public getScore(activeOnly: string, orderBy: 'rank' | 'totalScore' | 'crossCheckScore' | 'githubId' | 'name' | 'cityName' | 'mentor' | 'totalScoreChangeDate' | 'repositoryLastActivityDate', orderDirection: 'asc' | 'desc', current: string, pageSize: string, courseId: number, githubId?: string, name?: string, mentorGithubId?: string, cityName?: string, options?: AxiosRequestConfig) {\n        return StudentsScoreApiFp(this.configuration).getScore(activeOnly, orderBy, orderDirection, current, pageSize, courseId, githubId, name, mentorGithubId, cityName, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {string} githubId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof StudentsScoreApi\n     */\n    public getStudentScore(courseId: number, githubId: string, options?: AxiosRequestConfig) {\n        return StudentsScoreApiFp(this.configuration).getStudentScore(courseId, githubId, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * TasksApi - axios parameter creator\n * @export\n */\nexport const TasksApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateTaskDto} createTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTask: async (createTaskDto: CreateTaskDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createTaskDto' is not null or undefined\n            assertParamExists('createTask', 'createTaskDto', createTaskDto)\n            const localVarPath = `/tasks`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createTaskDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTask: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteTask', 'id', id)\n            const localVarPath = `/tasks/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTasks: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/tasks`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateTaskDto} updateTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTask: async (id: number, updateTaskDto: UpdateTaskDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateTask', 'id', id)\n            // verify required parameter 'updateTaskDto' is not null or undefined\n            assertParamExists('updateTask', 'updateTaskDto', updateTaskDto)\n            const localVarPath = `/tasks/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateTaskDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TasksApi - functional programming interface\n * @export\n */\nexport const TasksApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TasksApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateTaskDto} createTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTask(createTaskDto: CreateTaskDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTask(createTaskDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteTask(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTask(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTasks(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TaskDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTasks(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateTaskDto} updateTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateTask(id: number, updateTaskDto: UpdateTaskDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateTask(id, updateTaskDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * TasksApi - factory interface\n * @export\n */\nexport const TasksApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TasksApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateTaskDto} createTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTask(createTaskDto: CreateTaskDto, options?: any): AxiosPromise<TaskDto> {\n            return localVarFp.createTask(createTaskDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTask(id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteTask(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTasks(options?: any): AxiosPromise<Array<TaskDto>> {\n            return localVarFp.getTasks(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateTaskDto} updateTaskDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTask(id: number, updateTaskDto: UpdateTaskDto, options?: any): AxiosPromise<TaskDto> {\n            return localVarFp.updateTask(id, updateTaskDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * TasksApi - object-oriented interface\n * @export\n * @class TasksApi\n * @extends {BaseAPI}\n */\nexport class TasksApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateTaskDto} createTaskDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TasksApi\n     */\n    public createTask(createTaskDto: CreateTaskDto, options?: AxiosRequestConfig) {\n        return TasksApiFp(this.configuration).createTask(createTaskDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TasksApi\n     */\n    public deleteTask(id: number, options?: AxiosRequestConfig) {\n        return TasksApiFp(this.configuration).deleteTask(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TasksApi\n     */\n    public getTasks(options?: AxiosRequestConfig) {\n        return TasksApiFp(this.configuration).getTasks(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {UpdateTaskDto} updateTaskDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TasksApi\n     */\n    public updateTask(id: number, updateTaskDto: UpdateTaskDto, options?: AxiosRequestConfig) {\n        return TasksApiFp(this.configuration).updateTask(id, updateTaskDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * TasksCriteriaApi - axios parameter creator\n * @export\n */\nexport const TasksCriteriaApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} taskId \n         * @param {TaskCriteriaDto} taskCriteriaDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTaskCriteria: async (taskId: number, taskCriteriaDto: TaskCriteriaDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'taskId' is not null or undefined\n            assertParamExists('createTaskCriteria', 'taskId', taskId)\n            // verify required parameter 'taskCriteriaDto' is not null or undefined\n            assertParamExists('createTaskCriteria', 'taskCriteriaDto', taskCriteriaDto)\n            const localVarPath = `/tasks/{taskId}/criteria`\n                .replace(`{${\"taskId\"}}`, encodeURIComponent(String(taskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(taskCriteriaDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTaskCriteria: async (taskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'taskId' is not null or undefined\n            assertParamExists('getTaskCriteria', 'taskId', taskId)\n            const localVarPath = `/tasks/{taskId}/criteria`\n                .replace(`{${\"taskId\"}}`, encodeURIComponent(String(taskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} taskId \n         * @param {TaskCriteriaDto} taskCriteriaDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTaskCriteria: async (taskId: number, taskCriteriaDto: TaskCriteriaDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'taskId' is not null or undefined\n            assertParamExists('updateTaskCriteria', 'taskId', taskId)\n            // verify required parameter 'taskCriteriaDto' is not null or undefined\n            assertParamExists('updateTaskCriteria', 'taskCriteriaDto', taskCriteriaDto)\n            const localVarPath = `/tasks/{taskId}/criteria`\n                .replace(`{${\"taskId\"}}`, encodeURIComponent(String(taskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(taskCriteriaDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TasksCriteriaApi - functional programming interface\n * @export\n */\nexport const TasksCriteriaApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TasksCriteriaApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} taskId \n         * @param {TaskCriteriaDto} taskCriteriaDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTaskCriteria(taskId: number, taskCriteriaDto: TaskCriteriaDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskCriteriaDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTaskCriteria(taskId, taskCriteriaDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTaskCriteria(taskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskCriteriaDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTaskCriteria(taskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} taskId \n         * @param {TaskCriteriaDto} taskCriteriaDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateTaskCriteria(taskId: number, taskCriteriaDto: TaskCriteriaDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TaskCriteriaDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateTaskCriteria(taskId, taskCriteriaDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * TasksCriteriaApi - factory interface\n * @export\n */\nexport const TasksCriteriaApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TasksCriteriaApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} taskId \n         * @param {TaskCriteriaDto} taskCriteriaDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTaskCriteria(taskId: number, taskCriteriaDto: TaskCriteriaDto, options?: any): AxiosPromise<TaskCriteriaDto> {\n            return localVarFp.createTaskCriteria(taskId, taskCriteriaDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTaskCriteria(taskId: number, options?: any): AxiosPromise<TaskCriteriaDto> {\n            return localVarFp.getTaskCriteria(taskId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} taskId \n         * @param {TaskCriteriaDto} taskCriteriaDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTaskCriteria(taskId: number, taskCriteriaDto: TaskCriteriaDto, options?: any): AxiosPromise<TaskCriteriaDto> {\n            return localVarFp.updateTaskCriteria(taskId, taskCriteriaDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * TasksCriteriaApi - object-oriented interface\n * @export\n * @class TasksCriteriaApi\n * @extends {BaseAPI}\n */\nexport class TasksCriteriaApi extends BaseAPI {\n    /**\n     * \n     * @param {number} taskId \n     * @param {TaskCriteriaDto} taskCriteriaDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TasksCriteriaApi\n     */\n    public createTaskCriteria(taskId: number, taskCriteriaDto: TaskCriteriaDto, options?: AxiosRequestConfig) {\n        return TasksCriteriaApiFp(this.configuration).createTaskCriteria(taskId, taskCriteriaDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} taskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TasksCriteriaApi\n     */\n    public getTaskCriteria(taskId: number, options?: AxiosRequestConfig) {\n        return TasksCriteriaApiFp(this.configuration).getTaskCriteria(taskId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} taskId \n     * @param {TaskCriteriaDto} taskCriteriaDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TasksCriteriaApi\n     */\n    public updateTaskCriteria(taskId: number, taskCriteriaDto: TaskCriteriaDto, options?: AxiosRequestConfig) {\n        return TasksCriteriaApiFp(this.configuration).updateTaskCriteria(taskId, taskCriteriaDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * TeamApi - axios parameter creator\n * @export\n */\nexport const TeamApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changeTeamPassword: async (courseId: number, distributionId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('changeTeamPassword', 'courseId', courseId)\n            // verify required parameter 'distributionId' is not null or undefined\n            assertParamExists('changeTeamPassword', 'distributionId', distributionId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('changeTeamPassword', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{distributionId}/team/{id}/password`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"distributionId\"}}`, encodeURIComponent(String(distributionId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {CreateTeamDto} createTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTeam: async (courseId: number, distributionId: number, createTeamDto: CreateTeamDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('createTeam', 'courseId', courseId)\n            // verify required parameter 'distributionId' is not null or undefined\n            assertParamExists('createTeam', 'distributionId', distributionId)\n            // verify required parameter 'createTeamDto' is not null or undefined\n            assertParamExists('createTeam', 'createTeamDto', createTeamDto)\n            const localVarPath = `/courses/{courseId}/team-distribution/{distributionId}/team`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"distributionId\"}}`, encodeURIComponent(String(distributionId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createTeamDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTeamPassword: async (courseId: number, distributionId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getTeamPassword', 'courseId', courseId)\n            // verify required parameter 'distributionId' is not null or undefined\n            assertParamExists('getTeamPassword', 'distributionId', distributionId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('getTeamPassword', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{distributionId}/team/{id}/password`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"distributionId\"}}`, encodeURIComponent(String(distributionId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} search \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTeams: async (courseId: number, distributionId: number, pageSize: number, current: number, search: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getTeams', 'courseId', courseId)\n            // verify required parameter 'distributionId' is not null or undefined\n            assertParamExists('getTeams', 'distributionId', distributionId)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getTeams', 'pageSize', pageSize)\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getTeams', 'current', current)\n            // verify required parameter 'search' is not null or undefined\n            assertParamExists('getTeams', 'search', search)\n            const localVarPath = `/courses/{courseId}/team-distribution/{distributionId}/team`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"distributionId\"}}`, encodeURIComponent(String(distributionId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (search !== undefined) {\n                localVarQueryParameter['search'] = search;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {JoinTeamDto} joinTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        joinTeam: async (courseId: number, distributionId: number, id: number, joinTeamDto: JoinTeamDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('joinTeam', 'courseId', courseId)\n            // verify required parameter 'distributionId' is not null or undefined\n            assertParamExists('joinTeam', 'distributionId', distributionId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('joinTeam', 'id', id)\n            // verify required parameter 'joinTeamDto' is not null or undefined\n            assertParamExists('joinTeam', 'joinTeamDto', joinTeamDto)\n            const localVarPath = `/courses/{courseId}/team-distribution/{distributionId}/team/{id}/join`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"distributionId\"}}`, encodeURIComponent(String(distributionId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(joinTeamDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        leaveTeam: async (courseId: number, distributionId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('leaveTeam', 'courseId', courseId)\n            // verify required parameter 'distributionId' is not null or undefined\n            assertParamExists('leaveTeam', 'distributionId', distributionId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('leaveTeam', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{distributionId}/team/{id}/leave`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"distributionId\"}}`, encodeURIComponent(String(distributionId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {UpdateTeamDto} updateTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTeam: async (courseId: number, distributionId: number, id: number, updateTeamDto: UpdateTeamDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('updateTeam', 'courseId', courseId)\n            // verify required parameter 'distributionId' is not null or undefined\n            assertParamExists('updateTeam', 'distributionId', distributionId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateTeam', 'id', id)\n            // verify required parameter 'updateTeamDto' is not null or undefined\n            assertParamExists('updateTeam', 'updateTeamDto', updateTeamDto)\n            const localVarPath = `/courses/{courseId}/team-distribution/{distributionId}/team/{id}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"distributionId\"}}`, encodeURIComponent(String(distributionId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateTeamDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TeamApi - functional programming interface\n * @export\n */\nexport const TeamApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TeamApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async changeTeamPassword(courseId: number, distributionId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamPasswordDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.changeTeamPassword(courseId, distributionId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {CreateTeamDto} createTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTeam(courseId: number, distributionId: number, createTeamDto: CreateTeamDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTeam(courseId, distributionId, createTeamDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTeamPassword(courseId: number, distributionId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamPasswordDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTeamPassword(courseId, distributionId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} search \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getTeams(courseId: number, distributionId: number, pageSize: number, current: number, search: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getTeams(courseId, distributionId, pageSize, current, search, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {JoinTeamDto} joinTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async joinTeam(courseId: number, distributionId: number, id: number, joinTeamDto: JoinTeamDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamInfoDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.joinTeam(courseId, distributionId, id, joinTeamDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async leaveTeam(courseId: number, distributionId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.leaveTeam(courseId, distributionId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {UpdateTeamDto} updateTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateTeam(courseId: number, distributionId: number, id: number, updateTeamDto: UpdateTeamDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateTeam(courseId, distributionId, id, updateTeamDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * TeamApi - factory interface\n * @export\n */\nexport const TeamApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TeamApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        changeTeamPassword(courseId: number, distributionId: number, id: number, options?: any): AxiosPromise<TeamPasswordDto> {\n            return localVarFp.changeTeamPassword(courseId, distributionId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {CreateTeamDto} createTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTeam(courseId: number, distributionId: number, createTeamDto: CreateTeamDto, options?: any): AxiosPromise<TeamDto> {\n            return localVarFp.createTeam(courseId, distributionId, createTeamDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTeamPassword(courseId: number, distributionId: number, id: number, options?: any): AxiosPromise<TeamPasswordDto> {\n            return localVarFp.getTeamPassword(courseId, distributionId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} search \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getTeams(courseId: number, distributionId: number, pageSize: number, current: number, search: string, options?: any): AxiosPromise<TeamsDto> {\n            return localVarFp.getTeams(courseId, distributionId, pageSize, current, search, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {JoinTeamDto} joinTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        joinTeam(courseId: number, distributionId: number, id: number, joinTeamDto: JoinTeamDto, options?: any): AxiosPromise<TeamInfoDto> {\n            return localVarFp.joinTeam(courseId, distributionId, id, joinTeamDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        leaveTeam(courseId: number, distributionId: number, id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.leaveTeam(courseId, distributionId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} distributionId \n         * @param {number} id \n         * @param {UpdateTeamDto} updateTeamDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTeam(courseId: number, distributionId: number, id: number, updateTeamDto: UpdateTeamDto, options?: any): AxiosPromise<void> {\n            return localVarFp.updateTeam(courseId, distributionId, id, updateTeamDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * TeamApi - object-oriented interface\n * @export\n * @class TeamApi\n * @extends {BaseAPI}\n */\nexport class TeamApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} distributionId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamApi\n     */\n    public changeTeamPassword(courseId: number, distributionId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamApiFp(this.configuration).changeTeamPassword(courseId, distributionId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} distributionId \n     * @param {CreateTeamDto} createTeamDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamApi\n     */\n    public createTeam(courseId: number, distributionId: number, createTeamDto: CreateTeamDto, options?: AxiosRequestConfig) {\n        return TeamApiFp(this.configuration).createTeam(courseId, distributionId, createTeamDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} distributionId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamApi\n     */\n    public getTeamPassword(courseId: number, distributionId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamApiFp(this.configuration).getTeamPassword(courseId, distributionId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} distributionId \n     * @param {number} pageSize \n     * @param {number} current \n     * @param {string} search \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamApi\n     */\n    public getTeams(courseId: number, distributionId: number, pageSize: number, current: number, search: string, options?: AxiosRequestConfig) {\n        return TeamApiFp(this.configuration).getTeams(courseId, distributionId, pageSize, current, search, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} distributionId \n     * @param {number} id \n     * @param {JoinTeamDto} joinTeamDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamApi\n     */\n    public joinTeam(courseId: number, distributionId: number, id: number, joinTeamDto: JoinTeamDto, options?: AxiosRequestConfig) {\n        return TeamApiFp(this.configuration).joinTeam(courseId, distributionId, id, joinTeamDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} distributionId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamApi\n     */\n    public leaveTeam(courseId: number, distributionId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamApiFp(this.configuration).leaveTeam(courseId, distributionId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} distributionId \n     * @param {number} id \n     * @param {UpdateTeamDto} updateTeamDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamApi\n     */\n    public updateTeam(courseId: number, distributionId: number, id: number, updateTeamDto: UpdateTeamDto, options?: AxiosRequestConfig) {\n        return TeamApiFp(this.configuration).updateTeam(courseId, distributionId, id, updateTeamDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * TeamDistributionApi - axios parameter creator\n * @export\n */\nexport const TeamDistributionApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateTeamDistributionDto} createTeamDistributionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTeamDistribution: async (courseId: number, createTeamDistributionDto: CreateTeamDistributionDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('createTeamDistribution', 'courseId', courseId)\n            // verify required parameter 'createTeamDistributionDto' is not null or undefined\n            assertParamExists('createTeamDistribution', 'createTeamDistributionDto', createTeamDistributionDto)\n            const localVarPath = `/courses/{courseId}/team-distribution`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createTeamDistributionDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTeamDistribution: async (courseId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('deleteTeamDistribution', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteTeamDistribution', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        distributeStudentsToTeam: async (courseId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('distributeStudentsToTeam', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('distributeStudentsToTeam', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}/distribution`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTeamDistributionDetailed: async (courseId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseTeamDistributionDetailed', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('getCourseTeamDistributionDetailed', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}/detailed`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTeamDistributions: async (courseId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getCourseTeamDistributions', 'courseId', courseId)\n            const localVarPath = `/courses/{courseId}/team-distribution`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} search \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentsWithoutTeam: async (courseId: number, id: number, pageSize: number, current: number, search: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('getStudentsWithoutTeam', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('getStudentsWithoutTeam', 'id', id)\n            // verify required parameter 'pageSize' is not null or undefined\n            assertParamExists('getStudentsWithoutTeam', 'pageSize', pageSize)\n            // verify required parameter 'current' is not null or undefined\n            assertParamExists('getStudentsWithoutTeam', 'current', current)\n            // verify required parameter 'search' is not null or undefined\n            assertParamExists('getStudentsWithoutTeam', 'search', search)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}/students`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (pageSize !== undefined) {\n                localVarQueryParameter['pageSize'] = pageSize;\n            }\n\n            if (current !== undefined) {\n                localVarQueryParameter['current'] = current;\n            }\n\n            if (search !== undefined) {\n                localVarQueryParameter['search'] = search;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        submitScore: async (courseId: number, id: number, taskId: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('submitScore', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('submitScore', 'id', id)\n            // verify required parameter 'taskId' is not null or undefined\n            assertParamExists('submitScore', 'taskId', taskId)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}/submit-score/{taskId}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)))\n                .replace(`{${\"taskId\"}}`, encodeURIComponent(String(taskId)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        teamDistributionControllerDeleteStudentFromDistribution: async (studentId: number, courseId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'studentId' is not null or undefined\n            assertParamExists('teamDistributionControllerDeleteStudentFromDistribution', 'studentId', studentId)\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('teamDistributionControllerDeleteStudentFromDistribution', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('teamDistributionControllerDeleteStudentFromDistribution', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}/students/{studentId}`\n                .replace(`{${\"studentId\"}}`, encodeURIComponent(String(studentId)))\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        teamDistributionDeleteRegistry: async (courseId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('teamDistributionDeleteRegistry', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('teamDistributionDeleteRegistry', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}/registry`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        teamDistributionRegistry: async (courseId: number, id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('teamDistributionRegistry', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('teamDistributionRegistry', 'id', id)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}/registry`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {UpdateTeamDistributionDto} updateTeamDistributionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTeamDistribution: async (courseId: number, id: number, updateTeamDistributionDto: UpdateTeamDistributionDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'courseId' is not null or undefined\n            assertParamExists('updateTeamDistribution', 'courseId', courseId)\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateTeamDistribution', 'id', id)\n            // verify required parameter 'updateTeamDistributionDto' is not null or undefined\n            assertParamExists('updateTeamDistribution', 'updateTeamDistributionDto', updateTeamDistributionDto)\n            const localVarPath = `/courses/{courseId}/team-distribution/{id}`\n                .replace(`{${\"courseId\"}}`, encodeURIComponent(String(courseId)))\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateTeamDistributionDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * TeamDistributionApi - functional programming interface\n * @export\n */\nexport const TeamDistributionApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = TeamDistributionApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateTeamDistributionDto} createTeamDistributionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createTeamDistribution(courseId: number, createTeamDistributionDto: CreateTeamDistributionDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamDistributionDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createTeamDistribution(courseId, createTeamDistributionDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteTeamDistribution(courseId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteTeamDistribution(courseId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async distributeStudentsToTeam(courseId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.distributeStudentsToTeam(courseId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseTeamDistributionDetailed(courseId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamDistributionDetailedDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseTeamDistributionDetailed(courseId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getCourseTeamDistributions(courseId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TeamDistributionDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getCourseTeamDistributions(courseId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} search \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getStudentsWithoutTeam(courseId: number, id: number, pageSize: number, current: number, search: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TeamDistributionStudentDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getStudentsWithoutTeam(courseId, id, pageSize, current, search, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async submitScore(courseId: number, id: number, taskId: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamDistributionDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.submitScore(courseId, id, taskId, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async teamDistributionControllerDeleteStudentFromDistribution(studentId: number, courseId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.teamDistributionControllerDeleteStudentFromDistribution(studentId, courseId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async teamDistributionDeleteRegistry(courseId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.teamDistributionDeleteRegistry(courseId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async teamDistributionRegistry(courseId: number, id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TeamDistributionDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.teamDistributionRegistry(courseId, id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {UpdateTeamDistributionDto} updateTeamDistributionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateTeamDistribution(courseId: number, id: number, updateTeamDistributionDto: UpdateTeamDistributionDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateTeamDistribution(courseId, id, updateTeamDistributionDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * TeamDistributionApi - factory interface\n * @export\n */\nexport const TeamDistributionApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = TeamDistributionApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {number} courseId \n         * @param {CreateTeamDistributionDto} createTeamDistributionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createTeamDistribution(courseId: number, createTeamDistributionDto: CreateTeamDistributionDto, options?: any): AxiosPromise<TeamDistributionDto> {\n            return localVarFp.createTeamDistribution(courseId, createTeamDistributionDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteTeamDistribution(courseId: number, id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.deleteTeamDistribution(courseId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        distributeStudentsToTeam(courseId: number, id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.distributeStudentsToTeam(courseId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTeamDistributionDetailed(courseId: number, id: number, options?: any): AxiosPromise<TeamDistributionDetailedDto> {\n            return localVarFp.getCourseTeamDistributionDetailed(courseId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getCourseTeamDistributions(courseId: number, options?: any): AxiosPromise<Array<TeamDistributionDto>> {\n            return localVarFp.getCourseTeamDistributions(courseId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {number} pageSize \n         * @param {number} current \n         * @param {string} search \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getStudentsWithoutTeam(courseId: number, id: number, pageSize: number, current: number, search: string, options?: any): AxiosPromise<Array<TeamDistributionStudentDto>> {\n            return localVarFp.getStudentsWithoutTeam(courseId, id, pageSize, current, search, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {number} taskId \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        submitScore(courseId: number, id: number, taskId: number, options?: any): AxiosPromise<TeamDistributionDto> {\n            return localVarFp.submitScore(courseId, id, taskId, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} studentId \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        teamDistributionControllerDeleteStudentFromDistribution(studentId: number, courseId: number, id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.teamDistributionControllerDeleteStudentFromDistribution(studentId, courseId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        teamDistributionDeleteRegistry(courseId: number, id: number, options?: any): AxiosPromise<void> {\n            return localVarFp.teamDistributionDeleteRegistry(courseId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        teamDistributionRegistry(courseId: number, id: number, options?: any): AxiosPromise<TeamDistributionDto> {\n            return localVarFp.teamDistributionRegistry(courseId, id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} courseId \n         * @param {number} id \n         * @param {UpdateTeamDistributionDto} updateTeamDistributionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateTeamDistribution(courseId: number, id: number, updateTeamDistributionDto: UpdateTeamDistributionDto, options?: any): AxiosPromise<void> {\n            return localVarFp.updateTeamDistribution(courseId, id, updateTeamDistributionDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * TeamDistributionApi - object-oriented interface\n * @export\n * @class TeamDistributionApi\n * @extends {BaseAPI}\n */\nexport class TeamDistributionApi extends BaseAPI {\n    /**\n     * \n     * @param {number} courseId \n     * @param {CreateTeamDistributionDto} createTeamDistributionDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public createTeamDistribution(courseId: number, createTeamDistributionDto: CreateTeamDistributionDto, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).createTeamDistribution(courseId, createTeamDistributionDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public deleteTeamDistribution(courseId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).deleteTeamDistribution(courseId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public distributeStudentsToTeam(courseId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).distributeStudentsToTeam(courseId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public getCourseTeamDistributionDetailed(courseId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).getCourseTeamDistributionDetailed(courseId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public getCourseTeamDistributions(courseId: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).getCourseTeamDistributions(courseId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {number} pageSize \n     * @param {number} current \n     * @param {string} search \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public getStudentsWithoutTeam(courseId: number, id: number, pageSize: number, current: number, search: string, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).getStudentsWithoutTeam(courseId, id, pageSize, current, search, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {number} taskId \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public submitScore(courseId: number, id: number, taskId: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).submitScore(courseId, id, taskId, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} studentId \n     * @param {number} courseId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public teamDistributionControllerDeleteStudentFromDistribution(studentId: number, courseId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).teamDistributionControllerDeleteStudentFromDistribution(studentId, courseId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public teamDistributionDeleteRegistry(courseId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).teamDistributionDeleteRegistry(courseId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public teamDistributionRegistry(courseId: number, id: number, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).teamDistributionRegistry(courseId, id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} courseId \n     * @param {number} id \n     * @param {UpdateTeamDistributionDto} updateTeamDistributionDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof TeamDistributionApi\n     */\n    public updateTeamDistribution(courseId: number, id: number, updateTeamDistributionDto: UpdateTeamDistributionDto, options?: AxiosRequestConfig) {\n        return TeamDistributionApiFp(this.configuration).updateTeamDistribution(courseId, id, updateTeamDistributionDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * UserGroupApi - axios parameter creator\n * @export\n */\nexport const UserGroupApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {CreateUserGroupDto} createUserGroupDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUserGroup: async (createUserGroupDto: CreateUserGroupDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'createUserGroupDto' is not null or undefined\n            assertParamExists('createUserGroup', 'createUserGroupDto', createUserGroupDto)\n            const localVarPath = `/user-group`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(createUserGroupDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUserGroup: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('deleteUserGroup', 'id', id)\n            const localVarPath = `/user-group/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserGroups: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/user-group`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateUserGroupDto} updateUserGroupDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUserGroup: async (id: number, updateUserGroupDto: UpdateUserGroupDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'id' is not null or undefined\n            assertParamExists('updateUserGroup', 'id', id)\n            // verify required parameter 'updateUserGroupDto' is not null or undefined\n            assertParamExists('updateUserGroup', 'updateUserGroupDto', updateUserGroupDto)\n            const localVarPath = `/user-group/{id}`\n                .replace(`{${\"id\"}}`, encodeURIComponent(String(id)));\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateUserGroupDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UserGroupApi - functional programming interface\n * @export\n */\nexport const UserGroupApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UserGroupApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {CreateUserGroupDto} createUserGroupDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async createUserGroup(createUserGroupDto: CreateUserGroupDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserGroupDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.createUserGroup(createUserGroupDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async deleteUserGroup(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserGroupDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.deleteUserGroup(id, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUserGroups(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserGroupDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserGroups(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateUserGroupDto} updateUserGroupDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateUserGroup(id: number, updateUserGroupDto: UpdateUserGroupDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserGroupDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateUserGroup(id, updateUserGroupDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * UserGroupApi - factory interface\n * @export\n */\nexport const UserGroupApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UserGroupApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {CreateUserGroupDto} createUserGroupDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        createUserGroup(createUserGroupDto: CreateUserGroupDto, options?: any): AxiosPromise<UserGroupDto> {\n            return localVarFp.createUserGroup(createUserGroupDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        deleteUserGroup(id: number, options?: any): AxiosPromise<UserGroupDto> {\n            return localVarFp.deleteUserGroup(id, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserGroups(options?: any): AxiosPromise<Array<UserGroupDto>> {\n            return localVarFp.getUserGroups(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {number} id \n         * @param {UpdateUserGroupDto} updateUserGroupDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUserGroup(id: number, updateUserGroupDto: UpdateUserGroupDto, options?: any): AxiosPromise<UserGroupDto> {\n            return localVarFp.updateUserGroup(id, updateUserGroupDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * UserGroupApi - object-oriented interface\n * @export\n * @class UserGroupApi\n * @extends {BaseAPI}\n */\nexport class UserGroupApi extends BaseAPI {\n    /**\n     * \n     * @param {CreateUserGroupDto} createUserGroupDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UserGroupApi\n     */\n    public createUserGroup(createUserGroupDto: CreateUserGroupDto, options?: AxiosRequestConfig) {\n        return UserGroupApiFp(this.configuration).createUserGroup(createUserGroupDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UserGroupApi\n     */\n    public deleteUserGroup(id: number, options?: AxiosRequestConfig) {\n        return UserGroupApiFp(this.configuration).deleteUserGroup(id, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UserGroupApi\n     */\n    public getUserGroups(options?: AxiosRequestConfig) {\n        return UserGroupApiFp(this.configuration).getUserGroups(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {number} id \n     * @param {UpdateUserGroupDto} updateUserGroupDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UserGroupApi\n     */\n    public updateUserGroup(id: number, updateUserGroupDto: UpdateUserGroupDto, options?: AxiosRequestConfig) {\n        return UserGroupApiFp(this.configuration).updateUserGroup(id, updateUserGroupDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * UsersApi - axios parameter creator\n * @export\n */\nexport const UsersApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {string} query \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        searchUsers: async (query: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'query' is not null or undefined\n            assertParamExists('searchUsers', 'query', query)\n            const localVarPath = `/users/search`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n            if (query !== undefined) {\n                localVarQueryParameter['query'] = query;\n            }\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UsersApi - functional programming interface\n * @export\n */\nexport const UsersApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UsersApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {string} query \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async searchUsers(query: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UserSearchDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.searchUsers(query, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * UsersApi - factory interface\n * @export\n */\nexport const UsersApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UsersApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {string} query \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        searchUsers(query: string, options?: any): AxiosPromise<Array<UserSearchDto>> {\n            return localVarFp.searchUsers(query, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * UsersApi - object-oriented interface\n * @export\n * @class UsersApi\n * @extends {BaseAPI}\n */\nexport class UsersApi extends BaseAPI {\n    /**\n     * \n     * @param {string} query \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersApi\n     */\n    public searchUsers(query: string, options?: AxiosRequestConfig) {\n        return UsersApiFp(this.configuration).searchUsers(query, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n/**\n * UsersNotificationsApi - axios parameter creator\n * @export\n */\nexport const UsersNotificationsApiAxiosParamCreator = function (configuration?: Configuration) {\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserNotificationConnections: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/users/notifications/connections`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserNotifications: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/users/notifications`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        sendEmailConfirmationLink: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            const localVarPath = `/users/notifications/confirmation/email`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {SendUserNotificationDto} sendUserNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        sendNotification: async (sendUserNotificationDto: SendUserNotificationDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'sendUserNotificationDto' is not null or undefined\n            assertParamExists('sendNotification', 'sendUserNotificationDto', sendUserNotificationDto)\n            const localVarPath = `/users/notifications/send`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(sendUserNotificationDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {Array<UpdateNotificationUserSettingsDto>} updateNotificationUserSettingsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUserNotifications: async (updateNotificationUserSettingsDto: Array<UpdateNotificationUserSettingsDto>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'updateNotificationUserSettingsDto' is not null or undefined\n            assertParamExists('updateUserNotifications', 'updateNotificationUserSettingsDto', updateNotificationUserSettingsDto)\n            const localVarPath = `/users/notifications`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(updateNotificationUserSettingsDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {UpsertNotificationConnectionDto} upsertNotificationConnectionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        usersNotificationsControllerCreateUserConnection: async (upsertNotificationConnectionDto: UpsertNotificationConnectionDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'upsertNotificationConnectionDto' is not null or undefined\n            assertParamExists('usersNotificationsControllerCreateUserConnection', 'upsertNotificationConnectionDto', upsertNotificationConnectionDto)\n            const localVarPath = `/users/notifications/connection`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(upsertNotificationConnectionDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n        /**\n         * \n         * @param {NotificationConnectionExistsDto} notificationConnectionExistsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        usersNotificationsControllerFindConnection: async (notificationConnectionExistsDto: NotificationConnectionExistsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {\n            // verify required parameter 'notificationConnectionExistsDto' is not null or undefined\n            assertParamExists('usersNotificationsControllerFindConnection', 'notificationConnectionExistsDto', notificationConnectionExistsDto)\n            const localVarPath = `/users/notifications/connection/find`;\n            // use dummy base URL string because the URL constructor only accepts absolute URLs.\n            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);\n            let baseOptions;\n            if (configuration) {\n                baseOptions = configuration.baseOptions;\n            }\n\n            const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};\n            const localVarHeaderParameter = {} as any;\n            const localVarQueryParameter = {} as any;\n\n\n    \n            localVarHeaderParameter['Content-Type'] = 'application/json';\n\n            setSearchParams(localVarUrlObj, localVarQueryParameter);\n            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};\n            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};\n            localVarRequestOptions.data = serializeDataIfNeeded(notificationConnectionExistsDto, localVarRequestOptions, configuration)\n\n            return {\n                url: toPathString(localVarUrlObj),\n                options: localVarRequestOptions,\n            };\n        },\n    }\n};\n\n/**\n * UsersNotificationsApi - functional programming interface\n * @export\n */\nexport const UsersNotificationsApiFp = function(configuration?: Configuration) {\n    const localVarAxiosParamCreator = UsersNotificationsApiAxiosParamCreator(configuration)\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUserNotificationConnections(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationUserConnectionsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserNotificationConnections(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async getUserNotifications(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserNotificationsDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.getUserNotifications(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async sendEmailConfirmationLink(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.sendEmailConfirmationLink(options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {SendUserNotificationDto} sendUserNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async sendNotification(sendUserNotificationDto: SendUserNotificationDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.sendNotification(sendUserNotificationDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {Array<UpdateNotificationUserSettingsDto>} updateNotificationUserSettingsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async updateUserNotifications(updateNotificationUserSettingsDto: Array<UpdateNotificationUserSettingsDto>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<UpdateNotificationUserSettingsDto>>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.updateUserNotifications(updateNotificationUserSettingsDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {UpsertNotificationConnectionDto} upsertNotificationConnectionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async usersNotificationsControllerCreateUserConnection(upsertNotificationConnectionDto: UpsertNotificationConnectionDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationConnectionDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.usersNotificationsControllerCreateUserConnection(upsertNotificationConnectionDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n        /**\n         * \n         * @param {NotificationConnectionExistsDto} notificationConnectionExistsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        async usersNotificationsControllerFindConnection(notificationConnectionExistsDto: NotificationConnectionExistsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<NotificationConnectionDto>> {\n            const localVarAxiosArgs = await localVarAxiosParamCreator.usersNotificationsControllerFindConnection(notificationConnectionExistsDto, options);\n            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);\n        },\n    }\n};\n\n/**\n * UsersNotificationsApi - factory interface\n * @export\n */\nexport const UsersNotificationsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {\n    const localVarFp = UsersNotificationsApiFp(configuration)\n    return {\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserNotificationConnections(options?: any): AxiosPromise<NotificationUserConnectionsDto> {\n            return localVarFp.getUserNotificationConnections(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        getUserNotifications(options?: any): AxiosPromise<UserNotificationsDto> {\n            return localVarFp.getUserNotifications(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        sendEmailConfirmationLink(options?: any): AxiosPromise<void> {\n            return localVarFp.sendEmailConfirmationLink(options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {SendUserNotificationDto} sendUserNotificationDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        sendNotification(sendUserNotificationDto: SendUserNotificationDto, options?: any): AxiosPromise<void> {\n            return localVarFp.sendNotification(sendUserNotificationDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {Array<UpdateNotificationUserSettingsDto>} updateNotificationUserSettingsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        updateUserNotifications(updateNotificationUserSettingsDto: Array<UpdateNotificationUserSettingsDto>, options?: any): AxiosPromise<Array<UpdateNotificationUserSettingsDto>> {\n            return localVarFp.updateUserNotifications(updateNotificationUserSettingsDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {UpsertNotificationConnectionDto} upsertNotificationConnectionDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        usersNotificationsControllerCreateUserConnection(upsertNotificationConnectionDto: UpsertNotificationConnectionDto, options?: any): AxiosPromise<NotificationConnectionDto> {\n            return localVarFp.usersNotificationsControllerCreateUserConnection(upsertNotificationConnectionDto, options).then((request) => request(axios, basePath));\n        },\n        /**\n         * \n         * @param {NotificationConnectionExistsDto} notificationConnectionExistsDto \n         * @param {*} [options] Override http request option.\n         * @throws {RequiredError}\n         */\n        usersNotificationsControllerFindConnection(notificationConnectionExistsDto: NotificationConnectionExistsDto, options?: any): AxiosPromise<NotificationConnectionDto> {\n            return localVarFp.usersNotificationsControllerFindConnection(notificationConnectionExistsDto, options).then((request) => request(axios, basePath));\n        },\n    };\n};\n\n/**\n * UsersNotificationsApi - object-oriented interface\n * @export\n * @class UsersNotificationsApi\n * @extends {BaseAPI}\n */\nexport class UsersNotificationsApi extends BaseAPI {\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersNotificationsApi\n     */\n    public getUserNotificationConnections(options?: AxiosRequestConfig) {\n        return UsersNotificationsApiFp(this.configuration).getUserNotificationConnections(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersNotificationsApi\n     */\n    public getUserNotifications(options?: AxiosRequestConfig) {\n        return UsersNotificationsApiFp(this.configuration).getUserNotifications(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersNotificationsApi\n     */\n    public sendEmailConfirmationLink(options?: AxiosRequestConfig) {\n        return UsersNotificationsApiFp(this.configuration).sendEmailConfirmationLink(options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {SendUserNotificationDto} sendUserNotificationDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersNotificationsApi\n     */\n    public sendNotification(sendUserNotificationDto: SendUserNotificationDto, options?: AxiosRequestConfig) {\n        return UsersNotificationsApiFp(this.configuration).sendNotification(sendUserNotificationDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {Array<UpdateNotificationUserSettingsDto>} updateNotificationUserSettingsDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersNotificationsApi\n     */\n    public updateUserNotifications(updateNotificationUserSettingsDto: Array<UpdateNotificationUserSettingsDto>, options?: AxiosRequestConfig) {\n        return UsersNotificationsApiFp(this.configuration).updateUserNotifications(updateNotificationUserSettingsDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {UpsertNotificationConnectionDto} upsertNotificationConnectionDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersNotificationsApi\n     */\n    public usersNotificationsControllerCreateUserConnection(upsertNotificationConnectionDto: UpsertNotificationConnectionDto, options?: AxiosRequestConfig) {\n        return UsersNotificationsApiFp(this.configuration).usersNotificationsControllerCreateUserConnection(upsertNotificationConnectionDto, options).then((request) => request(this.axios, this.basePath));\n    }\n\n    /**\n     * \n     * @param {NotificationConnectionExistsDto} notificationConnectionExistsDto \n     * @param {*} [options] Override http request option.\n     * @throws {RequiredError}\n     * @memberof UsersNotificationsApi\n     */\n    public usersNotificationsControllerFindConnection(notificationConnectionExistsDto: NotificationConnectionExistsDto, options?: AxiosRequestConfig) {\n        return UsersNotificationsApiFp(this.configuration).usersNotificationsControllerFindConnection(notificationConnectionExistsDto, options).then((request) => request(this.axios, this.basePath));\n    }\n}\n\n\n"
  },
  {
    "path": "client/src/api/base.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * \n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 1.0.0\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport { Configuration } from \"./configuration\";\n// Some imports not used depending on template conditions\n// @ts-ignore\nimport globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';\n\nexport const BASE_PATH = \"/api/v2\".replace(/\\/+$/, \"\");\n\n/**\n *\n * @export\n */\nexport const COLLECTION_FORMATS = {\n    csv: \",\",\n    ssv: \" \",\n    tsv: \"\\t\",\n    pipes: \"|\",\n};\n\n/**\n *\n * @export\n * @interface RequestArgs\n */\nexport interface RequestArgs {\n    url: string;\n    options: AxiosRequestConfig;\n}\n\n/**\n *\n * @export\n * @class BaseAPI\n */\nexport class BaseAPI {\n    protected configuration: Configuration | undefined;\n\n    constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {\n        if (configuration) {\n            this.configuration = configuration;\n            this.basePath = configuration.basePath || this.basePath;\n        }\n    }\n};\n\n/**\n *\n * @export\n * @class RequiredError\n * @extends {Error}\n */\nexport class RequiredError extends Error {\n    name: \"RequiredError\" = \"RequiredError\";\n    constructor(public field: string, msg?: string) {\n        super(msg);\n    }\n}\n"
  },
  {
    "path": "client/src/api/common.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * \n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 1.0.0\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nimport { Configuration } from \"./configuration\";\nimport { RequiredError, RequestArgs } from \"./base\";\nimport { AxiosInstance, AxiosResponse } from 'axios';\n\n/**\n *\n * @export\n */\nexport const DUMMY_BASE_URL = 'https://example.com'\n\n/**\n *\n * @throws {RequiredError}\n * @export\n */\nexport const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {\n    if (paramValue === null || paramValue === undefined) {\n        throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);\n    }\n}\n\n/**\n *\n * @export\n */\nexport const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {\n    if (configuration && configuration.apiKey) {\n        const localVarApiKeyValue = typeof configuration.apiKey === 'function'\n            ? await configuration.apiKey(keyParamName)\n            : await configuration.apiKey;\n        object[keyParamName] = localVarApiKeyValue;\n    }\n}\n\n/**\n *\n * @export\n */\nexport const setBasicAuthToObject = function (object: any, configuration?: Configuration) {\n    if (configuration && (configuration.username || configuration.password)) {\n        object[\"auth\"] = { username: configuration.username, password: configuration.password };\n    }\n}\n\n/**\n *\n * @export\n */\nexport const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {\n    if (configuration && configuration.accessToken) {\n        const accessToken = typeof configuration.accessToken === 'function'\n            ? await configuration.accessToken()\n            : await configuration.accessToken;\n        object[\"Authorization\"] = \"Bearer \" + accessToken;\n    }\n}\n\n/**\n *\n * @export\n */\nexport const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {\n    if (configuration && configuration.accessToken) {\n        const localVarAccessTokenValue = typeof configuration.accessToken === 'function'\n            ? await configuration.accessToken(name, scopes)\n            : await configuration.accessToken;\n        object[\"Authorization\"] = \"Bearer \" + localVarAccessTokenValue;\n    }\n}\n\n/**\n *\n * @export\n */\nexport const setSearchParams = function (url: URL, ...objects: any[]) {\n    const searchParams = new URLSearchParams(url.search);\n    for (const object of objects) {\n        for (const key in object) {\n            if (Array.isArray(object[key])) {\n                searchParams.delete(key);\n                for (const item of object[key]) {\n                    searchParams.append(key, item);\n                }\n            } else {\n                searchParams.set(key, object[key]);\n            }\n        }\n    }\n    url.search = searchParams.toString();\n}\n\n/**\n *\n * @export\n */\nexport const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {\n    const nonString = typeof value !== 'string';\n    const needsSerialization = nonString && configuration && configuration.isJsonMime\n        ? configuration.isJsonMime(requestOptions.headers['Content-Type'])\n        : nonString;\n    return needsSerialization\n        ? JSON.stringify(value !== undefined ? value : {})\n        : (value || \"\");\n}\n\n/**\n *\n * @export\n */\nexport const toPathString = function (url: URL) {\n    return url.pathname + url.search + url.hash\n}\n\n/**\n *\n * @export\n */\nexport const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {\n    return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {\n        const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || basePath) + axiosArgs.url};\n        return axios.request<T, R>(axiosRequestArgs);\n    };\n}\n"
  },
  {
    "path": "client/src/api/configuration.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * \n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 1.0.0\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nexport interface ConfigurationParameters {\n    apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);\n    username?: string;\n    password?: string;\n    accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);\n    basePath?: string;\n    baseOptions?: any;\n    formDataCtor?: new () => any;\n}\n\nexport class Configuration {\n    /**\n     * parameter for apiKey security\n     * @param name security name\n     * @memberof Configuration\n     */\n    apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);\n    /**\n     * parameter for basic security\n     *\n     * @type {string}\n     * @memberof Configuration\n     */\n    username?: string;\n    /**\n     * parameter for basic security\n     *\n     * @type {string}\n     * @memberof Configuration\n     */\n    password?: string;\n    /**\n     * parameter for oauth2 security\n     * @param name security name\n     * @param scopes oauth2 scope\n     * @memberof Configuration\n     */\n    accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);\n    /**\n     * override base path\n     *\n     * @type {string}\n     * @memberof Configuration\n     */\n    basePath?: string;\n    /**\n     * base options for axios calls\n     *\n     * @type {any}\n     * @memberof Configuration\n     */\n    baseOptions?: any;\n    /**\n     * The FormData constructor that will be used to create multipart form data\n     * requests. You can inject this here so that execution environments that\n     * do not support the FormData class can still run the generated client.\n     *\n     * @type {new () => FormData}\n     */\n    formDataCtor?: new () => any;\n\n    constructor(param: ConfigurationParameters = {}) {\n        this.apiKey = param.apiKey;\n        this.username = param.username;\n        this.password = param.password;\n        this.accessToken = param.accessToken;\n        this.basePath = param.basePath;\n        this.baseOptions = param.baseOptions;\n        this.formDataCtor = param.formDataCtor;\n    }\n\n    /**\n     * Check if the given MIME is a JSON MIME.\n     * JSON MIME examples:\n     *   application/json\n     *   application/json; charset=UTF8\n     *   APPLICATION/JSON\n     *   application/vnd.company+json\n     * @param mime - MIME (Multipurpose Internet Mail Extensions)\n     * @return True if the given MIME is JSON, false otherwise.\n     */\n    public isJsonMime(mime: string): boolean {\n        const jsonMime: RegExp = new RegExp('^(application\\/json|[^;/ \\t]+\\/[^;/ \\t]+[+]json)[ \\t]*(;.*)?$', 'i');\n        return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');\n    }\n}\n"
  },
  {
    "path": "client/src/api/git_push.sh",
    "content": "#!/bin/sh\n# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/\n#\n# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl \"minor update\" \"gitlab.com\"\n\ngit_user_id=$1\ngit_repo_id=$2\nrelease_note=$3\ngit_host=$4\n\nif [ \"$git_host\" = \"\" ]; then\n    git_host=\"github.com\"\n    echo \"[INFO] No command line input provided. Set \\$git_host to $git_host\"\nfi\n\nif [ \"$git_user_id\" = \"\" ]; then\n    git_user_id=\"GIT_USER_ID\"\n    echo \"[INFO] No command line input provided. Set \\$git_user_id to $git_user_id\"\nfi\n\nif [ \"$git_repo_id\" = \"\" ]; then\n    git_repo_id=\"GIT_REPO_ID\"\n    echo \"[INFO] No command line input provided. Set \\$git_repo_id to $git_repo_id\"\nfi\n\nif [ \"$release_note\" = \"\" ]; then\n    release_note=\"Minor update\"\n    echo \"[INFO] No command line input provided. Set \\$release_note to $release_note\"\nfi\n\n# Initialize the local directory as a Git repository\ngit init\n\n# Adds the files in the local repository and stages them for commit.\ngit add .\n\n# Commits the tracked changes and prepares them to be pushed to a remote repository.\ngit commit -m \"$release_note\"\n\n# Sets the new remote\ngit_remote=$(git remote)\nif [ \"$git_remote\" = \"\" ]; then # git remote not defined\n\n    if [ \"$GIT_TOKEN\" = \"\" ]; then\n        echo \"[INFO] \\$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment.\"\n        git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git\n    else\n        git remote add origin https://${git_user_id}:\"${GIT_TOKEN}\"@${git_host}/${git_user_id}/${git_repo_id}.git\n    fi\n\nfi\n\ngit pull origin master\n\n# Pushes (Forces) the changes in the local repository up to the remote repository\necho \"Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git\"\ngit push origin master 2>&1 | grep -v 'To https'\n"
  },
  {
    "path": "client/src/api/index.ts",
    "content": "/* tslint:disable */\n/* eslint-disable */\n/**\n * \n * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)\n *\n * The version of the OpenAPI document: 1.0.0\n * \n *\n * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).\n * https://openapi-generator.tech\n * Do not edit the class manually.\n */\n\n\nexport * from \"./api\";\nexport * from \"./configuration\";\n\n"
  },
  {
    "path": "client/src/components/Analytics.tsx",
    "content": "import Script from 'next/script';\n\nconst enableAnalytics = process.env.NODE_ENV === 'production';\n\nexport function Analytics() {\n  if (!enableAnalytics) {\n    return null;\n  }\n\n  return (\n    <>\n      <Script defer src=\"https://www.googletagmanager.com/gtag/js?id=G-WJLHZ9CCXJ\" />\n      <Script id=\"google-analytics\">\n        {`\n        window.dataLayer = window.dataLayer || [];\n        function gtag(){dataLayer.push(arguments);}\n        gtag('js', new Date());\n\n        gtag('config', 'G-WJLHZ9CCXJ');\n      `}\n      </Script>\n\n      <Script\n        defer\n        src=\"https://static.cloudflareinsights.com/beacon.min.js\"\n        data-cf-beacon={'{\"token\": \"e607238d732c4713b01b851ed3df61c2\"}'}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Comment.tsx",
    "content": "import { ReactNode } from 'react';\nimport { theme } from 'antd';\n\ntype CommentProps = {\n  author?: ReactNode;\n  avatar?: ReactNode;\n  content?: ReactNode;\n  datetime?: ReactNode;\n  children?: ReactNode;\n};\n\nexport function Comment({ author, avatar, content, datetime, children }: CommentProps) {\n  const { token } = theme.useToken();\n  return (\n    <div style={{ display: 'flex', gap: 8 }}>\n      {avatar && <div style={{ flexShrink: 0 }}>{avatar}</div>}\n      <div style={{ flex: 1, minWidth: 0 }}>\n        {(author || datetime) && (\n          <div style={{ marginBottom: 4 }}>\n            {author && <span style={{ fontWeight: 500, marginRight: 8 }}>{author}</span>}\n            {datetime && <span style={{ color: token.colorTextSecondary, fontSize: 12 }}>{datetime}</span>}\n          </div>\n        )}\n        {content && <div>{content}</div>}\n        {children && <div style={{ marginTop: 8 }}>{children}</div>}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/components/CountBadge/CountBadge.tsx",
    "content": "import { Badge, BadgeProps } from 'antd';\n\ntype PresetColors = BadgeProps['status'];\n\nfunction getStylesByStatus(status?: PresetColors) {\n  switch (status) {\n    case 'default':\n      return {\n        backgroundColor: '#f0f2f5',\n        color: 'rgba(0, 0, 0, 0.45)',\n      };\n    case 'processing':\n      return {\n        backgroundColor: '#e6f7ff',\n        color: '#1677ff',\n      };\n    default:\n      return {};\n  }\n}\n\nfunction CountBadge(props: BadgeProps) {\n  const { status, ...restProps } = props;\n\n  return <Badge {...restProps} style={getStylesByStatus(status)} />;\n}\n\nexport default CountBadge;\n"
  },
  {
    "path": "client/src/components/CountBadge/index.tsx",
    "content": "export { default as CountBadge } from './CountBadge';\n"
  },
  {
    "path": "client/src/components/CoursePageLayout.tsx",
    "content": "import { Layout, Spin, theme } from 'antd';\nimport { Course } from '@client/services/models';\nimport { CourseNoAccess } from '../modules/Course/components/CourseNoAccess';\nimport { Header } from '@client/shared/components/Header';\nimport { ReactNode } from 'react';\n\ntype Props = {\n  loading: boolean;\n  githubId: string;\n  course: Course;\n  title?: string;\n  children?: ReactNode;\n  showCourseName?: boolean;\n};\n\nexport function CoursePageLayout(props: Props) {\n  const { token } = theme.useToken();\n\n  if (props.course == null) {\n    return <CourseNoAccess />;\n  }\n\n  return (\n    <Layout style={{ minHeight: '100vh', background: token.colorBgLayout }}>\n      <Header title={props.title} showCourseName={props.showCourseName} />\n      <Layout.Content style={{ margin: 16 }}>\n        <Spin spinning={props.loading}>{props.children}</Spin>\n      </Layout.Content>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "client/src/components/DevTools/DevToolsContainer.tsx",
    "content": "import { Button, Card, FloatButton } from 'antd';\nimport { CSSProperties, ReactNode, useState } from 'react';\nimport { BugOutlined, CloseOutlined } from '@ant-design/icons';\nimport DevToolsUsers from '@client/components/DevTools/DevToolsUsers';\nimport DevToolsCurrentUser from '@client/components/DevTools/DevToolsCurrentUser';\n\nconst STYLE: CSSProperties = {\n  position: 'fixed',\n  bottom: '2rem',\n  right: '2rem',\n};\n\nconst TABS = [\n  {\n    key: 'users',\n    tab: 'Users',\n  },\n  {\n    key: 'currentUser',\n    tab: 'Current user session',\n  },\n];\n\nexport function DevToolsContainer({ children }: { children?: ReactNode }) {\n  const [visible, setVisible] = useState<boolean>(false);\n  const [activeTab, setActiveTab] = useState<string>('users');\n\n  function toggleVisible() {\n    setVisible(prev => !prev);\n  }\n\n  function onTabChange(key: string) {\n    setActiveTab(key);\n  }\n\n  const tabContent: Record<string, ReactNode> = {\n    users: <DevToolsUsers />,\n    currentUser: <DevToolsCurrentUser />,\n  };\n\n  return (\n    <>\n      {children}\n      {!visible ? (\n        <FloatButton shape=\"circle\" type=\"primary\" icon={<BugOutlined />} onClick={toggleVisible} style={STYLE} />\n      ) : (\n        <Card\n          title=\"Dev tools\"\n          tabList={TABS}\n          activeTabKey={activeTab}\n          onTabChange={onTabChange}\n          extra={<Button type=\"link\" icon={<CloseOutlined />} onClick={toggleVisible} />}\n          style={{\n            ...STYLE,\n            width: '40rem',\n            height: '27rem',\n          }}\n        >\n          {tabContent[activeTab]}\n        </Card>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/DevTools/DevToolsCurrentUser.tsx",
    "content": "import useRequest from 'ahooks/lib/useRequest';\nimport { Descriptions, Typography } from 'antd';\nimport { SessionApi } from '@client/api';\n\nconst { Text } = Typography;\nconst { Item } = Descriptions;\n\nconst sessionApi = new SessionApi();\n\nexport default function DevToolsCurrentUser() {\n  const { data, error } = useRequest(\n    async () => {\n      const response = await sessionApi.getSession();\n      return response.data;\n    },\n    {\n      cacheKey: 'devtools-session',\n      staleTime: 1000 * 60 * 5,\n    },\n  );\n\n  if (error || !data) {\n    return <Text type=\"secondary\">No active session</Text>;\n  }\n\n  return (\n    <Descriptions title=\"Active user session\" layout=\"vertical\" bordered={true}>\n      <Item label=\"User ID\">{data?.id}</Item>\n      <Item label=\"GitHub ID\">{data?.githubId}</Item>\n      <Item label=\"Admin\">{data?.isAdmin ? 'true' : 'false'}</Item>\n      <Item label=\"Course ids\">{Object.keys(data?.courses ?? {}).toString()}</Item>\n    </Descriptions>\n  );\n}\n"
  },
  {
    "path": "client/src/components/DevTools/DevToolsUsers.tsx",
    "content": "import { Button, Flex, Table, Typography } from 'antd';\nimport { useEffect, useMemo, useState } from 'react';\nimport { DevtoolsApi, DevtoolsUserDto } from '@client/api';\nimport { useRouter } from 'next/navigation';\n\nconst { Text } = Typography;\n\nconst devToolsApi = new DevtoolsApi();\n\nexport default function DevToolsUsers() {\n  const [users, setUsers] = useState<DevtoolsUserDto[]>([]);\n  const router = useRouter();\n\n  async function loginWithUser(user: DevtoolsUserDto) {\n    try {\n      await devToolsApi.getDevUserLogin(user.githubId);\n      router.push('/api/v2/auth/github/login');\n    } catch {\n      console.error('Failed to login user');\n    }\n  }\n\n  const tableColumns = useMemo(\n    () => [\n      {\n        title: 'id',\n        dataIndex: 'id',\n        key: 'id',\n      },\n      {\n        title: 'githubId',\n        dataIndex: 'githubId',\n        key: 'githubId',\n      },\n      {\n        title: 'student',\n        dataIndex: 'student',\n        key: 'student',\n        render: (title: string[]) => (\n          <Text ellipsis={{ tooltip: true }} style={{ width: '12ch' }}>\n            {title.join(', ')}\n          </Text>\n        ),\n      },\n      {\n        title: 'mentor',\n        dataIndex: 'mentor',\n        key: 'mentor',\n        render: (title: string[]) => (\n          <Text ellipsis={{ tooltip: true }} style={{ width: '12ch' }}>\n            {title.join(', ')}\n          </Text>\n        ),\n      },\n      {\n        title: 'action',\n        dataIndex: 'action',\n        key: 'action',\n        render: (_value: unknown, record: DevtoolsUserDto) => (\n          <Button type=\"link\" onClick={() => loginWithUser(record)}>\n            Login\n          </Button>\n        ),\n      },\n    ],\n    [loginWithUser],\n  );\n\n  useEffect(() => {\n    devToolsApi\n      .getDevUsers()\n      .then(res => {\n        setUsers(res.data);\n      })\n      .catch(console.error);\n  }, []);\n\n  return (\n    <Flex>\n      <Table\n        style={{ width: '100%' }}\n        size=\"small\"\n        bordered={false}\n        rowKey=\"id\"\n        dataSource={users}\n        columns={tableColumns}\n        pagination={{\n          pageSize: 4,\n        }}\n      ></Table>\n    </Flex>\n  );\n}\n"
  },
  {
    "path": "client/src/components/DevTools/index.ts",
    "content": "export { DevToolsContainer } from './DevToolsContainer';\n"
  },
  {
    "path": "client/src/components/Footer/Donation.tsx",
    "content": "import { Button } from 'antd';\nimport HeartTwoTone from '@ant-design/icons/HeartTwoTone';\n\ntype Props = {\n  maxDonatorsShown: number;\n};\n\nfunction Donation(props: Props) {\n  const { maxDonatorsShown } = props;\n\n  const widgetUrlPartial = `https://opencollective.com/rsschool/backers.svg?avatarHeight=36&button=false&width=300&limit=${maxDonatorsShown}`;\n\n  return (\n    <>\n      <h3>Thank you for your support!</h3>\n      <h4>Top {maxDonatorsShown} donators:</h4>\n      <p style={{ overflow: 'hidden' }}>\n        <object type=\"image/svg+xml\" data={widgetUrlPartial} />\n      </p>\n      <p>\n        <Button href=\"https://opencollective.com/rsschool#section-contribute\" target=\"_blank\">\n          <HeartTwoTone twoToneColor=\"#eb2f96\" />\n          ️Make a donation\n        </Button>\n      </p>\n    </>\n  );\n}\n\nexport { Donation };\n"
  },
  {
    "path": "client/src/components/Footer/Feedback.tsx",
    "content": "import { Menu } from './Menu';\nimport { HeartOutlined, LikeOutlined, TrophyOutlined } from '@ant-design/icons';\n\nconst publicRoutes = [\n  {\n    icon: <LikeOutlined />,\n    name: 'Say Thank you (Discord >> #gratitude)',\n    link: `/gratitude`,\n    newTab: false,\n  },\n  {\n    icon: <TrophyOutlined style={{ color: '#d60000' }} />,\n    name: 'Heroes page',\n    link: `/heroes`,\n    newTab: false,\n  },\n  {\n    icon: <HeartOutlined style={{ color: '#eb2f96' }} />,\n    name: 'Feedback on RS School',\n    link: `https://docs.google.com/forms/d/1F4NeS0oBq-CY805aqiPVp6CIrl4_nIYJ7Z_vUcMOFrQ/viewform`,\n    newTab: true,\n  },\n];\n\nexport const Feedback = function () {\n  return <Menu title=\"Feedback\" data={publicRoutes} />;\n};\n"
  },
  {
    "path": "client/src/components/Footer/FooterLayout.tsx",
    "content": "import * as React from 'react';\nimport { Col, Layout, Row } from 'antd';\nimport { Feedback } from './Feedback';\nimport { Help } from './Help';\nimport { SocialNetworks } from './SocialNetworks';\nimport { Donation } from './Donation';\n\nconst { Footer } = Layout;\n\nconst maxDonatorsShown = 21;\n\nclass FooterLayout extends React.Component {\n  getYear() {\n    const date = new Date();\n    return date.getFullYear();\n  }\n\n  render() {\n    return (\n      <div>\n        <Footer className=\"footer\">\n          <Row gutter={[16, 16]}>\n            <Col xs={24} sm={12} lg={16}>\n              <Row>\n                <Col xs={24} lg={12}>\n                  <Help />\n                </Col>\n                <Col xs={24} lg={12}>\n                  <Feedback />\n                </Col>\n              </Row>\n            </Col>\n            <Col xs={24} sm={12} lg={8}>\n              <Donation maxDonatorsShown={maxDonatorsShown} />\n            </Col>\n            <Col xs={24} sm={12} lg={16}>\n              <SocialNetworks />\n            </Col>\n            <Col xs={24} sm={12} lg={8}>\n              <small>&copy; The Rolling Scopes {this.getYear()}</small>\n            </Col>\n          </Row>\n        </Footer>\n      </div>\n    );\n  }\n}\n\nexport { FooterLayout };\n"
  },
  {
    "path": "client/src/components/Footer/Help.tsx",
    "content": "import { Menu } from './Menu';\nimport { BookOutlined, BugOutlined, HddOutlined } from '@ant-design/icons';\n\nconst githubIssuesUrl = 'https://github.com/rolling-scopes/rsschool-app/issues';\nconst publicRoutes = [\n  {\n    icon: <BookOutlined style={{ color: '#52c41a' }} />,\n    name: 'Docs',\n    link: 'https://docs.rs.school',\n    newTab: true,\n  },\n  {\n    icon: <BugOutlined style={{ color: '#d60000' }} />,\n    name: 'Report a bug',\n    link: `${githubIssuesUrl}/new?assignees=apalchys&labels=&template=bug-report.md`,\n    newTab: true,\n  },\n  {\n    icon: <HddOutlined style={{ color: '#d60000' }} />,\n    name: 'Report a data issue',\n    link: `${githubIssuesUrl}/new?assignees=apalchys&labels=&template=data-issue-report.md&title=`,\n    newTab: true,\n  },\n];\n\nexport const Help = function () {\n  return <Menu title=\"Help\" data={publicRoutes} />;\n};\n"
  },
  {
    "path": "client/src/components/Footer/Menu.tsx",
    "content": "import React from 'react';\nimport { List } from 'antd';\nimport Link from 'next/link';\n\ntype LinkInfo = { icon: React.ReactNode; name: string; link: string; newTab: boolean };\n\ntype MenuProps = {\n  title: string;\n  data: LinkInfo[];\n};\n\nclass Menu extends React.Component<MenuProps> {\n  render() {\n    return (\n      <div style={{ marginBottom: 16 }}>\n        <h3>{this.props.title}</h3>\n        <List\n          size=\"small\"\n          dataSource={this.props.data}\n          renderItem={(linkInfo: LinkInfo) => (\n            <List.Item key={linkInfo.link}>\n              <Link prefetch={false} href={linkInfo.link} target={linkInfo.newTab ? '_blank' : '_self'}>\n                {linkInfo.icon}&nbsp;{linkInfo.name}\n              </Link>\n            </List.Item>\n          )}\n        />\n      </div>\n    );\n  }\n}\n\nexport { Menu };\n"
  },
  {
    "path": "client/src/components/Footer/SocialNetworks.tsx",
    "content": "import * as React from 'react';\nimport { Row, Col, Space } from 'antd';\nimport YoutubeFilled from '@ant-design/icons/YoutubeFilled';\nimport GithubFilled from '@ant-design/icons/GithubFilled';\nimport LinkedinOutlined from '@ant-design/icons/LinkedinOutlined';\nimport { DiscordOutlined } from '@client/shared/components/Icons/DiscordOutlined';\n\nconst iconStyle = { fontSize: 24 };\n\nconst socialLinks = [\n  {\n    icon: <GithubFilled style={iconStyle} />,\n    name: 'GitHub',\n    link: `https://github.com/rolling-scopes/rsschool-app`,\n    newTab: true,\n  },\n  {\n    icon: <YoutubeFilled style={iconStyle} />,\n    name: 'YouTube',\n    link: `https://www.youtube.com/c/rollingscopesschool`,\n    newTab: true,\n  },\n  {\n    icon: <DiscordOutlined style={iconStyle} />,\n    name: 'Discord',\n    link: `https://discord.gg/PRADsJB`,\n    newTab: true,\n  },\n  {\n    icon: <LinkedinOutlined style={iconStyle} />,\n    name: 'LinkedIn',\n    link: `https://www.linkedin.com/company/the-rolling-scopes-school/`,\n    newTab: true,\n  },\n];\n\ntype LinkInfo = { icon: React.ReactNode; name: string; link: string; newTab: boolean };\n\nclass SocialNetworks extends React.Component {\n  render() {\n    return (\n      <Row gutter={[16, 8]}>\n        {socialLinks.map((linkInfo: LinkInfo) => {\n          return (\n            <Col key={linkInfo.link}>\n              <a\n                target={linkInfo.newTab ? '_blank' : '_self'}\n                href={linkInfo.link}\n                style={{ color: 'var(--text-primary-color)' }}\n              >\n                <Space>\n                  {linkInfo.icon}\n                  {linkInfo.name}\n                </Space>\n              </a>\n            </Col>\n          );\n        })}\n      </Row>\n    );\n  }\n}\n\nexport { SocialNetworks };\n"
  },
  {
    "path": "client/src/components/Footer/index.tsx",
    "content": "export { FooterLayout } from './FooterLayout';\n"
  },
  {
    "path": "client/src/components/HeaderMiniBannerCarousel.module.css",
    "content": ".carousel {\n  position: relative;\n  width: 284px;\n  height: 56px;\n  border-radius: 6px;\n}\n\n.carouselViewport {\n  width: 100%;\n  height: 100%;\n  padding: 0 22px;\n  overflow: hidden;\n}\n\n.carouselInner {\n  width: 100%;\n  height: 100%;\n}\n\n.carouselInner :global(.slick-list),\n.carouselInner :global(.slick-track),\n.carouselInner :global(.slick-slide),\n.carouselInner :global(.slick-slide > div) {\n  height: 100%;\n}\n\n/* text-only slide */\n.slideContent {\n  height: 100%;\n}\n\n/* banner slide — needs position:relative so .bannerLink overlay works */\n.slideContentBanner {\n  position: relative;\n  height: 100%;\n  overflow: hidden;\n}\n\n.slide {\n  display: flex;\n  width: 100%;\n  height: 100%;\n  align-items: center;\n  justify-content: center;\n  color: inherit;\n  text-decoration: none;\n  padding: 0 26px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\na.slide:hover {\n  text-decoration: underline;\n}\n\n.banner {\n  display: block;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n/* transparent full-size link overlay on top of banner img */\n.bannerLink {\n  position: absolute;\n  inset: 0;\n}\n\n.title {\n  width: 100%;\n  padding: 0 8px;\n  font-size: 12px;\n  font-weight: 600;\n  text-align: center;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.controlLeft,\n.controlRight {\n  position: absolute;\n  top: 50%;\n  width: 18px;\n  height: 18px;\n  margin-top: -9px;\n  border: 1px solid transparent;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  font-size: 11px;\n  line-height: 1;\n  z-index: 1;\n  transition:\n    filter 0.2s ease,\n    box-shadow 0.2s ease;\n}\n\n.controlLeft {\n  left: 2px;\n}\n\n.controlRight {\n  right: 2px;\n}\n\n:global(.dark) .controlLeft:hover,\n:global(.dark) .controlRight:hover {\n  filter: brightness(1.35);\n  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.38);\n}\n\n:global(.light) .controlLeft:hover,\n:global(.light) .controlRight:hover {\n  filter: brightness(0.72);\n  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.28);\n}\n"
  },
  {
    "path": "client/src/components/HeaderMiniBannerCarousel.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { HeaderMiniBannerCarousel } from './HeaderMiniBannerCarousel';\n\ndescribe('HeaderMiniBannerCarousel', () => {\n  it('should render title when no banner is set', () => {\n    render(<HeaderMiniBannerCarousel items={[{ title: 'Feature updates' }]} />);\n\n    expect(screen.getByText('Feature updates')).toBeInTheDocument();\n  });\n\n  it('should render banner image when banner is set', () => {\n    const bannerPath = 'test-banner-xyz123.png';\n    render(<HeaderMiniBannerCarousel items={[{ banner: bannerPath, title: 'Logo banner' }]} />);\n\n    const bannerImage = screen.getByRole('img');\n    expect(bannerImage).toBeInTheDocument();\n    expect(bannerImage).toHaveAttribute('src', bannerPath);\n  });\n\n  it('should render link when item has url', () => {\n    render(<HeaderMiniBannerCarousel items={[{ title: 'Open docs', url: 'https://rs.school/docs/en' }]} />);\n\n    expect(screen.getByRole('link', { name: 'Open docs' })).toHaveAttribute('href', 'https://rs.school/docs/en');\n  });\n\n  it('should render controls for multiple items', () => {\n    render(<HeaderMiniBannerCarousel items={[{ title: 'First slide' }, { title: 'Second slide' }]} />);\n\n    expect(screen.getByRole('button', { name: 'Previous banner' })).toBeInTheDocument();\n    expect(screen.getByRole('button', { name: 'Next banner' })).toBeInTheDocument();\n  });\n\n  it('should not render controls for single item', () => {\n    render(<HeaderMiniBannerCarousel items={[{ title: 'Single slide' }]} />);\n\n    expect(screen.queryByRole('button', { name: 'Previous banner' })).not.toBeInTheDocument();\n    expect(screen.queryByRole('button', { name: 'Next banner' })).not.toBeInTheDocument();\n  });\n\n  it('should not render when items array is empty', () => {\n    render(<HeaderMiniBannerCarousel items={[]} />);\n\n    expect(screen.queryByTestId('carouselContainer')).not.toBeInTheDocument();\n  });\n\n  it('should not render when items have no banner or title', () => {\n    render(<HeaderMiniBannerCarousel items={[{ url: 'https://example.com' }]} />);\n\n    expect(screen.queryByTestId('carouselContainer')).not.toBeInTheDocument();\n  });\n\n  it('should not render when items are empty objects', () => {\n    render(<HeaderMiniBannerCarousel items={[{}]} />);\n\n    expect(screen.queryByTestId('carouselContainer')).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/HeaderMiniBannerCarousel.tsx",
    "content": "import { useMemo, useRef } from 'react';\nimport { Carousel, theme } from 'antd';\nimport type { CarouselRef } from 'antd/es/carousel';\nimport { LeftOutlined, RightOutlined } from '@ant-design/icons';\nimport styles from './HeaderMiniBannerCarousel.module.css';\n\nexport type HeaderMiniBannerCarouselItem = {\n  banner?: string;\n  title?: string;\n  url?: string;\n};\n\ntype Props = {\n  items?: ReadonlyArray<HeaderMiniBannerCarouselItem>;\n  intervalMs?: number;\n  className?: string;\n};\n\nconst DEFAULT_INTERVAL_MS = 5000;\n\nexport function HeaderMiniBannerCarousel({ items = [], intervalMs = DEFAULT_INTERVAL_MS, className }: Props) {\n  const { token } = theme.useToken();\n  const carouselRef = useRef<CarouselRef>(null);\n  const visibleItems = useMemo(() => items.filter(item => item.banner || item.title), [items]);\n  const hasControls = visibleItems.length > 1;\n\n  if (visibleItems.length === 0) {\n    return null;\n  }\n\n  const goToPrevItem = () => {\n    carouselRef.current?.prev();\n  };\n\n  const goToNextItem = () => {\n    carouselRef.current?.next();\n  };\n  const carouselClassName = [styles.carousel, className].filter(Boolean).join(' ');\n  const controlStyle = {\n    backgroundColor: token.colorFillSecondary,\n    borderColor: token.colorBorder,\n    color: token.colorText,\n  };\n\n  return (\n    <div data-testid=\"carouselContainer\" className={carouselClassName}>\n      <div className={styles.carouselViewport}>\n        <Carousel\n          ref={carouselRef}\n          className={styles.carouselInner}\n          autoplay={hasControls && intervalMs > 0}\n          autoplaySpeed={intervalMs}\n          dots={false}\n          infinite={hasControls}\n        >\n          {visibleItems.map((item, idx) => {\n            const label = item.title ?? 'Header banner';\n            const hasBanner = Boolean(item.banner);\n\n            return (\n              <div\n                key={`${item.url ?? ''}-${item.banner ?? ''}-${item.title ?? label}-${idx}`}\n                className={hasBanner ? styles.slideContentBanner : styles.slideContent}\n              >\n                {hasBanner ? (\n                  <>\n                    <img src={item.banner} alt={label} className={styles.banner} />\n                    {item.url && <a href={item.url} className={styles.bannerLink} title={label} aria-label={label} />}\n                  </>\n                ) : item.url ? (\n                  <a href={item.url} className={styles.slide} title={label}>\n                    <span className={styles.title} style={{ color: token.colorText }}>\n                      {item.title}\n                    </span>\n                  </a>\n                ) : (\n                  <span className={styles.slide} title={label}>\n                    <span className={styles.title} style={{ color: token.colorText }}>\n                      {item.title}\n                    </span>\n                  </span>\n                )}\n              </div>\n            );\n          })}\n        </Carousel>\n      </div>\n      {hasControls ? (\n        <>\n          <button\n            type=\"button\"\n            aria-label=\"Previous banner\"\n            className={styles.controlLeft}\n            style={controlStyle}\n            onClick={goToPrevItem}\n          >\n            <LeftOutlined />\n          </button>\n          <button\n            type=\"button\"\n            aria-label=\"Next banner\"\n            className={styles.controlRight}\n            style={controlStyle}\n            onClick={goToNextItem}\n          >\n            <RightOutlined />\n          </button>\n        </>\n      ) : null}\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Heroes/HeroesCountBadge.tsx",
    "content": "import heroesBadges from '@client/configs/heroes-badges';\nimport { Avatar, Badge, Tooltip } from 'antd';\nimport { HeroesRadarBadgeDto } from '@client/api';\nimport dayjs from 'dayjs';\n\ntype HeroesCountBadgeProps = {\n  badge: Omit<HeroesRadarBadgeDto, 'id' | 'comment' | 'date'> &\n    Partial<Pick<HeroesRadarBadgeDto, 'comment' | 'date'> & { count: number }>;\n};\n\nfunction HeroesCountBadge({ badge: { badgeId, count = 0, comment, date } }: HeroesCountBadgeProps) {\n  return (\n    <div style={{ margin: 5, display: 'inline-block' }}>\n      <Badge count={count}>\n        <Tooltip\n          title={\n            <>\n              {heroesBadges[badgeId]?.name ?? ''}\n              {comment && date && (\n                <>\n                  <br />\n                  {comment}\n                  <br />\n                  {dayjs(date).format('YYYY-MM-DD HH:mm')}\n                </>\n              )}\n            </>\n          }\n        >\n          <Avatar src={`/static/svg/badges/${heroesBadges[badgeId]?.url ?? ''}`} alt={`${badgeId} badge`} size={48} />\n        </Tooltip>\n      </Badge>\n    </div>\n  );\n}\n\nexport default HeroesCountBadge;\n"
  },
  {
    "path": "client/src/components/Heroes/HeroesRadarTab.tsx",
    "content": "import { FileExcelOutlined } from '@ant-design/icons';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { onlyDefined } from '@client/shared/utils/onlyDefined';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport type { TimeRangePickerProps } from 'antd';\nimport { Button, Checkbox, DatePicker, Form, Row, Select, Space, TableProps } from 'antd';\nimport { CountryDto, GratitudesApi, HeroesRadarDto, HeroRadarDto } from '@client/api';\nimport dayjs from 'dayjs';\nimport { useCallback, useContext, useEffect, useState } from 'react';\nimport HeroesRadarTable from './HeroesRadarTable';\n\nexport type HeroesRadarFormProps = {\n  courseId?: number;\n  notActivist?: boolean;\n  countryName?: string;\n  dates?: (dayjs.Dayjs | null)[];\n};\n\ntype GetHeroesProps = HeroesRadarFormProps & Partial<IPaginationInfo>;\n\nexport type LayoutType = Parameters<typeof Form>[0]['layout'];\n\nconst initialPage = 1;\nconst initialPageSize = 20;\nconst initialQueryParams = { current: initialPage, pageSize: initialPageSize };\n\nconst { RangePicker } = DatePicker;\n\nconst currentDayjs = dayjs();\nconst rangePresets: TimeRangePickerProps['presets'] = [\n  { label: 'Last 7 Days', value: [currentDayjs.add(-7, 'd'), currentDayjs] },\n  { label: 'Last 14 Days', value: [currentDayjs.add(-14, 'd'), currentDayjs] },\n  { label: 'Last 30 Days', value: [currentDayjs.add(-30, 'd'), currentDayjs] },\n  { label: 'Last 90 Days', value: [currentDayjs.add(-90, 'd'), currentDayjs] },\n];\n\nfunction HeroesRadarTab({ setLoading }: { setLoading: (arg: boolean) => void }) {\n  const { courses } = useActiveCourseContext();\n\n  const [heroes, setHeroes] = useState<HeroesRadarDto>({\n    content: [],\n    pagination: { current: initialPage, pageSize: initialPageSize, itemCount: 0, total: 0, totalPages: 0 },\n  });\n\n  const [countries, setCountries] = useState<CountryDto[]>([]);\n  const [form] = Form.useForm();\n  const [formData, setFormData] = useState<HeroesRadarFormProps>(form.getFieldsValue());\n  const [formLayout, setFormLayout] = useState<LayoutType>('inline');\n  const { isAdmin } = useContext(SessionContext);\n\n  const gratitudeApi = new GratitudesApi();\n\n  const getCountries = async () => {\n    const { data } = await gratitudeApi.getHeroesCountries();\n    setCountries(data);\n  };\n\n  const getHeroes = async ({\n    current = initialPage,\n    pageSize = initialPageSize,\n    courseId,\n    notActivist,\n    countryName,\n    dates,\n  }: GetHeroesProps) => {\n    try {\n      setLoading(true);\n      const [startDate, endDate] = dates?.map(date => date?.format('YYYY-MM-DD')) ?? [];\n      const { data } = await gratitudeApi.getHeroesRadar(\n        current,\n        pageSize,\n        courseId,\n        notActivist,\n        countryName,\n        startDate,\n        endDate,\n      );\n      setHeroes(data);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  useEffect(() => {\n    getHeroes(initialQueryParams);\n    if (isAdmin) {\n      getCountries();\n    }\n  }, []);\n\n  const handleSubmit = useCallback(async (formData: HeroesRadarFormProps) => {\n    const data = onlyDefined(formData);\n    setFormData(data);\n    await getHeroes(data);\n  }, []);\n\n  const onClear = useCallback(async () => {\n    form.resetFields();\n    setFormData(form.getFieldsValue());\n    await getHeroes(initialQueryParams);\n  }, []);\n\n  const handleChange: TableProps<HeroRadarDto>['onChange'] = async ({ current, pageSize }) => {\n    try {\n      setLoading(true);\n      await getHeroes({ current, pageSize, ...formData });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const exportToCsv = () => {\n    const data = onlyDefined(formData);\n    const formParams = Object.entries(data).reduce((acc: string[][], [key, value]) => {\n      if (key === 'dates' && Array.isArray(value)) {\n        const [startDate, endDate] = value.map(date => date?.format('YYYY-MM-DD'));\n        return [...acc, ['startDate', `${startDate}`], ['endDate', `${endDate}`]];\n      }\n      return [...acc, [key, `${value}`]];\n    }, []);\n\n    const params = new URLSearchParams([['current', '1'], ['pageSize', `${heroes.pagination.total}`], ...formParams]);\n    window.location.href = `/api/v2/gratitudes/heroes/radar/csv?${params}`;\n  };\n\n  return (\n    <>\n      <Row style={{ marginBottom: 24 }} justify=\"space-between\">\n        <Form layout={formLayout} form={form} onFinish={handleSubmit}>\n          <Form.Item name={'courseId'} label=\"Courses\" style={{ minWidth: 260, marginBottom: 16 }}>\n            <Select\n              placeholder=\"Select course\"\n              showSearch\n              optionFilterProp=\"label\"\n              options={courses.map(({ id, name }) => ({ value: id, label: name }))}\n            />\n          </Form.Item>\n          {isAdmin && (\n            <Form.Item name={'countryName'} label=\"Countries\" style={{ minWidth: 260, marginBottom: 16 }}>\n              <Select\n                placeholder=\"Select country\"\n                showSearch\n                options={countries.map(({ countryName }) => ({ value: countryName, label: countryName }))}\n              />\n            </Form.Item>\n          )}\n          <Form.Item name={'dates'} label=\"Dates\" style={{ minWidth: 260, marginBottom: 16 }}>\n            <RangePicker presets={rangePresets} />\n          </Form.Item>\n          <Form.Item name={'notActivist'} valuePropName=\"checked\" style={{ marginBottom: 16 }}>\n            <Checkbox>Show only not activists</Checkbox>\n          </Form.Item>\n          <Space align=\"start\" size={20} style={{ marginBottom: 16 }}>\n            <Button type=\"primary\" htmlType=\"submit\">\n              Filter\n            </Button>\n            <Button type=\"primary\" onClick={onClear}>\n              Clear\n            </Button>\n          </Space>\n        </Form>\n        {isAdmin && (\n          <Button icon={<FileExcelOutlined />} style={{ marginRight: 8 }} onClick={exportToCsv}>\n            Export CSV\n          </Button>\n        )}\n      </Row>\n      <HeroesRadarTable heroes={heroes} onChange={handleChange} setFormLayout={setFormLayout} />\n    </>\n  );\n}\n\nexport default HeroesRadarTab;\n"
  },
  {
    "path": "client/src/components/Heroes/HeroesRadarTable.tsx",
    "content": "import { Table, TableProps, Tag } from 'antd';\nimport { ColumnType } from 'antd/lib/table';\nimport { HeroRadarDto, HeroesRadarBadgeDto, HeroesRadarDto } from '@client/api';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport Link from 'next/link';\nimport HeroesCountBadge from './HeroesCountBadge';\nimport useWindowDimensions from '@client/shared/hooks/useWindowDimensions';\nimport { useState, useEffect } from 'react';\nimport type { LayoutType } from './HeroesRadarTab';\nimport { getTableWidth } from '@client/modules/Score/components/ScoreTable';\nimport heroesBadges from '@client/configs/heroes-badges';\n\ninterface HeroesRadarTableProps {\n  heroes: HeroesRadarDto;\n  onChange: TableProps<HeroRadarDto>['onChange'];\n  setFormLayout: (layout: LayoutType) => void;\n}\n\nconst BADGE_SIZE = 48;\nconst BADGE_SUM_HORIZONTAL_MARGIN = 2 * 5;\nconst XS_BREAKPOINT_IN_PX = 575;\n\nconst initColumns: ColumnType<HeroRadarDto>[] = [\n  {\n    title: '#',\n    fixed: 'left',\n    dataIndex: 'rank',\n    key: 'rank',\n    width: 50,\n    defaultSortOrder: 'ascend',\n    sorter: (a, b) => a.rank - b.rank,\n    render: (value: number) => (value >= 999999 ? 'New' : value),\n  },\n  {\n    title: 'GitHub',\n    fixed: 'left',\n    key: 'githubId',\n    dataIndex: 'githubId',\n    width: 150,\n    render: (value: string) => (\n      <div>\n        <GithubAvatar githubId={value} size={24} />\n        &nbsp;\n        <a target=\"_blank\" href={`https://github.com/${value}`}>\n          {value}\n        </a>\n      </div>\n    ),\n  },\n  {\n    title: 'Name',\n    dataIndex: 'name',\n    key: 'name',\n    width: 150,\n    render: (value: string, record: HeroRadarDto) => (\n      <Link prefetch={false} href={`/profile?githubId=${record.githubId}`}>\n        {value}\n      </Link>\n    ),\n  },\n  {\n    title: 'Total badges count',\n    dataIndex: 'total',\n    key: 'total',\n    width: 80,\n    render: (value: number) => <b>{value}</b>,\n  },\n  {\n    title: 'Badges',\n    dataIndex: 'badges',\n    key: 'badges',\n    width: Object.keys(heroesBadges).length * (BADGE_SIZE + BADGE_SUM_HORIZONTAL_MARGIN),\n    responsive: ['xxl', 'xl', 'lg', 'md', 'sm'],\n    render: (value: HeroesRadarBadgeDto[], { total }: HeroRadarDto) => (\n      <>\n        {value.map(({ id, badgeId, comment, date }) => {\n          return <HeroesCountBadge key={id} badge={{ badgeId, comment, date }} />;\n        })}\n\n        {total > 20 && <Tag>+{total - 20} More</Tag>}\n      </>\n    ),\n  },\n];\n\nfunction HeroesRadarTable({ heroes, onChange, setFormLayout }: HeroesRadarTableProps) {\n  const { width } = useWindowDimensions();\n  const [fixedColumn, setFixedColumn] = useState<boolean>(true);\n  const [columns, setColumns] = useState(initColumns);\n\n  useEffect(() => {\n    if (width < XS_BREAKPOINT_IN_PX) {\n      setFixedColumn(false);\n      setFormLayout('vertical');\n      return;\n    }\n\n    setFixedColumn(true);\n    setFormLayout('inline');\n  }, [width]);\n\n  useEffect(() => {\n    setColumns(prevColumns => {\n      const githubColumn = prevColumns.find(el => el.key === 'githubId');\n      if (githubColumn) {\n        githubColumn.fixed = fixedColumn ? 'left' : false;\n      }\n\n      return prevColumns;\n    });\n  }, [fixedColumn]);\n\n  return (\n    <Table\n      pagination={{ ...heroes.pagination, showTotal: total => `Total ${total} students` }}\n      onChange={onChange}\n      rowKey=\"githubId\"\n      scroll={{ x: getTableWidth(columns.length), y: 'calc(95vh - 290px)' }}\n      dataSource={heroes.content}\n      columns={columns}\n    />\n  );\n}\n\nexport default HeroesRadarTable;\n"
  },
  {
    "path": "client/src/components/LoadingScreen.module.css",
    "content": ".loadingScreen {\n  z-index: 1;\n  top: 0;\n  left: 0;\n}\n"
  },
  {
    "path": "client/src/components/MentorOptions.tsx",
    "content": "import { Button, Form, FormInstance, Select } from 'antd';\nimport { StudentSearch } from '@client/shared/components/StudentSearch';\nimport { MentorDetailsDtoStudentsPreferenceEnum } from '@client/api';\n\nexport type Options = {\n  maxStudentsLimit: number;\n  preferedStudentsLocation: MentorDetailsDtoStudentsPreferenceEnum;\n  students?: { value: number }[];\n  preselectedStudents?: { id: number; githubId: string; name: string }[];\n};\n\nconst STUDENTS_NUMBERS = [0, 1, 2, 3, 4, 5, 6];\n\nexport function MentorOptions({\n  course,\n  mentorData,\n  form,\n  handleSubmit,\n  showSubmitButton = true,\n}: {\n  form: FormInstance;\n  mentorData: Options | null;\n  handleSubmit?: (values: Options) => Promise<void>;\n  course: { id: number; name: string; minStudentsPerMentor?: number };\n  showSubmitButton?: boolean;\n}) {\n  return (\n    <>\n      <Form\n        initialValues={mentorData ?? undefined}\n        style={{ marginTop: 32 }}\n        form={form}\n        onFinish={handleSubmit}\n        layout=\"vertical\"\n      >\n        <Form.Item\n          name=\"maxStudentsLimit\"\n          label=\"How many students are you ready to mentor per course?\"\n          rules={[{ required: true, message: 'Please select students count' }]}\n        >\n          <Select style={{ width: 200 }} placeholder=\"Students count...\">\n            {STUDENTS_NUMBERS.map(num => {\n              const studentsNumber = num + Number(course.minStudentsPerMentor);\n              return (\n                <Select.Option key={studentsNumber} value={studentsNumber}>\n                  {studentsNumber}\n                </Select.Option>\n              );\n            })}\n          </Select>\n        </Form.Item>\n\n        <Form.Item\n          name=\"preferedStudentsLocation\"\n          label=\"Preferred students location\"\n          help=\"We will use this information to distribute students\"\n          rules={[{ required: true, message: 'Please select a prefered location option' }]}\n        >\n          <Select placeholder=\"Select a prefered option...\">\n            <Select.Option value={'any'}>Any city or country</Select.Option>\n            <Select.Option value={'country'}>My country only</Select.Option>\n            <Select.Option value={'city'}>My city only</Select.Option>\n          </Select>\n        </Form.Item>\n\n        <Form.Item\n          help=\"If you want to be a mentor of particular students\"\n          name=\"students\"\n          label=\"Predefined students (if any)\"\n        >\n          <StudentSearch\n            defaultValues={mentorData?.preselectedStudents}\n            onlyStudentsWithoutMentorShown={true}\n            labelInValue\n            courseId={course.id}\n            mode=\"multiple\"\n          />\n        </Form.Item>\n\n        {showSubmitButton && (\n          <Button style={{ marginTop: 32 }} size=\"large\" type=\"primary\" htmlType=\"submit\">\n            Confirm\n          </Button>\n        )}\n      </Form>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/AboutCard.tsx",
    "content": "import { ChangeEvent, useEffect, useState } from 'react';\nimport { Typography, Input } from 'antd';\nimport InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';\nimport { UpdateProfileInfoDto } from '@client/api';\nimport CommonCardWithSettingsModal from './CommonCardWithSettingsModal';\n\nconst { Paragraph, Text } = Typography;\nconst { TextArea } = Input;\n\ntype Props = {\n  data: string;\n  isEditingModeEnabled: boolean;\n  updateProfile: (data: UpdateProfileInfoDto) => Promise<boolean>;\n};\n\nconst AboutCard = ({ isEditingModeEnabled, data, updateProfile }: Props) => {\n  const [displayValue, setDisplayValue] = useState(data);\n  const [value, setValue] = useState(displayValue);\n  const [isSaveDisabled, setIsSaveDisabled] = useState(true);\n\n  const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {\n    setValue(e.target.value);\n  };\n\n  const handleSave = async () => {\n    const isUpdated = await updateProfile({ aboutMyself: value });\n    if (!isUpdated) {\n      return;\n    }\n\n    setDisplayValue(value);\n  };\n\n  const handleCancel = () => {\n    setValue(displayValue);\n  };\n\n  useEffect(() => {\n    const readyToUpdate = displayValue !== value;\n\n    setIsSaveDisabled(!readyToUpdate);\n  }, [displayValue, value]);\n\n  return (\n    <CommonCardWithSettingsModal\n      title=\"About\"\n      icon={<InfoCircleOutlined />}\n      content={displayValue ? <Paragraph ellipsis={{ rows: 2, expandable: true }}>{displayValue}</Paragraph> : null}\n      noDataDescription=\"About info isn't written\"\n      isEditingModeEnabled={isEditingModeEnabled}\n      profileSettingsContent={\n        <div>\n          <p style={{ fontSize: 18, marginBottom: 5 }}>\n            <Text strong>About myself:</Text>\n          </p>\n          <TextArea rows={4} value={value} onChange={handleChange} />\n        </div>\n      }\n      saveProfile={handleSave}\n      cancelChanges={handleCancel}\n      isSaveDisabled={isSaveDisabled}\n    />\n  );\n};\n\nexport default AboutCard;\n"
  },
  {
    "path": "client/src/components/Profile/CommonCard.tsx",
    "content": "import { ReactNode } from 'react';\nimport { Card, Typography, Empty } from 'antd';\nimport EditOutlined from '@ant-design/icons/EditOutlined';\n\nconst { Title } = Typography;\n\ntype Props = {\n  noDataDescription?: string | JSX.Element;\n  title: string;\n  icon: JSX.Element;\n  content: JSX.Element | null;\n  actions?: ReactNode[];\n  handleEdit?: () => void;\n};\n\nconst CommonCard = ({ title, icon, content, noDataDescription, actions, handleEdit }: Props) => {\n  return (\n    <Card\n      title={\n        <Title\n          level={2}\n          ellipsis={true}\n          style={{\n            fontSize: 16,\n            marginBottom: 0,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'space-between',\n          }}\n        >\n          <span>\n            {icon} {title}\n          </span>\n          {handleEdit ? <EditOutlined key=\"main-card-actions-edit\" onClick={handleEdit} /> : null}\n        </Title>\n      }\n      actions={actions}\n    >\n      {content ?? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={noDataDescription} />}\n    </Card>\n  );\n};\n\nexport default CommonCard;\n"
  },
  {
    "path": "client/src/components/Profile/CommonCardWithSettingsModal.tsx",
    "content": "import { ReactNode, useState } from 'react';\nimport ProfileSettingsModal from './ProfileSettingsModal';\nimport CommonCard from './CommonCard';\n\ntype Props = {\n  profileSettingsContent: JSX.Element;\n  isEditingModeEnabled: boolean;\n  noDataDescription?: string | JSX.Element;\n  settingsTitle?: string;\n  title: string;\n  icon: JSX.Element;\n  content: JSX.Element | null;\n  actions?: ReactNode[];\n  isSaveDisabled?: boolean;\n  saveProfile: () => void;\n  cancelChanges: () => void;\n};\n\nconst CommonCardWithSettingsModal = ({\n  title,\n  icon,\n  content,\n  profileSettingsContent,\n  isEditingModeEnabled,\n  noDataDescription,\n  settingsTitle,\n  actions,\n  isSaveDisabled,\n  saveProfile,\n  cancelChanges,\n}: Props) => {\n  const [isProfileSettingsVisible, setIsProfileSettingsVisible] = useState(false);\n\n  const showProfileSettings = () => {\n    setIsProfileSettingsVisible(true);\n  };\n\n  const hideProfileSettings = () => {\n    setIsProfileSettingsVisible(false);\n  };\n\n  const onSave = () => {\n    saveProfile();\n    hideProfileSettings();\n  };\n\n  const onCancel = () => {\n    cancelChanges();\n    hideProfileSettings();\n  };\n\n  return (\n    <>\n      <CommonCard\n        title={title}\n        noDataDescription={noDataDescription}\n        icon={icon}\n        actions={actions}\n        content={content}\n        handleEdit={isEditingModeEnabled ? showProfileSettings : undefined}\n      />\n      {isEditingModeEnabled ? (\n        <ProfileSettingsModal\n          isSettingsVisible={isProfileSettingsVisible}\n          settingsTitle={settingsTitle}\n          content={profileSettingsContent}\n          onSave={onSave}\n          onCancel={onCancel}\n          isSaveDisabled={isSaveDisabled}\n        />\n      ) : null}\n    </>\n  );\n};\n\nexport default CommonCardWithSettingsModal;\n"
  },
  {
    "path": "client/src/components/Profile/ContactsCard.tsx",
    "content": "import { useEffect, useMemo, useState } from 'react';\nimport { List, theme, Typography } from 'antd';\nimport ContactsOutlined from '@ant-design/icons/ContactsOutlined';\nimport isEqual from 'lodash/isEqual';\nimport { UpdateProfileInfoDto } from '@client/api';\nimport { Contacts } from '@common/models/profile';\nimport { NotificationChannel } from '@client/modules/Notifications/services/notifications';\nimport { EmailConfirmation } from './EmailConfirmation';\nimport CommonCardWithSettingsModal from './CommonCardWithSettingsModal';\nimport { Contact, ContactsKeys } from '@client/services/user';\nimport ContactsCardForm from './ContactsCardForm';\nimport { epamEmailPattern } from '@client/services/validators';\n\nconst { Paragraph, Text } = Typography;\n\ntype ConnectionValue = {\n  value: string;\n  enabled: boolean;\n  lastLinkSentAt?: string;\n};\n\ntype Connections = Partial<Record<NotificationChannel, ConnectionValue | undefined>>;\n\ntype Props = {\n  data: Contacts;\n  isEditingModeEnabled: boolean;\n  connections: Connections;\n  sendConfirmationEmail: () => void;\n  updateProfile: (data: UpdateProfileInfoDto) => Promise<boolean>;\n};\n\nconst ContactsCard = ({ connections, data, isEditingModeEnabled, sendConfirmationEmail, updateProfile }: Props) => {\n  const [displayValues, setDisplayValues] = useState(data);\n  const [values, setValues] = useState(displayValues);\n  const [hasError, setHasError] = useState(false);\n  const { email, epamEmail, telegram, phone, skype, notes, linkedIn, whatsApp } = displayValues;\n  const [isSaveDisabled, setIsSaveDisabled] = useState(true);\n\n  const contacts: Contact[] = useMemo(\n    () => [\n      {\n        name: 'EPAM E-mail',\n        value: epamEmail,\n        key: ContactsKeys.EpamEmail,\n        rules: [{ pattern: epamEmailPattern, message: 'Please enter valid Epam email' }],\n      },\n      {\n        name: 'E-mail',\n        value: email,\n        key: ContactsKeys.Email,\n        rules: [{ type: 'email', message: 'Email is not valid' }],\n      },\n      {\n        name: 'Telegram',\n        value: telegram,\n        key: ContactsKeys.Telegram,\n      },\n      {\n        name: 'Phone',\n        value: phone,\n        key: ContactsKeys.Phone,\n      },\n      {\n        name: 'Skype',\n        value: skype,\n        key: ContactsKeys.Skype,\n      },\n      {\n        name: 'WhatsApp',\n        value: whatsApp,\n        key: ContactsKeys.WhatsApp,\n      },\n      {\n        name: 'Notes',\n        value: notes,\n        key: ContactsKeys.Notes,\n      },\n      {\n        name: 'LinkedIn',\n        value: linkedIn,\n        key: ContactsKeys.LinkedIn,\n      },\n    ],\n    [displayValues],\n  );\n\n  const filledContacts = contacts.filter(({ value }: Contact) => value);\n\n  const handleSave = async () => {\n    const { email, epamEmail, telegram, phone, skype, notes, linkedIn, whatsApp } = values;\n    const updatedContacts: UpdateProfileInfoDto = {\n      contactsEpamEmail: epamEmail,\n      contactsEmail: email,\n      contactsTelegram: telegram,\n      contactsPhone: phone,\n      contactsSkype: skype,\n      contactsNotes: notes,\n      contactsLinkedIn: linkedIn,\n      contactsWhatsApp: whatsApp,\n    };\n\n    const isUpdated = await updateProfile(updatedContacts);\n\n    if (!isUpdated) {\n      return;\n    }\n\n    setDisplayValues(values);\n  };\n\n  const handleCancel = () => {\n    setValues(displayValues);\n    setHasError(false);\n  };\n\n  const content = filledContacts.length ? (\n    <List\n      itemLayout=\"horizontal\"\n      dataSource={filledContacts}\n      renderItem={({ name, value, key }: Contact) => (\n        <List.Item>\n          <Text strong>{name}:</Text>{' '}\n          {key !== ContactsKeys.LinkedIn ? (\n            <>\n              {value}\n              {key === ContactsKeys.Email &&\n              (!connections.email || !connections.email.enabled) &&\n              isEditingModeEnabled ? (\n                <EmailConfirmation connection={connections.email} sendConfirmationEmail={sendConfirmationEmail} />\n              ) : null}\n            </>\n          ) : value ? (\n            <Text ellipsis>\n              <a target=\"__blank\" href={value}>\n                {value}\n              </a>\n            </Text>\n          ) : null}\n        </List.Item>\n      )}\n    />\n  ) : null;\n\n  useEffect(() => {\n    const readyToUpdate = !isEqual(displayValues, values) && !hasError;\n    setIsSaveDisabled(!readyToUpdate);\n  }, [hasError, values, displayValues]);\n\n  const { token } = theme.useToken();\n\n  const noDataDescriptionWithNote: JSX.Element = (\n    <Paragraph style={{ color: token.colorTextDisabled }}>\n      Contacts aren't filled in.\n      <br />\n      <br />\n      <Text strong type=\"secondary\">\n        Your contact information will only be visible to course coordinators, RS School admins, and the mentor assigned\n        to you.\n      </Text>\n      <br />\n      Please make sure to provide at least one reliable way to contact you.\n      <br />\n      <Text strong type=\"secondary\">\n        Telegram is preferred.\n      </Text>\n    </Paragraph>\n  );\n\n  return (\n    <CommonCardWithSettingsModal\n      title=\"Contacts\"\n      icon={<ContactsOutlined />}\n      content={content}\n      noDataDescription={noDataDescriptionWithNote}\n      isEditingModeEnabled={isEditingModeEnabled}\n      saveProfile={handleSave}\n      cancelChanges={handleCancel}\n      isSaveDisabled={isSaveDisabled}\n      profileSettingsContent={<ContactsCardForm contacts={contacts} setValues={setValues} setHasError={setHasError} />}\n    />\n  );\n};\n\nexport default ContactsCard;\n"
  },
  {
    "path": "client/src/components/Profile/ContactsCardForm.tsx",
    "content": "import { Form, Input, List, Typography } from 'antd';\nimport { Contacts } from '@common/models/profile';\nimport { Contact } from '@client/services/user';\n\nconst { Text } = Typography;\n\ntype FormProps = {\n  contacts: Contact[];\n  setValues: React.Dispatch<React.SetStateAction<Contacts>>;\n  setHasError: React.Dispatch<React.SetStateAction<boolean>>;\n};\n\nconst ContactsCardForm = ({ contacts, setValues, setHasError }: FormProps) => {\n  const [form] = Form.useForm();\n\n  const handleChanges = () => {\n    form.validateFields().catch(({ errorFields }) => setHasError(!!errorFields?.length));\n\n    const values: Contacts = form.getFieldsValue();\n    setValues(values);\n  };\n\n  return (\n    <Form\n      form={form}\n      onValuesChange={handleChanges}\n      initialValues={Object.fromEntries(contacts.map(c => [c.key, c.value]))}\n    >\n      <List<Contact>\n        itemLayout=\"horizontal\"\n        dataSource={contacts}\n        renderItem={({ name, key, rules }: Contact) => (\n          <List.Item>\n            <label style={{ width: '100%' }}>\n              <Text style={{ fontSize: 18, marginBottom: 5, display: 'inline-block' }} strong>\n                {name}:\n              </Text>\n              <Form.Item rules={rules} name={key}>\n                <Input style={{ width: '100%' }} />\n              </Form.Item>\n            </label>\n          </List.Item>\n        )}\n      />\n    </Form>\n  );\n};\n\nexport default ContactsCardForm;\n"
  },
  {
    "path": "client/src/components/Profile/DiscordCard.tsx",
    "content": "import { Typography } from 'antd';\nimport { WarningTwoTone } from '@ant-design/icons';\nimport { Discord } from '@common/models/profile';\nimport discordIntegration from '../../configs/discord-integration';\nimport { DiscordOutlined } from '@client/shared/components/Icons/DiscordOutlined';\nimport CommonCard from './CommonCard';\nimport { StudentDiscord } from '@client/components/StudentDiscord';\n\nconst { Paragraph } = Typography;\n\ntype Props = {\n  data: Discord | null;\n  isProfileOwner: boolean;\n};\n\nconst DiscordCard: React.FC<Props> = ({ data, isProfileOwner }) => {\n  const authorizedAsMessage = isProfileOwner ? 'You are authorized as' : 'The user is authorized as';\n  const notAuthorizedMessage = isProfileOwner ? `You haven't authorized yet` : `The user hasn't authorized yet`;\n\n  return (\n    <CommonCard\n      title=\"Discord Integration\"\n      icon={<DiscordOutlined />}\n      content={\n        <>\n          <Paragraph>\n            {data?.id ? (\n              <>\n                {authorizedAsMessage}:\n                <br />\n                <StudentDiscord discord={data} />\n              </>\n            ) : (\n              <>\n                <WarningTwoTone twoToneColor=\"#ff4d4f\" /> {notAuthorizedMessage}\n              </>\n            )}\n          </Paragraph>\n          {isProfileOwner && (\n            <Paragraph>\n              {data?.id && 'Switch to another Discord account:'}\n              <br />\n              <a href={discordIntegration.api.auth}>{data?.id ? 'Reauthorize' : 'Authorize'}</a>\n            </Paragraph>\n          )}\n        </>\n      }\n    />\n  );\n};\n\nexport default DiscordCard;\n"
  },
  {
    "path": "client/src/components/Profile/EducationCard.tsx",
    "content": "import { ChangeEvent, useMemo, useState } from 'react';\nimport { Typography, List, Input, Button } from 'antd';\nimport { ReadOutlined, FileAddOutlined, DeleteOutlined } from '@ant-design/icons';\nimport isEqual from 'lodash/isEqual';\nimport CommonCardWithSettingsModal from './CommonCardWithSettingsModal';\nimport { Education, UpdateProfileInfoDto } from '@client/api';\n\nconst { Text } = Typography;\n\ntype UniversityProps = {\n  university: string | null;\n  faculty: string | null;\n  graduationYear: number | string | null;\n};\n\ntype Props = {\n  data: UniversityProps[];\n  isEditingModeEnabled: boolean;\n  updateProfile: (data: UpdateProfileInfoDto) => Promise<boolean>;\n};\n\nconst hasEmptyFields = (universities: UniversityProps[]) =>\n  universities.some(({ university, faculty, graduationYear }) => !university || !faculty || !graduationYear);\n\nconst EducationCard = ({ isEditingModeEnabled, data, updateProfile }: Props) => {\n  const [displayUniversities, setDisplayUniversities] = useState(data);\n  const [universities, setUniversities] = useState(displayUniversities);\n  const isAddDisabled = useMemo(() => !!universities.length && hasEmptyFields(universities), [universities]);\n  const isSaveDisabled = useMemo(\n    () => isEqual(displayUniversities, universities) || hasEmptyFields(universities),\n    [displayUniversities, universities],\n  );\n\n  const handleChange = (e: ChangeEvent<HTMLInputElement>, field: keyof UniversityProps, index: number) => {\n    const { value } = e.target;\n\n    setUniversities(prev => {\n      const university = prev[index];\n      if (university) {\n        university[field] = value;\n      }\n      return [...prev];\n    });\n  };\n\n  const handleSave = async () => {\n    const educationHistory = universities as Education[];\n    const isUpdated = await updateProfile({ educationHistory });\n\n    if (!isUpdated) {\n      return;\n    }\n\n    setDisplayUniversities(universities);\n  };\n\n  const handleCancel = () => {\n    setUniversities(displayUniversities);\n  };\n\n  const addUniversity = () => {\n    const emptyUniversity = {\n      university: null,\n      faculty: null,\n      graduationYear: null,\n    };\n\n    setUniversities(prev => [...prev, emptyUniversity]);\n  };\n\n  const handleDelete = (index: number) => {\n    setUniversities(prev => prev.filter((_, i) => i !== index));\n  };\n\n  const renderSettingsItem = ({ graduationYear, university, faculty }: UniversityProps, index: number) => {\n    const fields = [\n      {\n        label: 'University:',\n        value: university ?? '',\n        onChange: (event: ChangeEvent<HTMLInputElement>) => handleChange(event, 'university', index),\n      },\n      {\n        label: 'Faculty:',\n        value: faculty ?? '',\n        onChange: (event: ChangeEvent<HTMLInputElement>) => handleChange(event, 'faculty', index),\n      },\n      {\n        label: 'Graduation year:',\n        value: graduationYear ?? '',\n        onChange: (event: ChangeEvent<HTMLInputElement>) => handleChange(event, 'graduationYear', index),\n      },\n    ];\n\n    return (\n      <List.Item>\n        <div style={{ width: '100%' }}>\n          <p style={{ marginBottom: 5 }}>\n            {university && faculty && graduationYear ? (\n              <>\n                <Text strong>{graduationYear}</Text> {`${university} / ${faculty}`}\n              </>\n            ) : (\n              '(Empty)'\n            )}\n          </p>\n          <p style={{ marginBottom: 10 }}>\n            <Button size=\"small\" type=\"dashed\" onClick={() => handleDelete(index)}>\n              <DeleteOutlined /> Delete\n            </Button>\n          </p>\n          {fields.map(({ label, value, onChange }, id) => (\n            <label key={id} style={{ fontSize: 18, marginBottom: 10, display: 'block' }}>\n              <Text style={{ fontSize: 18, marginBottom: 5, display: 'block' }} strong>\n                {label}\n              </Text>\n              <Input value={value} style={{ width: '100%' }} onChange={onChange} />\n            </label>\n          ))}\n        </div>\n      </List.Item>\n    );\n  };\n\n  const renderContentItem = ({ graduationYear, university, faculty }: UniversityProps) => (\n    <List.Item>\n      <p>\n        {graduationYear && university && faculty ? (\n          <>\n            <Text strong>{graduationYear}</Text> {` ${university} / ${faculty}`}\n          </>\n        ) : (\n          '(Empty)'\n        )}\n      </p>\n    </List.Item>\n  );\n\n  return (\n    <CommonCardWithSettingsModal\n      title=\"Education\"\n      icon={<ReadOutlined />}\n      noDataDescription=\"Education history isn't filled in\"\n      isEditingModeEnabled={isEditingModeEnabled}\n      saveProfile={handleSave}\n      cancelChanges={handleCancel}\n      isSaveDisabled={isSaveDisabled}\n      content={\n        displayUniversities.length ? (\n          <List itemLayout=\"horizontal\" dataSource={displayUniversities} renderItem={renderContentItem} />\n        ) : null\n      }\n      profileSettingsContent={\n        <>\n          <List itemLayout=\"horizontal\" dataSource={universities} renderItem={renderSettingsItem} />\n          <Button type=\"dashed\" style={{ width: '100%' }} onClick={addUniversity} disabled={isAddDisabled}>\n            <FileAddOutlined /> Add new university\n          </Button>\n        </>\n      }\n    />\n  );\n};\n\nexport default EducationCard;\n"
  },
  {
    "path": "client/src/components/Profile/EmailConfirmation.tsx",
    "content": "import { Alert } from 'antd';\nimport { Timer } from '@client/shared/components/Timer';\nimport dayjs from 'dayjs';\nimport { useCallback, useEffect, useState } from 'react';\n\nexport type Connection = {\n  value: string;\n  enabled: boolean;\n  lastLinkSentAt?: string;\n};\n\ntype Props = {\n  connection?: Connection;\n  sendConfirmationEmail: () => void;\n};\n\nexport function EmailConfirmation({ connection, sendConfirmationEmail }: Props) {\n  const [lastSent, setLastSent] = useState(connection?.lastLinkSentAt);\n  const allowedToResend = !lastSent ? true : dayjs().diff(dayjs(lastSent), 'seconds') > 60;\n\n  useEffect(() => {\n    setLastSent(connection?.lastLinkSentAt);\n  }, [connection?.lastLinkSentAt]);\n\n  const sendEmailConfirmationLink = useCallback(() => {\n    sendConfirmationEmail();\n    setLastSent(new Date().toISOString());\n  }, []);\n\n  return (\n    <Alert\n      type=\"error\"\n      message={\n        <div>\n          Email is not verified.{' '}\n          {allowedToResend && (\n            <span style={{ textDecoration: 'underline', cursor: 'pointer' }} onClick={sendEmailConfirmationLink}>\n              Send confirmation email?\n            </span>\n          )}\n          {!allowedToResend && (\n            <span>\n              Send confirmation email in{' '}\n              <Timer\n                seconds={60 - dayjs().diff(dayjs(lastSent), 'seconds')}\n                onElapsed={() => {\n                  setLastSent(undefined);\n                }}\n              />\n            </span>\n          )}\n        </div>\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/InterviewCard.tsx",
    "content": "import CommonCard from '@client/components/Profile/CommonCard';\nimport { QuestionCircleOutlined } from '@ant-design/icons';\nimport { CSSProperties, ReactNode, useState } from 'react';\nimport { Empty, Flex, List, theme, Typography } from 'antd';\nimport { DecisionTag, getRating } from '@client/domain/interview';\nimport { Decision } from '@client/data/interviews/technical-screening';\nimport {\n  DateWidget,\n  ExpandButtonWidget,\n  InterviewerWidget,\n  IsGoodCandidateWidget,\n  ScoreWidget,\n} from '@client/components/Profile/ui/';\nimport { CoreJsInterviewFeedback, StageInterviewDetailedFeedback } from '@common/models';\nimport InterviewModal from '@client/components/Profile/InterviewModal';\nimport { Rating } from '@client/shared/components/Rating';\n\nconst { Text } = Typography;\n\ntype InterviewCardProps = {\n  coreJsInterview?: CoreJsInterviewFeedback[];\n  prescreeningInterview?: StageInterviewDetailedFeedback[];\n};\n\ntype CardRenderProps<T> = {\n  cardData?: T[];\n  setModalData: (idx: number, data?: T) => void;\n};\n\nfunction InterviewCardListItem({\n  keyIndex,\n  interviewer,\n  date,\n  onClick,\n  children,\n  styles,\n}: {\n  keyIndex: number;\n  interviewer: { name: string; githubId: string };\n  date?: string;\n  onClick: () => void;\n  children: ReactNode;\n  styles?: CSSProperties;\n}) {\n  return (\n    <List.Item\n      key={keyIndex}\n      style={{\n        flexDirection: 'column',\n        alignItems: 'flex-start',\n        gap: '0.5em',\n      }}\n    >\n      <Flex justify=\"space-between\" style={{ width: '100%' }}>\n        <Flex vertical align=\"flex-start\" gap=\"0.5em\">\n          {children}\n        </Flex>\n        <ExpandButtonWidget onClick={onClick} />\n      </Flex>\n      <Flex justify=\"space-between\" align=\"center\" wrap=\"wrap\" gap=\"1em\" style={{ width: '100%', ...styles }}>\n        <InterviewerWidget interviewer={interviewer} vertical />\n        <DateWidget date={date} />\n      </Flex>\n    </List.Item>\n  );\n}\n\nfunction renderCoreJsInterviews({ cardData, setModalData }: CardRenderProps<CoreJsInterviewFeedback>) {\n  const { token } = theme.useToken();\n\n  if (!cardData || cardData.length === 0) return null;\n\n  return (\n    <List\n      itemLayout=\"horizontal\"\n      dataSource={cardData}\n      split={false}\n      renderItem={({ courseName, locationName, interviews }, idx) =>\n        interviews.map(({ score, interviewer, name, interviewDate }, interviewIndex) => (\n          <InterviewCardListItem\n            key={interviewIndex}\n            keyIndex={interviewIndex}\n            interviewer={interviewer}\n            date={interviewDate}\n            onClick={() => setModalData(interviewIndex, cardData[idx])}\n            styles={{\n              borderBottom: interviewIndex !== interviews.length - 1 ? `1px solid ${token.colorBorder}` : 'none',\n              paddingBottom: '1.5em',\n            }}\n          >\n            <Text strong>\n              {courseName}\n              {locationName && ` / ${locationName}`}\n            </Text>\n            <Text>{name}</Text>\n            <ScoreWidget score={score} />\n          </InterviewCardListItem>\n        ))\n      }\n    />\n  );\n}\n\nfunction renderPrescreeningInterviewCard({ cardData, setModalData }: CardRenderProps<StageInterviewDetailedFeedback>) {\n  const { token } = theme.useToken();\n\n  if (!cardData || cardData.length === 0) return null;\n\n  return (\n    <List\n      itemLayout=\"horizontal\"\n      dataSource={cardData}\n      split={false}\n      renderItem={({ courseName, interviewer, score, maxScore, date, isGoodCandidate, version, decision }, idx) => (\n        <InterviewCardListItem\n          keyIndex={idx}\n          interviewer={interviewer}\n          date={date}\n          onClick={() => setModalData(idx, cardData[idx])}\n          styles={{ borderBottom: `1px solid ${token.colorBorder}`, paddingBottom: '1.5em' }}\n        >\n          <Text strong>{courseName}</Text>\n          <Text>Pre-Screening Interview</Text>\n          <DecisionTag decision={decision as Decision} />\n          <Rating rating={getRating(score, maxScore, version)} />\n          <IsGoodCandidateWidget isGoodCandidate={isGoodCandidate} />\n        </InterviewCardListItem>\n      )}\n    />\n  );\n}\n\ntype ModalProps =\n  | {\n      type: 'coreJs';\n      interviewIdx: number;\n      data?: CoreJsInterviewFeedback;\n    }\n  | {\n      type: 'preScreening';\n      interviewIdx: number;\n      data?: StageInterviewDetailedFeedback;\n    }\n  | null;\n\nexport default function InterviewCard(props: InterviewCardProps) {\n  const [modalContent, setModalContent] = useState<ModalProps>(null);\n\n  const hasPre = Array.isArray(props.prescreeningInterview) && props.prescreeningInterview.length > 0;\n  const hasCore = Array.isArray(props.coreJsInterview) && props.coreJsInterview.length > 0;\n\n  function showModal(modalData: ModalProps) {\n    setModalContent(modalData);\n  }\n\n  function hideModal() {\n    setModalContent(null);\n  }\n\n  return (\n    <>\n      {modalContent?.type === 'coreJs' && modalContent.data && (\n        <InterviewModal\n          isVisible={!!modalContent}\n          onHide={hideModal}\n          coreJs={{ data: modalContent.data, idx: modalContent.interviewIdx }}\n        />\n      )}\n      {modalContent?.type === 'preScreening' && modalContent.data && (\n        <InterviewModal\n          isVisible={!!modalContent}\n          onHide={hideModal}\n          prescreening={{ data: modalContent.data, idx: modalContent.interviewIdx }}\n        />\n      )}\n      <CommonCard\n        title=\"Interviews\"\n        icon={<QuestionCircleOutlined />}\n        content={\n          <>\n            {!hasPre && !hasCore && <Empty />}\n\n            {renderPrescreeningInterviewCard({\n              cardData: props.prescreeningInterview,\n              setModalData: (interviewIdx, data) => showModal({ type: 'preScreening', data, interviewIdx }),\n            })}\n\n            {renderCoreJsInterviews({\n              cardData: props.coreJsInterview,\n              setModalData: (interviewIdx, data) => showModal({ type: 'coreJs', data, interviewIdx }),\n            })}\n          </>\n        }\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/InterviewModal.tsx",
    "content": "import { Flex, Modal, Space, Table, Tag, Typography } from 'antd';\nimport { CoreJsInterviewFeedback, LegacyFeedback, StageInterviewDetailedFeedback } from '@common/models';\nimport { DecisionTag, getRating } from '@client/domain/interview';\nimport { Decision } from '@client/data/interviews/technical-screening';\nimport {\n  InterviewerWidget,\n  IsGoodCandidateWidget,\n  LegacyScreeningFeedback,\n  PrescreeningFeedback,\n  ScoreWidget,\n} from '@client/components/Profile/ui';\nimport { Rating } from '@client/shared/components/Rating';\n\nconst { Text } = Typography;\n\ntype InterviewModalProps = {\n  isVisible: boolean;\n  onHide: () => void;\n  coreJs?: { data: CoreJsInterviewFeedback; idx: number };\n  prescreening?: { data: StageInterviewDetailedFeedback; idx: number };\n};\n\nfunction renderCoreJsModal({ data, idx }: { data: CoreJsInterviewFeedback; idx: number }) {\n  if (!data.interviews[idx]) {\n    return null;\n  }\n  const { score, comment, answers, interviewer } = data.interviews[idx];\n  return (\n    <Flex vertical gap=\"0.5em\">\n      <ScoreWidget score={score} />\n      <InterviewerWidget interviewer={interviewer} />\n      {comment && (\n        <Text style={{ paddingBottom: '1em' }}>\n          <Text strong>Comment: </Text>\n          {comment}\n        </Text>\n      )}\n      <Table\n        data-testid=\"profile-corejs-iviews-modal-table\"\n        dataSource={answers}\n        size=\"small\"\n        rowKey=\"questionId\"\n        pagination={false}\n        columns={[\n          {\n            dataIndex: 'questionText',\n            ellipsis: true,\n          },\n          {\n            dataIndex: 'answer',\n            render: answer =>\n              answer === true ? <Tag color=\"green\">Yes</Tag> : answer === false ? <Tag color=\"red\">No</Tag> : answer,\n          },\n        ]}\n      />\n    </Flex>\n  );\n}\n\nfunction renderPreScreeningModal({ data }: { data: StageInterviewDetailedFeedback; idx: number }) {\n  if (!data) {\n    return null;\n  }\n\n  const { score, interviewer, isGoodCandidate, feedback, version, maxScore, decision } = data;\n\n  return (\n    <Flex vertical gap=\"0.5em\">\n      <Space align=\"center\" style={{ marginBlock: '0.5em' }}>\n        <DecisionTag decision={decision as Decision} />\n        <Rating rating={getRating(score, maxScore, version)} />\n      </Space>\n      <IsGoodCandidateWidget isGoodCandidate={isGoodCandidate} />\n      <InterviewerWidget interviewer={interviewer} />\n      {version === 0 && <LegacyScreeningFeedback feedback={feedback as LegacyFeedback} />}\n      {version === 1 && <PrescreeningFeedback feedback={feedback} />}\n    </Flex>\n  );\n}\n\nexport default function InterviewModal({ isVisible, onHide, coreJs, prescreening }: InterviewModalProps) {\n  const title = coreJs\n    ? `${coreJs.data.courseFullName} CoreJS Interview Feedback`\n    : `${prescreening?.data.courseFullName} Pre-Screening Interview Feedback`;\n  return (\n    <Modal title={title} open={isVisible} onCancel={onHide} footer={null} width=\"80%\">\n      {coreJs && renderCoreJsModal(coreJs)}\n      {prescreening && renderPreScreeningModal(prescreening)}\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/LanguagesCard.tsx",
    "content": "import TranslationOutlined from '@ant-design/icons/TranslationOutlined';\nimport { Form, Space, Tag, Typography } from 'antd';\nimport { useState } from 'react';\nimport { UpdateProfileInfoDto, UpdateUserDtoLanguagesEnum } from '@client/api';\nimport { getLanguageName, SelectLanguages } from '@client/components/SelectLanguages';\nimport CommonCardWithSettingsModal from './CommonCardWithSettingsModal';\n\ntype Props = {\n  data: UpdateUserDtoLanguagesEnum[];\n  isEditingModeEnabled: boolean;\n  updateProfile: (data: UpdateProfileInfoDto) => Promise<boolean>;\n};\n\ntype FormData = {\n  languages: UpdateUserDtoLanguagesEnum[];\n};\n\nconst label = (\n  <Typography.Text strong style={{ fontSize: 18 }}>\n    My Languages:\n  </Typography.Text>\n);\nconst fieldName = 'languages';\n\nconst LanguagesCard = ({ isEditingModeEnabled, data, updateProfile }: Props) => {\n  const [form] = Form.useForm<FormData>();\n  const [languages, setLanguages] = useState(data);\n\n  const handleSave = async () => {\n    const values = form.getFieldValue(fieldName);\n    const isUpdated = await updateProfile({ languages: values });\n    if (!isUpdated) {\n      return;\n    }\n\n    setLanguages(values);\n  };\n\n  const handleCancel = () => {\n    form.setFieldValue(fieldName, languages);\n  };\n\n  return (\n    <CommonCardWithSettingsModal\n      title=\"My Languages\"\n      icon={<TranslationOutlined />}\n      isEditingModeEnabled={isEditingModeEnabled}\n      noDataDescription=\"Languages are not selected\"\n      content={\n        languages.length ? (\n          <Space size={[0, 8]} wrap>\n            {languages.map(el => (\n              <Tag key={el}>{getLanguageName(el)}</Tag>\n            ))}\n          </Space>\n        ) : null\n      }\n      profileSettingsContent={\n        <Form layout=\"vertical\" form={form} initialValues={{ languages }}>\n          <Form.Item label={label} name={fieldName}>\n            <SelectLanguages />\n          </Form.Item>\n        </Form>\n      }\n      saveProfile={handleSave}\n      cancelChanges={handleCancel}\n    />\n  );\n};\n\nexport default LanguagesCard;\n"
  },
  {
    "path": "client/src/components/Profile/MainCard.tsx",
    "content": "import { ChangeEvent, useEffect, useState } from 'react';\nimport { Card, Typography, Input, Row, Col, Button } from 'antd';\nimport { GithubFilled, EnvironmentFilled, EditOutlined } from '@ant-design/icons';\nimport isEqual from 'lodash/isEqual';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { LocationSelect } from '@client/shared/components/Forms';\nimport { Location } from '@common/models/profile';\nimport ProfileSettingsModal from './ProfileSettingsModal';\nimport { UpdateProfileInfoDto } from '@client/api';\nimport { ProfileMainCardData } from '@client/services/user';\nimport ObfuscationModal from './ObfuscateConfirmationModal';\n\nconst { Title, Paragraph, Text } = Typography;\n\ntype Props = {\n  isAdmin?: boolean;\n  data: ProfileMainCardData;\n  isEditingModeEnabled: boolean;\n  updateProfile: (data: UpdateProfileInfoDto) => Promise<boolean>;\n};\n\nconst MainCard = ({ data, isAdmin, isEditingModeEnabled, updateProfile }: Props) => {\n  const { githubId, name, location, publicCvUrl } = data;\n  const [isProfileSettingsVisible, setIsProfileSettingsVisible] = useState(false);\n  const [isSaveDisabled, setIsSaveDisabled] = useState(true);\n  const [displayName, setDisplayName] = useState(name);\n  const [displayLocation, setDisplayLocation] = useState(location);\n  const [nameInputValue, setNameInputValue] = useState(displayName);\n  const [locationSelectValue, setLocationSelectValue] = useState(displayLocation);\n  const [isObfuscateModalVisible, setIsObfuscateModalVisible] = useState(false);\n\n  const showProfileSettings = () => {\n    setIsProfileSettingsVisible(true);\n  };\n\n  const hideProfileSettings = () => {\n    setIsProfileSettingsVisible(false);\n  };\n\n  const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {\n    setNameInputValue(e.target.value);\n  };\n\n  const handleLocationChange = (value: Location | null) => {\n    setLocationSelectValue(value);\n  };\n\n  const onSave = async () => {\n    const isNameChanged = displayName !== nameInputValue;\n    const isLocationChanged = !isEqual(displayLocation, locationSelectValue);\n    const updateProfileDto: UpdateProfileInfoDto = {};\n\n    if (isNameChanged) {\n      updateProfileDto.name = nameInputValue;\n    }\n\n    if (isLocationChanged) {\n      updateProfileDto.cityName = locationSelectValue?.cityName ?? null;\n      updateProfileDto.countryName = locationSelectValue?.countryName ?? null;\n    }\n\n    const isUpdated = await updateProfile(updateProfileDto);\n\n    if (isUpdated) {\n      setDisplayName(nameInputValue);\n      setDisplayLocation(locationSelectValue);\n    }\n\n    hideProfileSettings();\n  };\n\n  const onCancel = () => {\n    setNameInputValue(displayName);\n    setLocationSelectValue(displayLocation);\n    hideProfileSettings();\n  };\n\n  useEffect(() => {\n    const isNameChanged = displayName !== nameInputValue && nameInputValue?.trim();\n    const isLocationChanged = !isEqual(displayLocation, locationSelectValue);\n    const readyToUpdate = isNameChanged || isLocationChanged;\n\n    setIsSaveDisabled(!readyToUpdate);\n  }, [nameInputValue, locationSelectValue, displayName, displayLocation]);\n\n  return (\n    <>\n      <ObfuscationModal\n        open={isObfuscateModalVisible}\n        githubId={githubId}\n        setIsModalVisible={setIsObfuscateModalVisible}\n      />\n      <Card style={{ position: 'relative' }}>\n        {isEditingModeEnabled ? (\n          <EditOutlined\n            style={{ position: 'absolute', top: 18, right: 24, fontSize: 16 }}\n            key=\"main-card-actions-edit\"\n            onClick={showProfileSettings}\n          />\n        ) : null}\n        {githubId ? (\n          <GithubAvatar size={96} githubId={githubId} style={{ margin: '0 auto 10px', display: 'block' }} />\n        ) : null}\n        <Title level={1} style={{ fontSize: 24, textAlign: 'center', margin: 0 }}>\n          {displayName}\n        </Title>\n\n        <Paragraph style={{ textAlign: 'center', marginBottom: 20 }}>\n          {githubId ? (\n            <a target=\"_blank\" href={`https://github.com/${githubId}`} style={{ fontSize: 16 }}>\n              <GithubFilled /> {githubId}\n            </a>\n          ) : null}\n        </Paragraph>\n\n        <Paragraph style={{ textAlign: 'center', margin: 0 }}>\n          {displayLocation ? (\n            <span>\n              <EnvironmentFilled /> {`${displayLocation.cityName}, ${displayLocation.countryName}`}\n            </span>\n          ) : null}\n        </Paragraph>\n        {publicCvUrl ? (\n          <Paragraph style={{ textAlign: 'center', marginTop: 20 }}>\n            <a target=\"_blank\" href={publicCvUrl}>\n              Public CV\n            </a>\n          </Paragraph>\n        ) : null}\n        {isAdmin ? (\n          <Paragraph style={{ textAlign: 'center', marginTop: 20 }}>\n            <Button danger onClick={() => setIsObfuscateModalVisible(true)}>\n              Obfuscate\n            </Button>\n          </Paragraph>\n        ) : null}\n        {isEditingModeEnabled && (\n          <ProfileSettingsModal\n            isSettingsVisible={isProfileSettingsVisible}\n            onCancel={onCancel}\n            onSave={onSave}\n            isSaveDisabled={isSaveDisabled}\n            content={\n              <Row>\n                <Col style={{ width: '100%' }}>\n                  <Row>\n                    <Text strong>Name</Text>\n                  </Row>\n                  <Row style={{ marginTop: 4 }}>\n                    <Input value={nameInputValue} placeholder=\"First-name Last-name\" onChange={handleNameChange} />\n                  </Row>\n                  <Row style={{ marginTop: 24 }}>\n                    <Text strong>Location</Text>\n                  </Row>\n                  <Row style={{ marginTop: 4 }}>\n                    <LocationSelect\n                      style={{ flex: 1 }}\n                      onChange={handleLocationChange}\n                      location={locationSelectValue}\n                    />\n                  </Row>\n                </Col>\n              </Row>\n            }\n          />\n        )}\n      </Card>\n    </>\n  );\n};\n\nexport default MainCard;\n"
  },
  {
    "path": "client/src/components/Profile/MentorStatsCard.tsx",
    "content": "import { useMemo, useState } from 'react';\nimport { Button, Card, Flex, List, Space, Typography } from 'antd';\nimport CommonCard from './CommonCard';\nimport MentorStatsModal from './MentorStatsModal';\nimport { MentorStats, Student } from '@common/models/profile';\nimport { FileTextOutlined, TeamOutlined } from '@ant-design/icons';\nimport { MentorEndorsement } from '@client/modules/Profile/components/MentorEndorsement';\nimport { ExpandButtonWidget, ScoreWidget } from '@client/components/Profile/ui';\n\nconst { Text } = Typography;\n\ntype Props = {\n  isAdmin?: boolean;\n  githubId: string;\n  data: MentorStats[];\n};\n\nexport function MentorStatsCard(props: Props) {\n  const [courseIndex, setCourseIndex] = useState(0);\n  const [isMentorStatsModalVisible, setIsMentorStatsModalVisible] = useState(false);\n  const [isEndorsementModalVisible, setIsEndorsementModalVisible] = useState(false);\n\n  const showMentorStatsModal = (courseIndex: number) => {\n    setCourseIndex(courseIndex);\n    setIsMentorStatsModalVisible(true);\n  };\n\n  const hideMentorStatsModal = () => {\n    setIsMentorStatsModalVisible(false);\n  };\n\n  const stats = props.data;\n  const count = useMemo(\n    () => props.data.reduce<Student[]>((acc, cur) => acc.concat(cur.students ?? []), []).length,\n    [],\n  );\n\n  return (\n    <>\n      {stats[courseIndex] ? (\n        <MentorStatsModal\n          stats={stats[courseIndex]}\n          isVisible={isMentorStatsModalVisible}\n          onHide={hideMentorStatsModal}\n        />\n      ) : null}\n      <MentorEndorsement\n        onClose={() => setIsEndorsementModalVisible(false)}\n        open={isEndorsementModalVisible}\n        githubId={props.githubId}\n      />\n      <CommonCard\n        title=\"Mentor Statistics\"\n        icon={<TeamOutlined />}\n        content={\n          <Flex vertical gap={8}>\n            <Space>\n              <Text>Mentored Students:</Text>\n              <Text style={{ fontSize: 18 }} strong>\n                {count}\n              </Text>\n            </Space>\n            <Space>\n              <Text>Courses as Mentor:</Text>\n              <Text style={{ fontSize: 18 }} strong>\n                {stats.length}\n              </Text>\n            </Space>\n            {props.isAdmin ? (\n              <Button\n                style={{ marginBlock: 8 }}\n                onClick={() => setIsEndorsementModalVisible(true)}\n                icon={<FileTextOutlined />}\n                type=\"primary\"\n              >\n                Get Endorsement\n              </Button>\n            ) : null}\n            {stats.map(({ courseName, courseLocationName, students }, idx) => (\n              <Card\n                key={courseName}\n                type=\"inner\"\n                size=\"small\"\n                title={courseName}\n                extra={students && <ExpandButtonWidget onClick={() => showMentorStatsModal(idx)} />}\n              >\n                <Card.Meta\n                  title={courseLocationName && ` / ${courseLocationName}`}\n                  description={\n                    students ? (\n                      idx === 0 ? (\n                        <List\n                          itemLayout=\"horizontal\"\n                          dataSource={students}\n                          split={false}\n                          renderItem={({ githubId, name, totalScore }) => (\n                            <List.Item key={`mentor-students-${githubId} ${courseName}`}>\n                              <Flex vertical gap={8}>\n                                <a href={`/profile?githubId=${githubId}`}>{name}</a> <ScoreWidget score={totalScore} />\n                              </Flex>\n                            </List.Item>\n                          )}\n                        />\n                      ) : (\n                        <Text>Students number: {students.length}</Text>\n                      )\n                    ) : (\n                      <Text>Does not have students at this course yet</Text>\n                    )\n                  }\n                />\n              </Card>\n            ))}\n          </Flex>\n        }\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/MentorStatsModal.tsx",
    "content": "import { MentorStats } from '@common/models/profile';\nimport { Card, Flex, Modal, Space } from 'antd';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { GithubOutlined, LockFilled } from '@ant-design/icons';\nimport { ScoreWidget } from '@client/components/Profile/ui';\n\ntype Props = {\n  stats: MentorStats;\n  isVisible: boolean;\n  onHide: () => void;\n};\n\nconst MentorStatsModal = ({ stats, isVisible, onHide }: Props) => {\n  const { courseName, students } = stats;\n\n  return (\n    <Modal title={`${courseName} statistics`} open={isVisible} onCancel={onHide} footer={null} width={'80%'}>\n      <Flex gap={16} wrap=\"wrap\" justify=\"center\">\n        {students?.map(({ name, githubId, totalScore, repoUrl }) => {\n          const profile = `/profile?githubId=${githubId}`;\n          const githubLink = `https://github.com/${githubId}`;\n\n          return (\n            <Card key={`mentor-stats-modal-student-${githubId}`} type=\"inner\" size=\"small\" style={{ width: '20rem' }}>\n              <Card.Meta\n                avatar={<GithubAvatar githubId={githubId} size={48} />}\n                title={<a href={profile}>{name}</a>}\n                description={\n                  <Flex vertical gap={8}>\n                    <ScoreWidget score={totalScore} />\n                    <Space>\n                      <GithubOutlined />\n                      <a href={githubLink} target=\"_blank\">\n                        {githubId}\n                      </a>\n                    </Space>\n                    <Space>\n                      <LockFilled />\n                      <a href={repoUrl} target=\"_blank\">\n                        {repoUrl?.split('/').pop()}\n                      </a>\n                    </Space>\n                  </Flex>\n                }\n              />\n            </Card>\n          );\n        })}\n      </Flex>\n    </Modal>\n  );\n};\n\nexport default MentorStatsModal;\n"
  },
  {
    "path": "client/src/components/Profile/ObfuscateConfirmationModal.tsx",
    "content": "import { useState } from 'react';\nimport { Modal, Input, Typography, Space } from 'antd';\nimport { ProfileApi } from '@client/api';\n\nconst { Text, Paragraph } = Typography;\n\ntype Props = {\n  githubId: string | null;\n  setIsModalVisible: (value: boolean) => void;\n  open: boolean;\n};\n\nconst profileApi = new ProfileApi();\n\nconst ObfuscationModal = ({ githubId, setIsModalVisible, open }: Props) => {\n  const [inputValue, setInputValue] = useState('');\n  const [isInputValid, setIsInputValid] = useState(true);\n\n  const handleOk = async () => {\n    if (githubId && inputValue === githubId) {\n      await profileApi.obfuscateProfile(githubId);\n      window.location.reload();\n    } else {\n      setIsInputValid(false);\n    }\n  };\n\n  const handleCancel = () => {\n    setIsModalVisible(false);\n    setInputValue('');\n    setIsInputValid(true);\n  };\n\n  return (\n    <Modal open={open} title=\"Confirm GitHub Profile Obfuscation\" onOk={handleOk} onCancel={handleCancel}>\n      <Space direction=\"vertical\">\n        <Paragraph>\n          <Text>\n            Please type the GitHub nickname <Text strong>\"{githubId}\"</Text> to confirm obfuscation.\n          </Text>\n        </Paragraph>\n        <Paragraph type=\"warning\">\n          <Text strong type=\"danger\">\n            Warning:\n          </Text>{' '}\n          Once initiated, the obfuscation process cannot be canceled. Upon completion, all user data will be permanently\n          deleted and this action is irreversible. Please verify the GitHub nickname and proceed with extreme caution.\n        </Paragraph>\n        <Input\n          placeholder=\"Enter GitHub nickname\"\n          value={inputValue}\n          onChange={(e: React.ChangeEvent<HTMLInputElement>) => {\n            setInputValue(e.target.value);\n            setIsInputValid(true);\n          }}\n          status={isInputValid ? '' : 'error'}\n        />\n        {!isInputValid && <Text type=\"danger\">Nickname does not match. Please try again.</Text>}\n      </Space>\n    </Modal>\n  );\n};\n\nexport default ObfuscationModal;\n"
  },
  {
    "path": "client/src/components/Profile/ProfileSettingsModal.module.css",
    "content": ".modal :global(.ant-modal-content) {\n  height: inherit !important;\n}\n\n.modal :global(.ant-modal-body) {\n  max-height: calc(90vh - 80px);\n  overflow-y: auto;\n}\n"
  },
  {
    "path": "client/src/components/Profile/ProfileSettingsModal.tsx",
    "content": "import { Modal } from 'antd';\n\nimport styles from './ProfileSettingsModal.module.css';\n\ntype Props = {\n  isSettingsVisible: boolean;\n  content: JSX.Element;\n  settingsTitle?: string;\n  isLoading?: boolean;\n  isSaveDisabled?: boolean;\n  onSave: () => void;\n  onCancel?: () => void;\n};\n\nconst ProfileSettingsModal = ({\n  isSettingsVisible,\n  content,\n  settingsTitle,\n  onSave,\n  onCancel,\n  isLoading = false,\n  isSaveDisabled = false,\n}: Props) => {\n  return (\n    <Modal\n      title={settingsTitle ?? 'Profile information'}\n      open={isSettingsVisible}\n      okText=\"Save\"\n      onOk={onSave}\n      okButtonProps={{ disabled: isSaveDisabled }}\n      onCancel={onCancel}\n      confirmLoading={isLoading}\n      centered\n      className={styles.modal}\n    >\n      {content}\n    </Modal>\n  );\n};\n\nexport default ProfileSettingsModal;\n"
  },
  {
    "path": "client/src/components/Profile/PublicFeedbackCard.tsx",
    "content": "import * as React from 'react';\nimport isEqual from 'lodash/isEqual';\nimport { Typography, Tooltip } from 'antd';\nimport { Comment } from '@client/components/Comment';\nimport FullscreenOutlined from '@ant-design/icons/FullscreenOutlined';\nimport MessageOutlined from '@ant-design/icons/MessageOutlined';\nimport CommonCard from './CommonCard';\nimport PublicFeedbackModal from './PublicFeedbackModal';\nimport heroesBadges from '../../configs/heroes-badges';\nimport { PublicFeedback } from '@common/models/profile';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport dayjs from 'dayjs';\nimport relative from 'dayjs/plugin/relativeTime';\nimport HeroesCountBadge from '@client/components/Heroes/HeroesCountBadge';\n\ndayjs.extend(relative);\n\nconst { Text, Paragraph } = Typography;\n\ntype Props = {\n  data: PublicFeedback[];\n};\n\ninterface State {\n  badgesCount: {\n    [key: string]: number;\n  };\n  isPublicFeedbackModalVisible: boolean;\n}\n\nclass PublicFeedbackCard extends React.Component<Props, State> {\n  state: State = {\n    badgesCount: {},\n    isPublicFeedbackModalVisible: false,\n  };\n\n  private showPublicFeedbackModal = () => {\n    this.setState({ isPublicFeedbackModalVisible: true });\n  };\n\n  private hidePublicFeedbackModal = () => {\n    this.setState({ isPublicFeedbackModalVisible: false });\n  };\n\n  private countBadges = () => {\n    const receivedBadges = this.props.data;\n    const badgesCount: Record<string, number> = {};\n\n    receivedBadges.forEach(({ badgeId }) => {\n      if (badgeId) {\n        badgesCount[badgeId] = badgesCount[badgeId] ? badgesCount[badgeId] + 1 : 1;\n      }\n    });\n\n    return badgesCount;\n  };\n\n  shouldComponentUpdate = (_nextProps: Props, nextState: State) =>\n    !(nextState.isPublicFeedbackModalVisible === this.state.isPublicFeedbackModalVisible) ||\n    !isEqual(nextState.badgesCount, this.state.badgesCount);\n\n  componentDidMount() {\n    const badgesCount = this.countBadges();\n    this.setState({ badgesCount });\n  }\n\n  render() {\n    const badges = this.props.data;\n    const { badgesCount } = this.state;\n\n    return (\n      <>\n        <PublicFeedbackModal\n          data={badges}\n          isVisible={this.state.isPublicFeedbackModalVisible}\n          onHide={this.hidePublicFeedbackModal}\n        />\n        <CommonCard\n          title=\"Public Feedback\"\n          icon={<MessageOutlined />}\n          actions={[\n            <FullscreenOutlined key=\"card-public-feedback-button-more\" onClick={this.showPublicFeedbackModal} />,\n          ]}\n          content={\n            <>\n              <div style={{ marginBottom: 20 }}>\n                <Text strong>Total badges:</Text> {badges.length}\n              </div>\n              <div style={{ marginBottom: 30 }}>\n                {Object.entries(badgesCount).map(([badgeId, count]) => (\n                  <HeroesCountBadge key={`badge-${badgeId}`} badge={{ badgeId, count }} />\n                ))}\n              </div>\n              <div style={{ marginBottom: 0 }}>\n                <Text strong>Last feedback:</Text>\n              </div>\n              {badges.slice(0, 1).map(({ fromUser, comment, feedbackDate, badgeId }, idx) => (\n                <Comment\n                  key={`comment-${idx}`}\n                  author={<a href={`/profile?githubId=${fromUser.githubId}`}>{fromUser.name}</a>}\n                  avatar={<GithubAvatar size={48} githubId={fromUser.githubId} />}\n                  content={\n                    <>\n                      {badgeId ? (\n                        <Text strong style={{ fontSize: 12 }}>\n                          {heroesBadges[badgeId]?.name ?? ''}\n                        </Text>\n                      ) : (\n                        ''\n                      )}\n                      <Paragraph ellipsis={{ rows: 3, expandable: true }}>{comment}</Paragraph>\n                    </>\n                  }\n                  datetime={\n                    <Tooltip title={dayjs(feedbackDate).format('YYYY-MM-DD HH:mm:ss')}>\n                      <span>{dayjs(feedbackDate).fromNow()}</span>\n                    </Tooltip>\n                  }\n                />\n              ))}\n            </>\n          }\n        />\n      </>\n    );\n  }\n}\n\nexport default PublicFeedbackCard;\n"
  },
  {
    "path": "client/src/components/Profile/PublicFeedbackModal.tsx",
    "content": "import * as React from 'react';\nimport dayjs from 'dayjs';\nimport relative from 'dayjs/plugin/relativeTime';\nimport { PublicFeedback } from '@common/models/profile';\nimport { Typography, Tooltip, Modal, Row, Col } from 'antd';\nimport { Comment } from '@client/components/Comment';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport heroesBadges from '../../configs/heroes-badges';\n\ndayjs.extend(relative);\n\nconst { Text } = Typography;\n\ntype Props = {\n  data: PublicFeedback[];\n  isVisible: boolean;\n  onHide: () => void;\n};\n\nclass PublicFeedbackModal extends React.PureComponent<Props> {\n  render() {\n    const badges = this.props.data;\n\n    return (\n      <Modal\n        title=\"Public Feedback\"\n        open={this.props.isVisible}\n        onCancel={this.props.onHide}\n        footer={null}\n        width={'80%'}\n      >\n        <Row gutter={[16, 16]}>\n          {badges.map(({ fromUser, comment, feedbackDate, badgeId }, idx) => (\n            <Col key={`modal-comment-${idx}`} xs={{ span: 24 }} sm={{ span: 12 }} md={{ span: 8 }} lg={{ span: 6 }}>\n              <Comment\n                author={<a href={`/profile?githubId=${fromUser.githubId}`}>{fromUser.name}</a>}\n                avatar={<GithubAvatar size={48} githubId={fromUser.githubId} />}\n                content={\n                  <>\n                    {badgeId ? (\n                      <Text strong style={{ fontSize: 12 }}>\n                        {heroesBadges[badgeId]?.name ?? ''}\n                      </Text>\n                    ) : (\n                      ''\n                    )}\n                    <p style={{ marginBottom: 5 }}>{comment}</p>\n                  </>\n                }\n                datetime={\n                  <Tooltip title={dayjs(feedbackDate).format('YYYY-MM-DD HH:mm:ss')}>\n                    <span>{dayjs(feedbackDate).fromNow()}</span>\n                  </Tooltip>\n                }\n              />\n            </Col>\n          ))}\n        </Row>\n      </Modal>\n    );\n  }\n}\n\nexport default PublicFeedbackModal;\n"
  },
  {
    "path": "client/src/components/Profile/StudentLeaveCourse.tsx",
    "content": "import { WarningOutlined } from '@ant-design/icons';\nimport { Divider, Modal, theme, Typography, Checkbox, Space, Form, Input } from 'antd';\n\nconst { Title, Paragraph } = Typography;\n\ntype SurveyResponses = {\n  reasonForLeaving?: string[];\n  otherComment?: string;\n};\n\ntype ReasonOption = {\n  value: string;\n  labelEn: string;\n  labelRu: string;\n};\n\ntype StudentLeaveCourseProps = {\n  isOpen: boolean;\n  onOk: (surveyData: SurveyResponses) => void;\n  onCancel: () => void;\n  confirmLoading?: boolean;\n  reasonsOptions: ReasonOption[];\n};\n\nconst messages = ['Are you sure you want to leave the course?', 'Your learning will be stopped.'];\n\nexport default function StudentLeaveCourse({\n  isOpen,\n  onOk,\n  onCancel,\n  confirmLoading,\n  reasonsOptions,\n}: StudentLeaveCourseProps) {\n  const {\n    token: { colorError },\n  } = theme.useToken();\n  const [form] = Form.useForm();\n\n  const handleOkClick = () => {\n    form\n      .validateFields()\n      .then(values => {\n        onOk(values);\n      })\n      .catch(info => {\n        console.log('Validate Failed:', info);\n      });\n  };\n\n  return (\n    <Modal\n      title={\n        <Title level={4} style={{ color: colorError, margin: 0 }}>\n          <WarningOutlined style={{ marginRight: 8 }} />\n          Confirm Leaving Course\n        </Title>\n      }\n      open={isOpen}\n      onOk={handleOkClick}\n      okText=\"Leave Course\"\n      okButtonProps={{ danger: true, loading: confirmLoading }}\n      onCancel={onCancel}\n      cancelText=\"Continue studying\"\n    >\n      <>\n        {messages.map((text, i) => (\n          <Paragraph key={i}>{text}</Paragraph>\n        ))}\n\n        <Divider />\n        <Form form={form} layout=\"vertical\" name=\"survey_form\">\n          <Form.Item\n            name=\"reasonForLeaving\"\n            label=\"Why are you leaving the course?\"\n            rules={[{ required: true, message: 'Please select at least one reason.' }]}\n          >\n            <Checkbox.Group>\n              <Space direction=\"vertical\">\n                {reasonsOptions.map(option => (\n                  <Checkbox key={option.value} value={option.value}>\n                    {option.labelEn}\n                    <br />\n                    {option.labelRu}\n                  </Checkbox>\n                ))}\n              </Space>\n            </Checkbox.Group>\n          </Form.Item>\n\n          <Form.Item name=\"otherComment\" label=\"Any other comments or suggestions?\">\n            <Input.TextArea rows={4} placeholder=\"Enter your feedback here...\" />\n          </Form.Item>\n        </Form>\n      </>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/StudentStatsCard.tsx",
    "content": "import * as React from 'react';\nimport isEqual from 'lodash/isEqual';\nimport { Typography, List, Button, Progress } from 'antd';\nimport axios from 'axios';\nimport CommonCard from './CommonCard';\nimport StudentStatsModal from './StudentStatsModal';\nimport { StudentStats } from '@common/models/profile';\nimport { CourseLeaveReason } from '@client/data/course-leave-reasons';\nimport { BookOutlined, LogoutOutlined, ReloadOutlined, SafetyCertificateTwoTone } from '@ant-design/icons';\nimport { CoursesApi } from '@client/api';\nimport StudentLeaveCourse from '@client/components/Profile/StudentLeaveCourse';\nimport { ExpandButtonWidget } from '@client/components/Profile/ui/ExpandButtonWidget';\n\nconst { Text } = Typography;\n\ntype Props = {\n  data: StudentStats[];\n  isProfileOwner: boolean;\n  username: string;\n};\n\ntype State = {\n  courseIndex: number;\n  coursesProgress: number[];\n  scoredTasks: number[];\n  isStudentStatsModalVisible: boolean;\n  isExpelConfirmationModalVisible: boolean;\n  courseId?: number;\n  isLoading: boolean;\n};\n\nconst coursesService = new CoursesApi();\n\nconst reasonsOptions = [\n  {\n    value: CourseLeaveReason.TooDifficult,\n    labelEn: 'Course was too difficult',\n    labelRu: 'Курс был слишком сложным',\n  },\n  {\n    value: CourseLeaveReason.NotUseful,\n    labelEn: 'Course was not useful',\n    labelRu: 'Курс был бесполезным',\n  },\n  {\n    value: CourseLeaveReason.LackOfTime,\n    labelEn: 'Lack of time',\n    labelRu: 'Нехватка времени',\n  },\n  {\n    value: CourseLeaveReason.Other,\n    labelEn: 'Other',\n    labelRu: 'Другое',\n  },\n  {\n    value: CourseLeaveReason.NoInterest,\n    labelEn: 'Lost interest in the subject',\n    labelRu: 'Потерял интерес к предмету',\n  },\n  {\n    value: CourseLeaveReason.PoorQuality,\n    labelEn: 'Course quality was poor',\n    labelRu: 'Низкое качество курса',\n  },\n  {\n    value: CourseLeaveReason.FoundAlternative,\n    labelEn: 'Found an alternative course/opportunity',\n    labelRu: 'Нашел альтернативный курс/возможность',\n  },\n  {\n    value: CourseLeaveReason.PersonalReasons,\n    labelEn: 'Personal reasons',\n    labelRu: 'Личные причины',\n  },\n  {\n    value: CourseLeaveReason.GotJob,\n    labelEn: 'Got a job',\n    labelRu: 'Устроился на работу',\n  },\n  {\n    value: CourseLeaveReason.GotInternship,\n    labelEn: 'Got an internship',\n    labelRu: 'Устроился на стажировку',\n  },\n];\n\nclass StudentStatsCard extends React.Component<Props, State> {\n  state = {\n    courseIndex: 0,\n    coursesProgress: [],\n    scoredTasks: [],\n    isStudentStatsModalVisible: false,\n    isExpelConfirmationModalVisible: false,\n    courseId: undefined,\n    isLoading: false,\n  };\n\n  shouldComponentUpdate = (_nextProps: Props, nextState: State) =>\n    !isEqual(nextState.isStudentStatsModalVisible, this.state.isStudentStatsModalVisible) ||\n    !isEqual(nextState.isExpelConfirmationModalVisible, this.state.isExpelConfirmationModalVisible) ||\n    !isEqual(nextState.isLoading, this.state.isLoading) ||\n    !isEqual(nextState.coursesProgress, this.state.coursesProgress);\n\n  private showStudentStatsModal = (courseIndex: number) => {\n    this.setState({ courseIndex, isStudentStatsModalVisible: true });\n  };\n\n  private showExpelConfirmationModal = (courseId: number) => {\n    this.setState({\n      isExpelConfirmationModalVisible: true,\n      courseId,\n    });\n  };\n\n  private hideStudentStatsModal = () => {\n    this.setState({ isStudentStatsModalVisible: false });\n  };\n\n  private hideExpelConfirmationModal = () => {\n    this.setState({\n      isExpelConfirmationModalVisible: false,\n      courseId: undefined,\n    });\n  };\n\n  private selfExpelStudent = async (courseId: number | undefined, surveyData: any) => {\n    if (!courseId) return;\n\n    this.setState({ isLoading: true });\n\n    try {\n      await axios.post(`/api/v2/courses/${courseId}/leave`, surveyData);\n\n      window.location.reload();\n    } finally {\n      this.setState({ isLoading: false });\n    }\n  };\n  private rejoinAsStudent = async (courseId: number) => {\n    await coursesService.rejoinCourse(courseId);\n    window.location.reload();\n  };\n\n  private countScoredTasks = (tasks: { score: number }[]) => tasks.filter(({ score }) => score !== null).length;\n  private countCourseCompletionPercentage = (tasks: { score: number }[]) =>\n    Number(((tasks.filter(({ score }) => score !== null).length / tasks.length) * 100).toFixed(1));\n\n  componentDidMount() {\n    const stats = this.props.data;\n    const scoredTasks = stats.map(({ tasks }) => this.countScoredTasks(tasks));\n    const coursesProgress = stats.map(({ tasks }) => this.countCourseCompletionPercentage(tasks));\n    this.setState({ coursesProgress, scoredTasks });\n  }\n\n  render() {\n    const { isProfileOwner } = this.props;\n    const stats = this.props.data;\n    const { isStudentStatsModalVisible, courseIndex, coursesProgress } = this.state;\n    return (\n      <>\n        {stats[courseIndex] ? (\n          <StudentStatsModal\n            stats={stats[courseIndex]}\n            isVisible={isStudentStatsModalVisible}\n            onHide={this.hideStudentStatsModal}\n          />\n        ) : null}\n        <StudentLeaveCourse\n          isOpen={this.state.isExpelConfirmationModalVisible}\n          onOk={surveyData => this.selfExpelStudent(this.state.courseId, surveyData)}\n          onCancel={this.hideExpelConfirmationModal}\n          confirmLoading={this.state.isLoading}\n          reasonsOptions={reasonsOptions}\n        />\n        <CommonCard\n          title=\"Student Statistics\"\n          icon={<BookOutlined />}\n          content={\n            <List\n              itemLayout=\"horizontal\"\n              dataSource={stats}\n              renderItem={(\n                {\n                  courseName,\n                  locationName,\n                  mentor,\n                  totalScore,\n                  isExpelled,\n                  rank,\n                  isCourseCompleted,\n                  isSelfExpelled,\n                  certificateId,\n                  courseId,\n                },\n                idx,\n              ) => {\n                return (\n                  <List.Item style={{ display: 'flex', justifyContent: 'space-between' }}>\n                    <div style={{ flexGrow: 2 }}>\n                      <p style={{ marginBottom: 0 }}>\n                        <Text strong>\n                          {courseName}\n                          {locationName && ` / ${locationName}`}\n                        </Text>\n                      </p>\n                      <div style={{ width: '80%', marginBottom: 5 }}>\n                        <Progress\n                          percent={coursesProgress.length ? coursesProgress[idx] : 0}\n                          status={isExpelled ? 'exception' : isCourseCompleted ? 'success' : undefined}\n                          size=\"small\"\n                        />\n                      </div>\n                      {certificateId && (\n                        <p style={{ fontSize: 16, marginBottom: 5 }}>\n                          <SafetyCertificateTwoTone twoToneColor=\"#52c41a\" />{' '}\n                          <a target=\"__blank\" href={`/certificate/${certificateId}`}>\n                            Certificate\n                          </a>\n                        </p>\n                      )}\n                      {mentor.githubId && (\n                        <p style={{ fontSize: 12, marginBottom: 5 }}>\n                          Mentor: <a href={`/profile?githubId=${mentor.githubId}`}>{mentor.name}</a>\n                        </p>\n                      )}\n                      {rank && <p style={{ fontSize: 12, marginBottom: 5 }}>Position: {rank}</p>}\n                      <p style={{ fontSize: 12, marginBottom: 5 }}>Score: {totalScore}</p>\n\n                      {isProfileOwner && !isCourseCompleted ? (\n                        !isExpelled ? (\n                          <Button\n                            icon={<LogoutOutlined />}\n                            danger\n                            size=\"small\"\n                            onClick={() => this.showExpelConfirmationModal(courseId)}\n                          >\n                            Leave Course\n                          </Button>\n                        ) : isSelfExpelled ? (\n                          <Button\n                            icon={<ReloadOutlined />}\n                            danger\n                            size=\"small\"\n                            onClick={() => this.rejoinAsStudent(courseId)}\n                          >\n                            Back to Course\n                          </Button>\n                        ) : (\n                          <Text mark>You expelled by Course Manager or Mentor</Text>\n                        )\n                      ) : (\n                        ''\n                      )}\n                    </div>\n                    <ExpandButtonWidget onClick={this.showStudentStatsModal.bind(null, idx)} />\n                  </List.Item>\n                );\n              }}\n            />\n          }\n        />\n      </>\n    );\n  }\n}\n\nexport default StudentStatsCard;\n"
  },
  {
    "path": "client/src/components/Profile/StudentStatsModal.tsx",
    "content": "import * as React from 'react';\nimport { StudentStats } from '@common/models/profile';\nimport { Modal, Table, Typography, Row, Col } from 'antd';\n\nconst { Text } = Typography;\n\ntype Props = {\n  stats: StudentStats;\n  isVisible: boolean;\n  onHide: () => void;\n};\n\nclass StudentStatsModal extends React.PureComponent<Props> {\n  render() {\n    const { stats } = this.props;\n    const { tasks, courseFullName, mentor, totalScore, isExpelled, expellingReason, rank } = stats;\n    const courseTasks = tasks.map((task, idx) => ({ key: `student-stats-modal-task-${idx}`, ...task }));\n    const maxCourseScore = tasks.every(({ maxScore }) => maxScore)\n      ? tasks.map(({ maxScore, scoreWeight }) => maxScore * scoreWeight).reduce((acc, cur) => acc + cur)\n      : null;\n\n    return (\n      <Modal\n        title={`${courseFullName} statistics`}\n        open={this.props.isVisible}\n        onCancel={this.props.onHide}\n        footer={null}\n        width={'80%'}\n      >\n        <Row>\n          <Col style={{ marginBottom: 20 }}>\n            {mentor.githubId && (\n              <p style={{ marginBottom: 5 }}>\n                Mentor: <a href={`/profile?githubId=${mentor.githubId}`}>{mentor.name}</a>\n              </p>\n            )}\n            {rank && (\n              <p style={{ marginBottom: 5 }}>\n                Position: <Text strong>{rank}</Text>\n              </p>\n            )}\n            <p style={{ marginBottom: 5 }}>\n              Total Score: <Text mark>{totalScore}</Text>\n              {maxCourseScore && ` / ${maxCourseScore.toFixed(1)}`}\n            </p>\n            {isExpelled && expellingReason && <p style={{ marginBottom: 5 }}>Expelling reason: {expellingReason}</p>}\n          </Col>\n        </Row>\n        <Table\n          dataSource={courseTasks}\n          size=\"small\"\n          rowKey=\"key\"\n          pagination={false}\n          columns={[\n            {\n              title: 'Task',\n              dataIndex: 'name',\n              render: (task: string, { descriptionUri }) =>\n                descriptionUri ? (\n                  <a href={descriptionUri} target=\"_blank\">\n                    {task}\n                  </a>\n                ) : (\n                  task\n                ),\n            },\n            {\n              title: 'Score / Max',\n              dataIndex: 'score',\n              render: (score: string, { maxScore }) => (\n                <>\n                  <Text strong>{score !== null ? score : '-'}</Text> / {maxScore ? maxScore : '-'}\n                </>\n              ),\n            },\n            {\n              title: '*Weight',\n              dataIndex: 'scoreWeight',\n              render: (scoreWeight: number, { score }) => (\n                <Text>\n                  *{scoreWeight}\n                  {score ? (\n                    <Text>\n                      {' '}\n                      = <Text strong>{(Number(score) * scoreWeight).toFixed(2)}</Text>\n                    </Text>\n                  ) : (\n                    ''\n                  )}\n                </Text>\n              ),\n            },\n            {\n              title: 'Comment',\n              dataIndex: 'comment',\n              ellipsis: true,\n            },\n            {\n              title: 'GitHub PR Uri',\n              dataIndex: 'githubPrUri',\n              render: (uri: string) => (uri ? <a href={uri}>PR</a> : uri),\n              ellipsis: true,\n            },\n          ]}\n        />\n      </Modal>\n    );\n  }\n}\n\nexport default StudentStatsModal;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/AboutCard.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport AboutCard from '../AboutCard';\n\ndescribe('AboutCard', () => {\n  describe('Should render correctly', () => {\n    it('if \"data\" is present', () => {\n      const { container } = render(\n        <AboutCard data={'Top contributor of Rolling Scopes'} isEditingModeEnabled={false} updateProfile={vi.fn()} />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n    it('if \"data\" is not present', () => {\n      const { container } = render(<AboutCard data={''} isEditingModeEnabled={false} updateProfile={vi.fn()} />);\n\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/CommonCard.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport CommonCard from '../CommonCard';\n\ndescribe('CommonCard', () => {\n  describe('Should render correctly', () => {\n    it('if just basic props is present', () => {\n      const { container } = render(<CommonCard title=\"Test\" icon={<i>Icon</i>} content={<p>Card body</p>} />);\n      expect(container).toMatchSnapshot();\n    });\n    it('if is null content passed', () => {\n      const { container } = render(<CommonCard title=\"Test\" icon={<i>Icon</i>} content={null} />);\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/CommonCardWithSettingsModal.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport CommonCardWithSettingsModal from '../CommonCardWithSettingsModal';\n\ndescribe('CommonCardWithSettingsModal', () => {\n  describe('Should render correctly', () => {\n    it('if just basic props are present', () => {\n      const { container } = render(\n        <CommonCardWithSettingsModal\n          title=\"Test\"\n          icon={<i>Icon</i>}\n          content={<p>Card body</p>}\n          profileSettingsContent={<div>Settings content</div>}\n          isEditingModeEnabled={true}\n          saveProfile={vi.fn()}\n          cancelChanges={vi.fn()}\n        />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n    it('if null content is passed and editing mode is disabled', () => {\n      const { container } = render(\n        <CommonCardWithSettingsModal\n          title=\"Test\"\n          icon={<i>Icon</i>}\n          content={null}\n          profileSettingsContent={<div>Settings content</div>}\n          isEditingModeEnabled={false}\n          saveProfile={vi.fn()}\n          cancelChanges={vi.fn()}\n        />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/ContactsCard.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport ContactsCard from '../ContactsCard';\n\ndescribe('ContactsCard', () => {\n  describe('Should render correctly', () => {\n    it('if editing mode is disabled', () => {\n      const { container } = render(\n        <ContactsCard\n          data={{\n            epamEmail: 'vasya@epam.com',\n            phone: '1232422',\n            email: 'vasya@tut.by',\n            skype: 'skype_vasya',\n            telegram: 'televasya',\n            notes: 'vasya',\n            linkedIn: 'http://linkedin_test.com/vasya',\n            whatsApp: '1234567890',\n          }}\n          isEditingModeEnabled={false}\n          sendConfirmationEmail={vi.fn()}\n          connections={{}}\n          updateProfile={vi.fn()}\n        />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n    it('if editing mode is enabled', () => {\n      const { container } = render(\n        <ContactsCard\n          data={{\n            epamEmail: 'vasya@epam.com',\n            phone: '1232422',\n            email: 'vasya@tut.by',\n            skype: 'skype_vasya',\n            telegram: null,\n            notes: null,\n            linkedIn: null,\n            whatsApp: '1234567890',\n          }}\n          isEditingModeEnabled={true}\n          sendConfirmationEmail={vi.fn()}\n          connections={{}}\n          updateProfile={vi.fn()}\n        />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/ContactsCardForm.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport ContactsCardForm from '../ContactsCardForm';\nimport { Contact, ContactsKeys } from '@client/services/user';\n\ndescribe('ContactsCardForm', () => {\n  const contacts: Contact[] = [\n    {\n      name: 'EPAM E-mail',\n      value: 'epamEmail',\n      key: ContactsKeys.EpamEmail,\n      rules: [{ type: 'email', message: 'Email is not valid' }],\n    },\n    {\n      name: 'E-mail',\n      value: 'email',\n      key: ContactsKeys.Email,\n      rules: [{ type: 'email', message: 'Email is not valid' }],\n    },\n  ];\n\n  describe('Should render correctly', () => {\n    it('if \"contacts\" is not empty', () => {\n      const { container } = render(<ContactsCardForm contacts={contacts} setHasError={vi.fn()} setValues={vi.fn()} />);\n      expect(container).toMatchSnapshot();\n    });\n    it('if \"contacts\" is empty', () => {\n      const { container } = render(<ContactsCardForm contacts={[]} setHasError={vi.fn()} setValues={vi.fn()} />);\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/EducationCard.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport EducationCard from '../EducationCard';\n\ndescribe('EducationCard', () => {\n  describe('Should render correctly', () => {\n    const mockData = [{ graduationYear: 2002, faculty: 'POIT', university: 'MIT' }];\n\n    it('if editing mode is disabled', () => {\n      const { container } = render(\n        <EducationCard data={mockData} isEditingModeEnabled={false} updateProfile={vi.fn()} />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n\n    it('if editing mode is enabled', () => {\n      const { container } = render(\n        <EducationCard data={mockData} isEditingModeEnabled={true} updateProfile={vi.fn()} />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n\n    it('if \"data\" has element with \"null\" value', () => {\n      const { container } = render(\n        <EducationCard\n          data={[{ graduationYear: null, faculty: null, university: null }]}\n          isEditingModeEnabled={true}\n          updateProfile={vi.fn()}\n        />,\n      );\n      expect(container).toMatchSnapshot();\n    });\n\n    it('if \"data\" is empty', () => {\n      const { container } = render(<EducationCard data={[]} isEditingModeEnabled={false} updateProfile={vi.fn()} />);\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/InterviewCard.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport InterviewCard from '../InterviewCard';\nimport { CoreJsInterviewFeedback, StageInterviewDetailedFeedback } from '@common/models/profile';\nimport { getStudentCoreJSInterviews } from '@client/utils/profilePageUtils';\n\ndescribe('InterviewCard', () => {\n  it('renders Empty when no data is provided', () => {\n    render(<InterviewCard />);\n    expect(screen.getByText(/No Data/i, { selector: ':not(title)' })).toBeInTheDocument();\n    expect(screen.getByText(/Interviews/i)).toBeInTheDocument();\n  });\n\n  it('renders Empty when review lists are empty', () => {\n    render(<InterviewCard prescreeningInterview={[]} coreJsInterview={getStudentCoreJSInterviews([])} />);\n    expect(screen.getByText(/No Data/i, { selector: ':not(title)' })).toBeInTheDocument();\n    expect(screen.getByText(/Interviews/i)).toBeInTheDocument();\n  });\n\n  it('renders Pre-Screening interviews and opens modal on expand', async () => {\n    const data: StageInterviewDetailedFeedback[] = [\n      {\n        decision: 'yes',\n        isGoodCandidate: true,\n        courseName: 'rs-2020-q1',\n        courseFullName: 'Rolling Scopes School 2020 Q1',\n        score: 34,\n        maxScore: 50,\n        date: '2020-05-17',\n        version: 0,\n        interviewer: { name: 'Inter Viewer', githubId: 'interviewer' },\n        feedback: {\n          comment: 'Legacy feedback',\n          english: 'b1',\n          programmingTask: { task: 'FizzBuzz', codeWritingLevel: 3, resolved: 1, comment: 'ok' },\n          skills: { htmlCss: 3, common: 3, dataStructures: 3 },\n        },\n      },\n    ];\n\n    render(<InterviewCard prescreeningInterview={data} />);\n\n    expect(screen.getByText('rs-2020-q1')).toBeInTheDocument();\n    expect(screen.getByText('Pre-Screening Interview')).toBeInTheDocument();\n    expect(screen.getByText('Completed')).toBeInTheDocument();\n    expect(screen.getByText('3.40')).toBeInTheDocument();\n\n    const expandButton = screen.getByTestId('expand-button');\n    expect(expandButton).toBeInTheDocument();\n    await userEvent.click(expandButton);\n\n    expect(\n      await screen.findByText(/Rolling Scopes School 2020 Q1 Pre-Screening Interview Feedback/),\n    ).toBeInTheDocument();\n  });\n\n  it('renders CoreJS interviews and opens modal on expand', async () => {\n    const coreJsData: CoreJsInterviewFeedback[] = [\n      {\n        courseName: 'JS Course',\n        courseFullName: 'JS Course 2021',\n        locationName: 'EU',\n        interviews: [\n          {\n            name: 'CoreJS Interview A',\n            score: 42,\n            comment: 'Great student',\n            answers: [\n              { questionId: 'q1', questionText: 'Q1', answer: true },\n              { questionId: 'q2', questionText: 'Q2', answer: false },\n            ],\n            interviewer: { name: 'Alice', githubId: 'alice' },\n            interviewDate: '2021-06-01',\n          },\n        ],\n      },\n    ];\n\n    render(<InterviewCard coreJsInterview={coreJsData} />);\n\n    expect(screen.getByText(/JS Course/i)).toBeInTheDocument();\n    expect(screen.getByText('CoreJS Interview A')).toBeInTheDocument();\n    expect(screen.getByText(/Score:/i)).toBeInTheDocument();\n\n    const expandButton = screen.getByTestId('expand-button');\n    expect(expandButton).toBeInTheDocument();\n    await userEvent.click(expandButton);\n\n    expect(await screen.findByText(/JS Course 2021 CoreJS Interview Feedback/)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/InterviewModal.test.tsx",
    "content": "import { render, screen, within } from '@testing-library/react';\nimport InterviewModal from '../InterviewModal';\nimport { CoreJsInterviewFeedback, LegacyFeedback, StageInterviewDetailedFeedback } from '@common/models/profile';\n\nconst noop = () => {};\n\ndescribe('InterviewModal', () => {\n  it('renders CoreJS modal with score, interviewer, comment and answers table', async () => {\n    const coreJsData: CoreJsInterviewFeedback = {\n      courseName: 'JS Course',\n      courseFullName: 'JS Course 2021',\n      locationName: 'EU',\n      interviews: [\n        {\n          name: 'CoreJS Interview A',\n          score: 42,\n          comment: 'Great student',\n          answers: [\n            { questionId: 'q1', questionText: 'Understands closures?', answer: true },\n            { questionId: 'q2', questionText: 'Knows event loop?', answer: false },\n          ],\n          interviewer: { name: 'Alice', githubId: 'alice' },\n          interviewDate: '2021-06-01',\n        },\n      ],\n    };\n\n    render(<InterviewModal isVisible={true} onHide={noop} coreJs={{ data: coreJsData, idx: 0 }} />);\n\n    expect(screen.getByText(/JS Course 2021 CoreJS Interview Feedback/)).toBeInTheDocument();\n    expect(screen.getByText(/Score:/)).toBeInTheDocument();\n    expect(screen.getByText('42')).toBeInTheDocument();\n    expect(screen.getByText(/Interviewer/)).toBeInTheDocument();\n    expect(screen.getByText('Alice')).toBeInTheDocument();\n    expect(screen.getByText(/Great student/)).toBeInTheDocument();\n\n    const table = screen.getByTestId('profile-corejs-iviews-modal-table');\n    const rows = within(table).getAllByRole('row');\n    expect(rows.length).toBeGreaterThan(1);\n    expect(screen.getByText('Yes')).toBeInTheDocument();\n    expect(screen.getByText('No')).toBeInTheDocument();\n  });\n\n  it('renders Pre-Screening (legacy v0) modal content', () => {\n    const legacyFeedback: LegacyFeedback = {\n      comment: 'Legacy comment',\n      english: 'b1',\n      programmingTask: { task: 'FizzBuzz', codeWritingLevel: 3, resolved: 1, comment: 'ok' },\n      skills: { htmlCss: 3, common: 2, dataStructures: 4 },\n    };\n\n    const data: StageInterviewDetailedFeedback = {\n      decision: 'yes',\n      isGoodCandidate: true,\n      courseName: 'rs-2020-q1',\n      courseFullName: 'Rolling Scopes School 2020 Q1',\n      score: 34,\n      maxScore: 50,\n      date: '2020-05-17',\n      version: 0,\n      interviewer: { name: 'Inter Viewer', githubId: 'interviewer' },\n      feedback: legacyFeedback,\n    };\n\n    render(<InterviewModal isVisible={true} onHide={noop} prescreening={{ data, idx: 0 }} />);\n\n    expect(screen.getByText(/Rolling Scopes School 2020 Q1 Pre-Screening Interview Feedback/)).toBeInTheDocument();\n    expect(screen.getByText('Completed')).toBeInTheDocument();\n    expect(screen.getByText('3.40')).toBeInTheDocument();\n    expect(screen.getByText(/Interviewer/)).toBeInTheDocument();\n    expect(screen.getByText('Inter Viewer')).toBeInTheDocument();\n  });\n\n  it('renders Pre-Screening (v1) modal content with sections', () => {\n    const data: StageInterviewDetailedFeedback = {\n      decision: 'yes',\n      isGoodCandidate: false,\n      courseName: 'rs-2021-q1',\n      courseFullName: 'Rolling Scopes School 2021 Q1',\n      score: 40,\n      maxScore: 50,\n      date: '2021-03-21',\n      version: 1,\n      interviewer: { name: 'Bob', githubId: 'bob' },\n      feedback: {\n        steps: {\n          intro: { isCompleted: true, values: { interviewResult: 'completed' } },\n          english: {\n            isCompleted: true,\n            values: { englishCertificate: 'B2', selfAssessment: 'B2', comment: 'self learned' },\n          },\n          theory: {\n            isCompleted: true,\n            values: {\n              questions: [\n                { id: 't1', title: 'Arrays', topic: 'JS', value: 3 },\n                { id: 't2', title: 'Promises', topic: 'JS', value: 4 },\n              ],\n              comment: 'theory ok',\n            },\n          },\n          practice: {\n            isCompleted: true,\n            values: {\n              questions: [\n                { id: 'p1', title: 'Implement map', topic: 'JS', value: 3 },\n                { id: 'p2', title: 'Optimize loop', topic: 'Perf', value: 2 },\n              ],\n              comment: 'practice ok',\n            },\n          },\n          decision: { isCompleted: true, values: { redFlags: 'none', comment: 'looks ok' } },\n        },\n      },\n    };\n\n    render(<InterviewModal isVisible={true} onHide={noop} prescreening={{ data, idx: 0 }} />);\n\n    expect(screen.getByText(/Rolling Scopes School 2021 Q1 Pre-Screening Interview Feedback/)).toBeInTheDocument();\n    expect(screen.getByText('Completed')).toBeInTheDocument();\n    expect(screen.getByText('Theory')).toBeInTheDocument();\n    expect(screen.getByText('Practice')).toBeInTheDocument();\n    expect(screen.getByText(/Certified level of English/)).toBeInTheDocument();\n    expect(screen.getByText(/English level by interviewers opinion/)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/MainCard.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport { ProfileMainCardData } from '@client/services/user';\nimport MainCard from '../MainCard';\n\n// TODO: Known Issue: https://stackoverflow.com/questions/59942808/how-can-i-use-jest-coverage-in-next-js\n\nconst mockData: ProfileMainCardData = {\n  name: 'John Doe',\n  githubId: 'john-doe',\n  location: {\n    countryName: 'Belarus',\n    cityName: 'Minsk',\n  },\n  publicCvUrl: 'public-url',\n};\n\ndescribe('MainCard', () => {\n  describe('Should render correctly', () => {\n    it('if editing mode is disabled', () => {\n      const { container } = render(<MainCard data={mockData} isEditingModeEnabled={false} updateProfile={vi.fn()} />);\n      expect(container).toMatchSnapshot();\n    });\n    it('if editing mode is enabled', () => {\n      const { container } = render(<MainCard data={mockData} isEditingModeEnabled={true} updateProfile={vi.fn()} />);\n      expect(container).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/MentorStatsCard.test.tsx",
    "content": "import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { MentorStatsCard } from '../MentorStatsCard';\n\nvi.mock('@client/modules/Profile/components/MentorEndorsement', () => ({\n  MentorEndorsement: ({ open, onClose }: { open: boolean; onClose: () => void }) => (\n    <div>\n      {open ? <div data-testid=\"endorsement-open\">endorsement-open</div> : null}\n      <button onClick={onClose}>mock-close-endorsement</button>\n    </div>\n  ),\n}));\n\ndescribe('MentorStatsCard', () => {\n  const mentorStats = [\n    {\n      courseName: 'rs-2018-q1',\n      courseLocationName: 'Minsk',\n      students: [\n        {\n          githubId: 'alex',\n          name: 'Alex Petrov',\n          isExpelled: false,\n          totalScore: 3453,\n          repoUrl: 'https://github.com/rolling-scopes-school/alex-RS2018Q1',\n        },\n        {\n          githubId: 'vasya',\n          name: 'Vasiliy Alexandrov',\n          isExpelled: true,\n          totalScore: 120,\n          repoUrl: 'https://github.com/rolling-scopes-school/vasya-RS2018Q1',\n        },\n      ],\n    },\n    {\n      courseName: 'rs-2020-q1',\n      courseLocationName: 'Minsk',\n    },\n  ];\n\n  it('shows stats', () => {\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} />);\n    expect(screen.getByText('Mentored Students:')).toBeInTheDocument();\n    expect(screen.getByText('Courses as Mentor:')).toBeInTheDocument();\n  });\n\n  it('shows all courses', () => {\n    const courseNames = mentorStats.map(course => course.courseName);\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} />);\n    courseNames.forEach(course => expect(screen.getByText(course)).toBeInTheDocument());\n  });\n\n  it('shows details button for courses with students', () => {\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} />);\n    const coursesWithStudents = mentorStats.reduce((acc, c) => (c?.students?.length ? acc + 1 : acc), 0);\n    const openButtons = screen.queryAllByTitle('Open details');\n    expect(openButtons.length).toBe(coursesWithStudents);\n  });\n\n  it('shows dedicated message if no there are no students in the course', () => {\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} />);\n    expect(screen.getByText('rs-2020-q1')).toBeInTheDocument();\n    expect(screen.getByText('Does not have students at this course yet')).toBeInTheDocument();\n  });\n\n  it('shows endorsement button for admins', () => {\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} isAdmin={true} />);\n    expect(screen.getByRole('button', { name: /Get Endorsement/i })).toBeInTheDocument();\n  });\n\n  it('do not shows endorsement button for non-admin users', () => {\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} isAdmin={false} />);\n    expect(screen.queryByRole('button', { name: /Get Endorsement/i })).not.toBeInTheDocument();\n  });\n\n  it('opens MentorStatsModal for a course with students when expand is clicked', () => {\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} />);\n\n    const expandBtn = screen.getByTestId('expand-button');\n    fireEvent.click(expandBtn);\n\n    expect(screen.getByText('rs-2018-q1 statistics')).toBeInTheDocument();\n  });\n\n  it('closes MentorStatsModal when Close is clicked', async () => {\n    render(<MentorStatsCard githubId=\"test\" data={mentorStats} />);\n\n    fireEvent.click(screen.getByTestId('expand-button'));\n    expect(screen.getByText('rs-2018-q1 statistics')).toBeInTheDocument();\n\n    fireEvent.click(screen.getByRole('button', { name: 'Close' }));\n    await waitFor(() => expect(screen.getByText('rs-2018-q1 statistics')).not.toBeVisible());\n  });\n\n  it('renders students list for the first course when it has students', () => {\n    render(<MentorStatsCard githubId=\"gh\" data={mentorStats} />);\n\n    expect(screen.getByRole('link', { name: 'Alex Petrov' })).toHaveAttribute('href', '/profile?githubId=alex');\n    expect(screen.getAllByText('Score:').length).toBe(2);\n    expect(screen.getByText('3453')).toBeInTheDocument();\n  });\n\n  it('opens and closes MentorEndorsement modal via the admin button', () => {\n    render(<MentorStatsCard githubId=\"mentor\" data={mentorStats} isAdmin={true} />);\n\n    fireEvent.click(screen.getByRole('button', { name: /Get Endorsement/i }));\n    expect(screen.getByTestId('endorsement-open')).toBeInTheDocument();\n\n    fireEvent.click(screen.getByText('mock-close-endorsement'));\n    expect(screen.queryByTestId('endorsement-open')).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/MentorStatsModal.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport MentorStatsModal from '../MentorStatsModal';\nimport { MentorStats } from '@common/models';\n\ndescribe('MentorStatsModal', () => {\n  const stats = {\n    courseLocationName: 'Minsk',\n    courseName: 'RS 2018 Q1',\n    students: [\n      {\n        githubId: 'alex',\n        name: 'Alex Petrov',\n        isExpelled: false,\n        totalScore: 3453,\n        repoUrl: 'https://github.com/rolling-scopes-school/alex-RS2018Q1',\n      },\n      {\n        githubId: 'vasya',\n        name: 'Vasiliy Alexandrov',\n        isExpelled: true,\n        totalScore: 120,\n      },\n    ],\n  } as const;\n\n  it('renders title and student items with proper links and score', () => {\n    render(<MentorStatsModal stats={stats as unknown as MentorStats} isVisible={true} onHide={vi.fn()} />);\n\n    expect(screen.getByText('RS 2018 Q1 statistics')).toBeInTheDocument();\n\n    expect(screen.getByRole('link', { name: 'Alex Petrov' })).toHaveAttribute('href', '/profile?githubId=alex');\n    expect(screen.getByRole('link', { name: 'Vasiliy Alexandrov' })).toHaveAttribute('href', '/profile?githubId=vasya');\n\n    expect(screen.getAllByText('Score:').length).toBeGreaterThanOrEqual(2);\n    expect(screen.getByText('3453')).toBeInTheDocument();\n    expect(screen.getByText('120')).toBeInTheDocument();\n\n    expect(screen.getByRole('link', { name: 'alex' })).toHaveAttribute('href', 'https://github.com/alex');\n    expect(screen.getByRole('link', { name: 'vasya' })).toHaveAttribute('href', 'https://github.com/vasya');\n\n    const repoLink = screen.getByRole('link', { name: 'alex-RS2018Q1' });\n    expect(repoLink).toHaveAttribute('href', 'https://github.com/rolling-scopes-school/alex-RS2018Q1');\n  });\n\n  it('calls onHide when close button is clicked', () => {\n    const onHide = vi.fn();\n    render(<MentorStatsModal stats={stats as unknown as MentorStats} isVisible={true} onHide={onHide} />);\n\n    const closeBtn = screen.getByRole('button', { name: 'Close' });\n    fireEvent.click(closeBtn);\n    expect(onHide).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/ProfileSettingsModal.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport ProfileSettingsModal from '../ProfileSettingsModal';\n\ndescribe('ProfileSettingsModal', () => {\n  it('should render correctly', () => {\n    render(\n      <ProfileSettingsModal\n        isSettingsVisible={true}\n        content={<div>content</div>}\n        settingsTitle=\"Settings Title\"\n        onSave={vi.fn()}\n        onCancel={vi.fn()}\n      />,\n    );\n\n    expect(screen.getByRole('dialog')).toMatchSnapshot();\n  });\n\n  it('should not be rendered if isSettingsVisible === false', async () => {\n    render(\n      <ProfileSettingsModal\n        isSettingsVisible={false}\n        content={<div>content</div>}\n        settingsTitle=\"Settings Title\"\n        onSave={vi.fn()}\n        onCancel={vi.fn()}\n      />,\n    );\n\n    const modal = screen.queryByRole('dialog');\n\n    expect(modal).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/PublicFeedbackCard.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport PublicFeedbackCard from '../PublicFeedbackCard';\n\ndescribe('PublicFeedbackCard', () => {\n  const data = [\n    {\n      feedbackDate: '2018-12-01T12:12:01.000Z',\n      badgeId: 'Congratulations',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Anton Petrov',\n        githubId: 'apetr',\n      },\n    },\n    {\n      feedbackDate: '2018-11-01T11:12:01.000Z',\n      badgeId: 'Great_speaker',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Artem Petrov',\n        githubId: 'temap',\n      },\n    },\n    {\n      feedbackDate: '2018-09-01T11:12:01.000Z',\n      badgeId: 'Great_speaker',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Artem Petrov',\n        githubId: 'temap',\n      },\n    },\n    {\n      feedbackDate: '2018-10-01T11:12:01.000Z',\n      badgeId: 'Great_speaker',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Artem Petrov',\n        githubId: 'temap',\n      },\n    },\n    {\n      feedbackDate: '2018-11-01T12:12:01.000Z',\n      badgeId: 'Thank_you',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Anton Vasilyev',\n        githubId: 'vasssa',\n      },\n    },\n    {\n      feedbackDate: '2019-12-01T12:12:01.000Z',\n      badgeId: 'Thank_you',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Dima Alexandrov',\n        githubId: 'demaa',\n      },\n    },\n  ];\n\n  afterEach(() => {\n    vi.useRealTimers();\n  });\n\n  it('should render correctly', () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2019-01-01'));\n    const { container } = render(<PublicFeedbackCard data={data} />);\n    expect(container).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/PublicFeedbackModal.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport PublicFeedbackModal from '../PublicFeedbackModal';\n\ndescribe('PublicFeedbackModal', () => {\n  const data = [\n    {\n      feedbackDate: '2018-12-01T12:12:01.000Z',\n      badgeId: 'Congratulations',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Anton Petrov',\n        githubId: 'apetr',\n      },\n    },\n    {\n      feedbackDate: '2018-11-01T11:12:01.000Z',\n      badgeId: 'Great_speaker',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Artem Petrov',\n        githubId: 'temap',\n      },\n    },\n    {\n      feedbackDate: '2018-09-01T11:12:01.000Z',\n      badgeId: 'Great_speaker',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Artem Petrov',\n        githubId: 'temap',\n      },\n    },\n    {\n      feedbackDate: '2018-10-01T11:12:01.000Z',\n      badgeId: 'Great_speaker',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Artem Petrov',\n        githubId: 'temap',\n      },\n    },\n    {\n      feedbackDate: '2018-11-01T12:12:01.000Z',\n      badgeId: 'Thank_you',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Anton Vasilyev',\n        githubId: 'vasssa',\n      },\n    },\n    {\n      feedbackDate: '2019-12-01T12:12:01.000Z',\n      badgeId: 'Thank_you',\n      comment: 'Test',\n      heroesUri: 'https://heroes.by/',\n      fromUser: {\n        name: 'Dima Alexandrov',\n        githubId: 'demaa',\n      },\n    },\n  ];\n\n  beforeAll(() => {\n    vi.useFakeTimers().setSystemTime(new Date('2022-01-01T00:00:00Z').getTime());\n  });\n\n  afterAll(() => {\n    vi.useRealTimers();\n  });\n\n  it('Should render correctly', () => {\n    const { container } = render(<PublicFeedbackModal data={data} isVisible={true} onHide={vi.fn()} />);\n    expect(container).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/StudentStatsCard.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport StudentStatsCard from '../StudentStatsCard';\nimport { StudentStats } from '@common/models/profile';\n\ndescribe('StudentStatsCard', () => {\n  const githubId = 'test';\n  const data = [\n    {\n      courseId: 1,\n      courseName: 'rs-2018-q1',\n      locationName: 'Minsk',\n      courseFullName: 'Rolling Scopes School 2018 Q1',\n      isExpelled: false,\n      expellingReason: '',\n      isCourseCompleted: true,\n      totalScore: 1201,\n      rank: 32,\n      mentor: {\n        githubId: 'andrew123',\n        name: 'Andrey Andreev',\n      },\n      tasks: [\n        {\n          maxScore: 130,\n          scoreWeight: 1,\n          name: 'Task 1',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 120,\n          comment: 'test',\n        },\n        {\n          maxScore: 100,\n          scoreWeight: 1,\n          name: 'Task 2',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 20,\n          comment: 'test',\n        },\n        {\n          maxScore: 110,\n          scoreWeight: 1,\n          name: 'Task 3',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 90,\n          comment: 'test',\n        },\n      ],\n    },\n    {\n      courseId: 1,\n      courseName: 'rs-2019-q1',\n      locationName: 'Minsk',\n      courseFullName: 'Rolling Scopes School 2019 Q1',\n      isExpelled: true,\n      expellingReason: 'test',\n      isCourseCompleted: false,\n      totalScore: 101,\n      rank: 32,\n      mentor: {\n        githubId: 'dimon12',\n        name: 'Dima Testovich',\n      },\n      tasks: [\n        {\n          maxScore: 100,\n          scoreWeight: 1,\n          name: 'Task 1',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 20,\n          comment: 'test',\n        },\n        {\n          maxScore: 100,\n          scoreWeight: 1,\n          name: 'Task 2',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: null,\n          comment: null,\n        },\n        {\n          maxScore: 100,\n          scoreWeight: 1,\n          name: 'Task 3',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 10,\n          comment: 'test',\n        },\n        {\n          maxScore: 100,\n          scoreWeight: 1,\n          name: 'Task 4',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: null,\n          comment: null,\n        },\n      ],\n    },\n  ] as StudentStats[];\n\n  it('should render correctly', () => {\n    const { container } = render(<StudentStatsCard isProfileOwner={false} data={data} username={githubId} />);\n    expect(container).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/StudentStatsModal.test.tsx",
    "content": "import { render } from '@testing-library/react';\nimport StudentStatsModal from '../StudentStatsModal';\nimport { StudentStats } from '@common/models/profile';\n\ndescribe('StudentStatsModal', () => {\n  it('Should render correctly', () => {\n    const stats: StudentStats = {\n      courseId: 1,\n      courseName: 'rs-2018-q1',\n      locationName: 'Minsk',\n      courseFullName: 'Rolling Scopes School 2018 Q1',\n      isExpelled: false,\n      isSelfExpelled: false,\n      expellingReason: '',\n      isCourseCompleted: true,\n      totalScore: 1201,\n      certificateId: 'asd',\n      rank: 32,\n      mentor: {\n        githubId: 'andrew123',\n        name: 'Andrey Andreev',\n      },\n      tasks: [\n        {\n          maxScore: 130,\n          scoreWeight: 1,\n          name: 'Task 1',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 120,\n          comment: 'test',\n        },\n        {\n          maxScore: 100,\n          scoreWeight: 1,\n          name: 'Task 2',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 20,\n          comment: 'test',\n        },\n        {\n          maxScore: 110,\n          scoreWeight: 1,\n          name: 'Task 3',\n          descriptionUri: 'https://description.com',\n          githubPrUri: 'https://description.com',\n          score: 90,\n          comment: 'test',\n        },\n      ],\n    };\n\n    const { container } = render(<StudentStatsModal stats={stats} isVisible={true} onHide={vi.fn()} />);\n    expect(container).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/AboutCard.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`AboutCard > Should render correctly > if \"data\" is not present 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"info-circle\"\n                class=\"anticon anticon-info-circle\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"info-circle\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z\"\n                  />\n                  <path\n                    d=\"M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z\"\n                  />\n                </svg>\n              </span>\n               \n              About\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"css-dev-only-do-not-override-1enej14 css-var-root ant-empty ant-empty-normal\"\n      >\n        <div\n          class=\"ant-empty-image\"\n        >\n          <svg\n            height=\"41\"\n            viewBox=\"0 0 64 41\"\n            width=\"64\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>\n              No data\n            </title>\n            <g\n              fill=\"none\"\n              fill-rule=\"evenodd\"\n              transform=\"translate(0 1)\"\n            >\n              <ellipse\n                cx=\"32\"\n                cy=\"33\"\n                fill=\"#f5f5f5\"\n                rx=\"32\"\n                ry=\"7\"\n              />\n              <g\n                fill-rule=\"nonzero\"\n                stroke=\"#d9d9d9\"\n              >\n                <path\n                  d=\"M55 12.8 44.9 1.3Q44 0 42.9 0H21.1q-1.2 0-2 1.3L9 12.8V22h46z\"\n                />\n                <path\n                  d=\"M41.6 16c0-1.7 1-3 2.2-3H55v18.1c0 2.2-1.3 3.9-3 3.9H12c-1.7 0-3-1.7-3-3.9V13h11.2c1.2 0 2.2 1.3 2.2 3s1 2.9 2.2 2.9h14.8c1.2 0 2.2-1.4 2.2-3\"\n                  fill=\"#fafafa\"\n                />\n              </g>\n            </g>\n          </svg>\n        </div>\n        <div\n          class=\"ant-empty-description\"\n        >\n          About info isn't written\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`AboutCard > Should render correctly > if \"data\" is present 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"info-circle\"\n                class=\"anticon anticon-info-circle\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"info-circle\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z\"\n                  />\n                  <path\n                    d=\"M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z\"\n                  />\n                </svg>\n              </span>\n               \n              About\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        aria-label=\"Top contributor of Rolling Scopes\"\n        class=\"ant-typography ant-typography-ellipsis css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"\"\n      >\n        Top contributor of Rolling Scopes\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/CommonCard.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`CommonCard > Should render correctly > if is null content passed 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <i>\n                Icon\n              </i>\n               \n              Test\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"css-dev-only-do-not-override-1enej14 css-var-root ant-empty ant-empty-normal\"\n      >\n        <div\n          class=\"ant-empty-image\"\n        >\n          <svg\n            height=\"41\"\n            viewBox=\"0 0 64 41\"\n            width=\"64\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>\n              No data\n            </title>\n            <g\n              fill=\"none\"\n              fill-rule=\"evenodd\"\n              transform=\"translate(0 1)\"\n            >\n              <ellipse\n                cx=\"32\"\n                cy=\"33\"\n                fill=\"#f5f5f5\"\n                rx=\"32\"\n                ry=\"7\"\n              />\n              <g\n                fill-rule=\"nonzero\"\n                stroke=\"#d9d9d9\"\n              >\n                <path\n                  d=\"M55 12.8 44.9 1.3Q44 0 42.9 0H21.1q-1.2 0-2 1.3L9 12.8V22h46z\"\n                />\n                <path\n                  d=\"M41.6 16c0-1.7 1-3 2.2-3H55v18.1c0 2.2-1.3 3.9-3 3.9H12c-1.7 0-3-1.7-3-3.9V13h11.2c1.2 0 2.2 1.3 2.2 3s1 2.9 2.2 2.9h14.8c1.2 0 2.2-1.4 2.2-3\"\n                  fill=\"#fafafa\"\n                />\n              </g>\n            </g>\n          </svg>\n        </div>\n        <div\n          class=\"ant-empty-description\"\n        >\n          No data\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`CommonCard > Should render correctly > if just basic props is present 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <i>\n                Icon\n              </i>\n               \n              Test\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <p>\n        Card body\n      </p>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/CommonCardWithSettingsModal.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`CommonCardWithSettingsModal > Should render correctly > if just basic props are present 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <i>\n                Icon\n              </i>\n               \n              Test\n            </span>\n            <span\n              aria-label=\"edit\"\n              class=\"anticon anticon-edit\"\n              role=\"img\"\n              tabindex=\"-1\"\n            >\n              <svg\n                aria-hidden=\"true\"\n                data-icon=\"edit\"\n                fill=\"currentColor\"\n                focusable=\"false\"\n                height=\"1em\"\n                viewBox=\"64 64 896 896\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z\"\n                />\n              </svg>\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <p>\n        Card body\n      </p>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`CommonCardWithSettingsModal > Should render correctly > if null content is passed and editing mode is disabled 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <i>\n                Icon\n              </i>\n               \n              Test\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"css-dev-only-do-not-override-1enej14 css-var-root ant-empty ant-empty-normal\"\n      >\n        <div\n          class=\"ant-empty-image\"\n        >\n          <svg\n            height=\"41\"\n            viewBox=\"0 0 64 41\"\n            width=\"64\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>\n              No data\n            </title>\n            <g\n              fill=\"none\"\n              fill-rule=\"evenodd\"\n              transform=\"translate(0 1)\"\n            >\n              <ellipse\n                cx=\"32\"\n                cy=\"33\"\n                fill=\"#f5f5f5\"\n                rx=\"32\"\n                ry=\"7\"\n              />\n              <g\n                fill-rule=\"nonzero\"\n                stroke=\"#d9d9d9\"\n              >\n                <path\n                  d=\"M55 12.8 44.9 1.3Q44 0 42.9 0H21.1q-1.2 0-2 1.3L9 12.8V22h46z\"\n                />\n                <path\n                  d=\"M41.6 16c0-1.7 1-3 2.2-3H55v18.1c0 2.2-1.3 3.9-3 3.9H12c-1.7 0-3-1.7-3-3.9V13h11.2c1.2 0 2.2 1.3 2.2 3s1 2.9 2.2 2.9h14.8c1.2 0 2.2-1.4 2.2-3\"\n                  fill=\"#fafafa\"\n                />\n              </g>\n            </g>\n          </svg>\n        </div>\n        <div\n          class=\"ant-empty-description\"\n        >\n          No data\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/ContactsCard.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`ContactsCard > Should render correctly > if editing mode is disabled 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"contacts\"\n                class=\"anticon anticon-contacts\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"contacts\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M594.3 601.5a111.8 111.8 0 0029.1-75.5c0-61.9-49.9-112-111.4-112s-111.4 50.1-111.4 112c0 29.1 11 55.5 29.1 75.5a158.09 158.09 0 00-74.6 126.1 8 8 0 008 8.4H407c4.2 0 7.6-3.3 7.9-7.5 3.8-50.6 46-90.5 97.2-90.5s93.4 40 97.2 90.5c.3 4.2 3.7 7.5 7.9 7.5H661a8 8 0 008-8.4c-2.8-53.3-32-99.7-74.7-126.1zM512 578c-28.5 0-51.7-23.3-51.7-52s23.2-52 51.7-52 51.7 23.3 51.7 52-23.2 52-51.7 52zm416-354H768v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56H548v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56H328v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56H96c-17.7 0-32 14.3-32 32v576c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V256c0-17.7-14.3-32-32-32zm-40 568H136V296h120v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56h148v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56h148v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56h120v496z\"\n                  />\n                </svg>\n              </span>\n               \n              Contacts\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          aria-busy=\"false\"\n          aria-live=\"polite\"\n          class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-spin-container\"\n          >\n            <ul\n              class=\"ant-list-items ant-list-container css-var-root\"\n            >\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    EPAM E-mail\n                    :\n                  </strong>\n                </span>\n                 \n                vasya@epam.com\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    E-mail\n                    :\n                  </strong>\n                </span>\n                 \n                vasya@tut.by\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    Telegram\n                    :\n                  </strong>\n                </span>\n                 \n                televasya\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    Phone\n                    :\n                  </strong>\n                </span>\n                 \n                1232422\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    Skype\n                    :\n                  </strong>\n                </span>\n                 \n                skype_vasya\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    WhatsApp\n                    :\n                  </strong>\n                </span>\n                 \n                1234567890\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    Notes\n                    :\n                  </strong>\n                </span>\n                 \n                vasya\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    LinkedIn\n                    :\n                  </strong>\n                </span>\n                 \n                <span\n                  class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <a\n                    href=\"http://linkedin_test.com/vasya\"\n                    target=\"__blank\"\n                  >\n                    http://linkedin_test.com/vasya\n                  </a>\n                </span>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`ContactsCard > Should render correctly > if editing mode is enabled 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"contacts\"\n                class=\"anticon anticon-contacts\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"contacts\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M594.3 601.5a111.8 111.8 0 0029.1-75.5c0-61.9-49.9-112-111.4-112s-111.4 50.1-111.4 112c0 29.1 11 55.5 29.1 75.5a158.09 158.09 0 00-74.6 126.1 8 8 0 008 8.4H407c4.2 0 7.6-3.3 7.9-7.5 3.8-50.6 46-90.5 97.2-90.5s93.4 40 97.2 90.5c.3 4.2 3.7 7.5 7.9 7.5H661a8 8 0 008-8.4c-2.8-53.3-32-99.7-74.7-126.1zM512 578c-28.5 0-51.7-23.3-51.7-52s23.2-52 51.7-52 51.7 23.3 51.7 52-23.2 52-51.7 52zm416-354H768v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56H548v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56H328v-56c0-4.4-3.6-8-8-8h-56c-4.4 0-8 3.6-8 8v56H96c-17.7 0-32 14.3-32 32v576c0 17.7 14.3 32 32 32h832c17.7 0 32-14.3 32-32V256c0-17.7-14.3-32-32-32zm-40 568H136V296h120v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56h148v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56h148v56c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8v-56h120v496z\"\n                  />\n                </svg>\n              </span>\n               \n              Contacts\n            </span>\n            <span\n              aria-label=\"edit\"\n              class=\"anticon anticon-edit\"\n              role=\"img\"\n              tabindex=\"-1\"\n            >\n              <svg\n                aria-hidden=\"true\"\n                data-icon=\"edit\"\n                fill=\"currentColor\"\n                focusable=\"false\"\n                height=\"1em\"\n                viewBox=\"64 64 896 896\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z\"\n                />\n              </svg>\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          aria-busy=\"false\"\n          aria-live=\"polite\"\n          class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-spin-container\"\n          >\n            <ul\n              class=\"ant-list-items ant-list-container css-var-root\"\n            >\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    EPAM E-mail\n                    :\n                  </strong>\n                </span>\n                 \n                vasya@epam.com\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    E-mail\n                    :\n                  </strong>\n                </span>\n                 \n                vasya@tut.by\n                <div\n                  class=\"ant-alert ant-alert-error ant-alert-no-icon css-var-root css-dev-only-do-not-override-1enej14\"\n                  data-show=\"true\"\n                  role=\"alert\"\n                >\n                  <div\n                    class=\"ant-alert-section\"\n                  >\n                    <div\n                      class=\"ant-alert-title\"\n                    >\n                      <div>\n                        Email is not verified.\n                         \n                        <span\n                          style=\"text-decoration: underline; cursor: pointer;\"\n                        >\n                          Send confirmation email?\n                        </span>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    Phone\n                    :\n                  </strong>\n                </span>\n                 \n                1232422\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    Skype\n                    :\n                  </strong>\n                </span>\n                 \n                skype_vasya\n              </li>\n              <li\n                class=\"ant-list-item ant-list-item-no-flex\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                >\n                  <strong>\n                    WhatsApp\n                    :\n                  </strong>\n                </span>\n                 \n                1234567890\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/ContactsCardForm.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`ContactsCardForm > Should render correctly > if \"contacts\" is empty 1`] = `\n<div>\n  <form\n    class=\"ant-form ant-form-horizontal css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14\"\n  >\n    <div\n      class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n    >\n      <div\n        aria-busy=\"false\"\n        aria-live=\"polite\"\n        class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          class=\"ant-spin-container\"\n        >\n          <div\n            class=\"ant-list-empty-text\"\n          >\n            <div\n              class=\"css-dev-only-do-not-override-1enej14 css-var-root ant-empty ant-empty-normal\"\n            >\n              <div\n                class=\"ant-empty-image\"\n              >\n                <svg\n                  height=\"41\"\n                  viewBox=\"0 0 64 41\"\n                  width=\"64\"\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                >\n                  <title>\n                    No data\n                  </title>\n                  <g\n                    fill=\"none\"\n                    fill-rule=\"evenodd\"\n                    transform=\"translate(0 1)\"\n                  >\n                    <ellipse\n                      cx=\"32\"\n                      cy=\"33\"\n                      fill=\"#f5f5f5\"\n                      rx=\"32\"\n                      ry=\"7\"\n                    />\n                    <g\n                      fill-rule=\"nonzero\"\n                      stroke=\"#d9d9d9\"\n                    >\n                      <path\n                        d=\"M55 12.8 44.9 1.3Q44 0 42.9 0H21.1q-1.2 0-2 1.3L9 12.8V22h46z\"\n                      />\n                      <path\n                        d=\"M41.6 16c0-1.7 1-3 2.2-3H55v18.1c0 2.2-1.3 3.9-3 3.9H12c-1.7 0-3-1.7-3-3.9V13h11.2c1.2 0 2.2 1.3 2.2 3s1 2.9 2.2 2.9h14.8c1.2 0 2.2-1.4 2.2-3\"\n                        fill=\"#fafafa\"\n                      />\n                    </g>\n                  </g>\n                </svg>\n              </div>\n              <div\n                class=\"ant-empty-description\"\n              >\n                No data\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </form>\n</div>\n`;\n\nexports[`ContactsCardForm > Should render correctly > if \"contacts\" is not empty 1`] = `\n<div>\n  <form\n    class=\"ant-form ant-form-horizontal css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14\"\n  >\n    <div\n      class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n    >\n      <div\n        aria-busy=\"false\"\n        aria-live=\"polite\"\n        class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          class=\"ant-spin-container\"\n        >\n          <ul\n            class=\"ant-list-items ant-list-container css-var-root\"\n          >\n            <li\n              class=\"ant-list-item\"\n            >\n              <label\n                style=\"width: 100%;\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                  style=\"font-size: 18px; margin-bottom: 5px; display: inline-block;\"\n                >\n                  <strong>\n                    EPAM E-mail\n                    :\n                  </strong>\n                </span>\n                <div\n                  class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n                >\n                  <div\n                    class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n                  >\n                    <div\n                      class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n                    >\n                      <div\n                        class=\"ant-form-item-control-input\"\n                      >\n                        <div\n                          class=\"ant-form-item-control-input-content\"\n                        >\n                          <input\n                            class=\"ant-input css-dev-only-do-not-override-1enej14 ant-input-outlined css-var-root ant-input-css-var\"\n                            id=\"epamEmail\"\n                            style=\"width: 100%;\"\n                            type=\"text\"\n                            value=\"epamEmail\"\n                          />\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </label>\n            </li>\n            <li\n              class=\"ant-list-item\"\n            >\n              <label\n                style=\"width: 100%;\"\n              >\n                <span\n                  class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                  style=\"font-size: 18px; margin-bottom: 5px; display: inline-block;\"\n                >\n                  <strong>\n                    E-mail\n                    :\n                  </strong>\n                </span>\n                <div\n                  class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n                >\n                  <div\n                    class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n                  >\n                    <div\n                      class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n                    >\n                      <div\n                        class=\"ant-form-item-control-input\"\n                      >\n                        <div\n                          class=\"ant-form-item-control-input-content\"\n                        >\n                          <input\n                            class=\"ant-input css-dev-only-do-not-override-1enej14 ant-input-outlined css-var-root ant-input-css-var\"\n                            id=\"email\"\n                            style=\"width: 100%;\"\n                            type=\"text\"\n                            value=\"email\"\n                          />\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </label>\n            </li>\n          </ul>\n        </div>\n      </div>\n    </div>\n  </form>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/EducationCard.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`EducationCard > Should render correctly > if \"data\" has element with \"null\" value 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"read\"\n                class=\"anticon anticon-read\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"read\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M928 161H699.2c-49.1 0-97.1 14.1-138.4 40.7L512 233l-48.8-31.3A255.2 255.2 0 00324.8 161H96c-17.7 0-32 14.3-32 32v568c0 17.7 14.3 32 32 32h228.8c49.1 0 97.1 14.1 138.4 40.7l44.4 28.6c1.3.8 2.8 1.3 4.3 1.3s3-.4 4.3-1.3l44.4-28.6C602 807.1 650.1 793 699.2 793H928c17.7 0 32-14.3 32-32V193c0-17.7-14.3-32-32-32zM324.8 721H136V233h188.8c35.4 0 69.8 10.1 99.5 29.2l48.8 31.3 6.9 4.5v462c-47.6-25.6-100.8-39-155.2-39zm563.2 0H699.2c-54.4 0-107.6 13.4-155.2 39V298l6.9-4.5 48.8-31.3c29.7-19.1 64.1-29.2 99.5-29.2H888v488zM396.9 361H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm223.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c0-4.1-3.2-7.5-7.1-7.5H627.1c-3.9 0-7.1 3.4-7.1 7.5zM396.9 501H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm416 0H627.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5z\"\n                  />\n                </svg>\n              </span>\n               \n              Education\n            </span>\n            <span\n              aria-label=\"edit\"\n              class=\"anticon anticon-edit\"\n              role=\"img\"\n              tabindex=\"-1\"\n            >\n              <svg\n                aria-hidden=\"true\"\n                data-icon=\"edit\"\n                fill=\"currentColor\"\n                focusable=\"false\"\n                height=\"1em\"\n                viewBox=\"64 64 896 896\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z\"\n                />\n              </svg>\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          aria-busy=\"false\"\n          aria-live=\"polite\"\n          class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-spin-container\"\n          >\n            <ul\n              class=\"ant-list-items ant-list-container css-var-root\"\n            >\n              <li\n                class=\"ant-list-item\"\n              >\n                <p>\n                  (Empty)\n                </p>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`EducationCard > Should render correctly > if \"data\" is empty 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"read\"\n                class=\"anticon anticon-read\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"read\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M928 161H699.2c-49.1 0-97.1 14.1-138.4 40.7L512 233l-48.8-31.3A255.2 255.2 0 00324.8 161H96c-17.7 0-32 14.3-32 32v568c0 17.7 14.3 32 32 32h228.8c49.1 0 97.1 14.1 138.4 40.7l44.4 28.6c1.3.8 2.8 1.3 4.3 1.3s3-.4 4.3-1.3l44.4-28.6C602 807.1 650.1 793 699.2 793H928c17.7 0 32-14.3 32-32V193c0-17.7-14.3-32-32-32zM324.8 721H136V233h188.8c35.4 0 69.8 10.1 99.5 29.2l48.8 31.3 6.9 4.5v462c-47.6-25.6-100.8-39-155.2-39zm563.2 0H699.2c-54.4 0-107.6 13.4-155.2 39V298l6.9-4.5 48.8-31.3c29.7-19.1 64.1-29.2 99.5-29.2H888v488zM396.9 361H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm223.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c0-4.1-3.2-7.5-7.1-7.5H627.1c-3.9 0-7.1 3.4-7.1 7.5zM396.9 501H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm416 0H627.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5z\"\n                  />\n                </svg>\n              </span>\n               \n              Education\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"css-dev-only-do-not-override-1enej14 css-var-root ant-empty ant-empty-normal\"\n      >\n        <div\n          class=\"ant-empty-image\"\n        >\n          <svg\n            height=\"41\"\n            viewBox=\"0 0 64 41\"\n            width=\"64\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n          >\n            <title>\n              No data\n            </title>\n            <g\n              fill=\"none\"\n              fill-rule=\"evenodd\"\n              transform=\"translate(0 1)\"\n            >\n              <ellipse\n                cx=\"32\"\n                cy=\"33\"\n                fill=\"#f5f5f5\"\n                rx=\"32\"\n                ry=\"7\"\n              />\n              <g\n                fill-rule=\"nonzero\"\n                stroke=\"#d9d9d9\"\n              >\n                <path\n                  d=\"M55 12.8 44.9 1.3Q44 0 42.9 0H21.1q-1.2 0-2 1.3L9 12.8V22h46z\"\n                />\n                <path\n                  d=\"M41.6 16c0-1.7 1-3 2.2-3H55v18.1c0 2.2-1.3 3.9-3 3.9H12c-1.7 0-3-1.7-3-3.9V13h11.2c1.2 0 2.2 1.3 2.2 3s1 2.9 2.2 2.9h14.8c1.2 0 2.2-1.4 2.2-3\"\n                  fill=\"#fafafa\"\n                />\n              </g>\n            </g>\n          </svg>\n        </div>\n        <div\n          class=\"ant-empty-description\"\n        >\n          Education history isn't filled in\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`EducationCard > Should render correctly > if editing mode is disabled 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"read\"\n                class=\"anticon anticon-read\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"read\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M928 161H699.2c-49.1 0-97.1 14.1-138.4 40.7L512 233l-48.8-31.3A255.2 255.2 0 00324.8 161H96c-17.7 0-32 14.3-32 32v568c0 17.7 14.3 32 32 32h228.8c49.1 0 97.1 14.1 138.4 40.7l44.4 28.6c1.3.8 2.8 1.3 4.3 1.3s3-.4 4.3-1.3l44.4-28.6C602 807.1 650.1 793 699.2 793H928c17.7 0 32-14.3 32-32V193c0-17.7-14.3-32-32-32zM324.8 721H136V233h188.8c35.4 0 69.8 10.1 99.5 29.2l48.8 31.3 6.9 4.5v462c-47.6-25.6-100.8-39-155.2-39zm563.2 0H699.2c-54.4 0-107.6 13.4-155.2 39V298l6.9-4.5 48.8-31.3c29.7-19.1 64.1-29.2 99.5-29.2H888v488zM396.9 361H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm223.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c0-4.1-3.2-7.5-7.1-7.5H627.1c-3.9 0-7.1 3.4-7.1 7.5zM396.9 501H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm416 0H627.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5z\"\n                  />\n                </svg>\n              </span>\n               \n              Education\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          aria-busy=\"false\"\n          aria-live=\"polite\"\n          class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-spin-container\"\n          >\n            <ul\n              class=\"ant-list-items ant-list-container css-var-root\"\n            >\n              <li\n                class=\"ant-list-item\"\n              >\n                <p>\n                  <span\n                    class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                  >\n                    <strong>\n                      2002\n                    </strong>\n                  </span>\n                   \n                   MIT / POIT\n                </p>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`EducationCard > Should render correctly > if editing mode is enabled 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"read\"\n                class=\"anticon anticon-read\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"read\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M928 161H699.2c-49.1 0-97.1 14.1-138.4 40.7L512 233l-48.8-31.3A255.2 255.2 0 00324.8 161H96c-17.7 0-32 14.3-32 32v568c0 17.7 14.3 32 32 32h228.8c49.1 0 97.1 14.1 138.4 40.7l44.4 28.6c1.3.8 2.8 1.3 4.3 1.3s3-.4 4.3-1.3l44.4-28.6C602 807.1 650.1 793 699.2 793H928c17.7 0 32-14.3 32-32V193c0-17.7-14.3-32-32-32zM324.8 721H136V233h188.8c35.4 0 69.8 10.1 99.5 29.2l48.8 31.3 6.9 4.5v462c-47.6-25.6-100.8-39-155.2-39zm563.2 0H699.2c-54.4 0-107.6 13.4-155.2 39V298l6.9-4.5 48.8-31.3c29.7-19.1 64.1-29.2 99.5-29.2H888v488zM396.9 361H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm223.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c0-4.1-3.2-7.5-7.1-7.5H627.1c-3.9 0-7.1 3.4-7.1 7.5zM396.9 501H211.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5zm416 0H627.1c-3.9 0-7.1 3.4-7.1 7.5v45c0 4.1 3.2 7.5 7.1 7.5h185.7c3.9 0 7.1-3.4 7.1-7.5v-45c.1-4.1-3.1-7.5-7-7.5z\"\n                  />\n                </svg>\n              </span>\n               \n              Education\n            </span>\n            <span\n              aria-label=\"edit\"\n              class=\"anticon anticon-edit\"\n              role=\"img\"\n              tabindex=\"-1\"\n            >\n              <svg\n                aria-hidden=\"true\"\n                data-icon=\"edit\"\n                fill=\"currentColor\"\n                focusable=\"false\"\n                height=\"1em\"\n                viewBox=\"64 64 896 896\"\n                width=\"1em\"\n              >\n                <path\n                  d=\"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z\"\n                />\n              </svg>\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          aria-busy=\"false\"\n          aria-live=\"polite\"\n          class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-spin-container\"\n          >\n            <ul\n              class=\"ant-list-items ant-list-container css-var-root\"\n            >\n              <li\n                class=\"ant-list-item\"\n              >\n                <p>\n                  <span\n                    class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                  >\n                    <strong>\n                      2002\n                    </strong>\n                  </span>\n                   \n                   MIT / POIT\n                </p>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/MainCard.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`MainCard > Should render correctly > if editing mode is disabled 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n    style=\"position: relative;\"\n  >\n    <div\n      class=\"ant-card-body\"\n    >\n      <span\n        class=\"ant-avatar ant-avatar-circle ant-avatar-image css-var-root ant-avatar-css-var css-dev-only-do-not-override-1enej14\"\n        style=\"width: 96px; height: 96px; font-size: 18px; margin: 0px auto 10px; display: block;\"\n      >\n        <img\n          src=\"https://cdn.rs.school/avatars/john-doe.png?size=192\"\n        />\n      </span>\n      <h1\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"font-size: 24px; text-align: center; margin: 0px;\"\n      >\n        John Doe\n      </h1>\n      <div\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"text-align: center; margin-bottom: 20px;\"\n      >\n        <a\n          href=\"https://github.com/john-doe\"\n          style=\"font-size: 16px;\"\n          target=\"_blank\"\n        >\n          <span\n            aria-label=\"github\"\n            class=\"anticon anticon-github\"\n            role=\"img\"\n          >\n            <svg\n              aria-hidden=\"true\"\n              data-icon=\"github\"\n              fill=\"currentColor\"\n              focusable=\"false\"\n              height=\"1em\"\n              viewBox=\"64 64 896 896\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z\"\n              />\n            </svg>\n          </span>\n           \n          john-doe\n        </a>\n      </div>\n      <div\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"text-align: center; margin: 0px;\"\n      >\n        <span>\n          <span\n            aria-label=\"environment\"\n            class=\"anticon anticon-environment\"\n            role=\"img\"\n          >\n            <svg\n              aria-hidden=\"true\"\n              data-icon=\"environment\"\n              fill=\"currentColor\"\n              focusable=\"false\"\n              height=\"1em\"\n              viewBox=\"64 64 896 896\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M512 327c-29.9 0-58 11.6-79.2 32.8A111.6 111.6 0 00400 439c0 29.9 11.7 58 32.8 79.2A111.6 111.6 0 00512 551c29.9 0 58-11.7 79.2-32.8C612.4 497 624 468.9 624 439c0-29.9-11.6-58-32.8-79.2S541.9 327 512 327zm342.6-37.9a362.49 362.49 0 00-79.9-115.7 370.83 370.83 0 00-118.2-77.8C610.7 76.6 562.1 67 512 67c-50.1 0-98.7 9.6-144.5 28.5-44.3 18.3-84 44.5-118.2 77.8A363.6 363.6 0 00169.4 289c-19.5 45-29.4 92.8-29.4 142 0 70.6 16.9 140.9 50.1 208.7 26.7 54.5 64 107.6 111 158.1 80.3 86.2 164.5 138.9 188.4 153a43.9 43.9 0 0022.4 6.1c7.8 0 15.5-2 22.4-6.1 23.9-14.1 108.1-66.8 188.4-153 47-50.4 84.3-103.6 111-158.1C867.1 572 884 501.8 884 431.1c0-49.2-9.9-97-29.4-142zM512 615c-97.2 0-176-78.8-176-176s78.8-176 176-176 176 78.8 176 176-78.8 176-176 176z\"\n              />\n            </svg>\n          </span>\n           \n          Minsk, Belarus\n        </span>\n      </div>\n      <div\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"text-align: center; margin-top: 20px;\"\n      >\n        <a\n          href=\"public-url\"\n          target=\"_blank\"\n        >\n          Public CV\n        </a>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n\nexports[`MainCard > Should render correctly > if editing mode is enabled 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n    style=\"position: relative;\"\n  >\n    <div\n      class=\"ant-card-body\"\n    >\n      <span\n        aria-label=\"edit\"\n        class=\"anticon anticon-edit\"\n        role=\"img\"\n        style=\"position: absolute; top: 18px; right: 24px; font-size: 16px;\"\n        tabindex=\"-1\"\n      >\n        <svg\n          aria-hidden=\"true\"\n          data-icon=\"edit\"\n          fill=\"currentColor\"\n          focusable=\"false\"\n          height=\"1em\"\n          viewBox=\"64 64 896 896\"\n          width=\"1em\"\n        >\n          <path\n            d=\"M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z\"\n          />\n        </svg>\n      </span>\n      <span\n        class=\"ant-avatar ant-avatar-circle ant-avatar-image css-var-root ant-avatar-css-var css-dev-only-do-not-override-1enej14\"\n        style=\"width: 96px; height: 96px; font-size: 18px; margin: 0px auto 10px; display: block;\"\n      >\n        <img\n          src=\"https://cdn.rs.school/avatars/john-doe.png?size=192\"\n        />\n      </span>\n      <h1\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"font-size: 24px; text-align: center; margin: 0px;\"\n      >\n        John Doe\n      </h1>\n      <div\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"text-align: center; margin-bottom: 20px;\"\n      >\n        <a\n          href=\"https://github.com/john-doe\"\n          style=\"font-size: 16px;\"\n          target=\"_blank\"\n        >\n          <span\n            aria-label=\"github\"\n            class=\"anticon anticon-github\"\n            role=\"img\"\n          >\n            <svg\n              aria-hidden=\"true\"\n              data-icon=\"github\"\n              fill=\"currentColor\"\n              focusable=\"false\"\n              height=\"1em\"\n              viewBox=\"64 64 896 896\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9a127.5 127.5 0 0138.1 91v112.5c.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z\"\n              />\n            </svg>\n          </span>\n           \n          john-doe\n        </a>\n      </div>\n      <div\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"text-align: center; margin: 0px;\"\n      >\n        <span>\n          <span\n            aria-label=\"environment\"\n            class=\"anticon anticon-environment\"\n            role=\"img\"\n          >\n            <svg\n              aria-hidden=\"true\"\n              data-icon=\"environment\"\n              fill=\"currentColor\"\n              focusable=\"false\"\n              height=\"1em\"\n              viewBox=\"64 64 896 896\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M512 327c-29.9 0-58 11.6-79.2 32.8A111.6 111.6 0 00400 439c0 29.9 11.7 58 32.8 79.2A111.6 111.6 0 00512 551c29.9 0 58-11.7 79.2-32.8C612.4 497 624 468.9 624 439c0-29.9-11.6-58-32.8-79.2S541.9 327 512 327zm342.6-37.9a362.49 362.49 0 00-79.9-115.7 370.83 370.83 0 00-118.2-77.8C610.7 76.6 562.1 67 512 67c-50.1 0-98.7 9.6-144.5 28.5-44.3 18.3-84 44.5-118.2 77.8A363.6 363.6 0 00169.4 289c-19.5 45-29.4 92.8-29.4 142 0 70.6 16.9 140.9 50.1 208.7 26.7 54.5 64 107.6 111 158.1 80.3 86.2 164.5 138.9 188.4 153a43.9 43.9 0 0022.4 6.1c7.8 0 15.5-2 22.4-6.1 23.9-14.1 108.1-66.8 188.4-153 47-50.4 84.3-103.6 111-158.1C867.1 572 884 501.8 884 431.1c0-49.2-9.9-97-29.4-142zM512 615c-97.2 0-176-78.8-176-176s78.8-176 176-176 176 78.8 176 176-78.8 176-176 176z\"\n              />\n            </svg>\n          </span>\n           \n          Minsk, Belarus\n        </span>\n      </div>\n      <div\n        class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        style=\"text-align: center; margin-top: 20px;\"\n      >\n        <a\n          href=\"public-url\"\n          target=\"_blank\"\n        >\n          Public CV\n        </a>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/ProfileSettingsModal.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`ProfileSettingsModal > should render correctly 1`] = `\n<div\n  aria-labelledby=\"test-id\"\n  aria-modal=\"true\"\n  class=\"ant-modal css-dev-only-do-not-override-1enej14 _modal_0afd3f ant-zoom-appear ant-zoom-appear-prepare ant-zoom\"\n  role=\"dialog\"\n  style=\"width: 520px;\"\n  tabindex=\"-1\"\n>\n  <div\n    class=\"ant-modal-container\"\n  >\n    <button\n      aria-label=\"Close\"\n      class=\"ant-modal-close\"\n      type=\"button\"\n    >\n      <span\n        aria-label=\"Close\"\n        class=\"ant-modal-close-x\"\n      >\n        <span\n          aria-label=\"close\"\n          class=\"anticon anticon-close ant-modal-close-icon\"\n          role=\"img\"\n        >\n          <svg\n            aria-hidden=\"true\"\n            data-icon=\"close\"\n            fill=\"currentColor\"\n            fill-rule=\"evenodd\"\n            focusable=\"false\"\n            height=\"1em\"\n            viewBox=\"64 64 896 896\"\n            width=\"1em\"\n          >\n            <path\n              d=\"M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z\"\n            />\n          </svg>\n        </span>\n      </span>\n    </button>\n    <div\n      class=\"ant-modal-header\"\n    >\n      <div\n        class=\"ant-modal-title\"\n        id=\"test-id\"\n      >\n        Settings Title\n      </div>\n    </div>\n    <div\n      class=\"ant-modal-body\"\n    >\n      <div>\n        content\n      </div>\n    </div>\n    <div\n      class=\"ant-modal-footer\"\n    >\n      <button\n        class=\"ant-btn css-dev-only-do-not-override-1enej14 css-var-root ant-btn-default ant-btn-color-default ant-btn-variant-outlined\"\n        type=\"button\"\n      >\n        <span>\n          Cancel\n        </span>\n      </button>\n      <button\n        class=\"ant-btn css-dev-only-do-not-override-1enej14 css-var-root ant-btn-primary ant-btn-color-primary ant-btn-variant-solid\"\n        type=\"button\"\n      >\n        <span>\n          Save\n        </span>\n      </button>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/PublicFeedbackCard.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`PublicFeedbackCard > should render correctly 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"message\"\n                class=\"anticon anticon-message\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"message\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M464 512a48 48 0 1096 0 48 48 0 10-96 0zm200 0a48 48 0 1096 0 48 48 0 10-96 0zm-400 0a48 48 0 1096 0 48 48 0 10-96 0zm661.2-173.6c-22.6-53.7-55-101.9-96.3-143.3a444.35 444.35 0 00-143.3-96.3C630.6 75.7 572.2 64 512 64h-2c-60.6.3-119.3 12.3-174.5 35.9a445.35 445.35 0 00-142 96.5c-40.9 41.3-73 89.3-95.2 142.8-23 55.4-34.6 114.3-34.3 174.9A449.4 449.4 0 00112 714v152a46 46 0 0046 46h152.1A449.4 449.4 0 00510 960h2.1c59.9 0 118-11.6 172.7-34.3a444.48 444.48 0 00142.8-95.2c41.3-40.9 73.8-88.7 96.5-142 23.6-55.2 35.6-113.9 35.9-174.5.3-60.9-11.5-120-34.8-175.6zm-151.1 438C704 845.8 611 884 512 884h-1.7c-60.3-.3-120.2-15.3-173.1-43.5l-8.4-4.5H188V695.2l-4.5-8.4C155.3 633.9 140.3 574 140 513.7c-.4-99.7 37.7-193.3 107.6-263.8 69.8-70.5 163.1-109.5 262.8-109.9h1.7c50 0 98.5 9.7 144.2 28.9 44.6 18.7 84.6 45.6 119 80 34.3 34.3 61.3 74.4 80 119 19.4 46.2 29.1 95.2 28.9 145.8-.6 99.6-39.7 192.9-110.1 262.7z\"\n                  />\n                </svg>\n              </span>\n               \n              Public Feedback\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        style=\"margin-bottom: 20px;\"\n      >\n        <span\n          class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <strong>\n            Total badges:\n          </strong>\n        </span>\n         \n        6\n      </div>\n      <div\n        style=\"margin-bottom: 30px;\"\n      >\n        <div\n          style=\"margin: 5px; display: inline-block;\"\n        >\n          <span\n            class=\"ant-badge css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <span\n              class=\"ant-avatar ant-avatar-circle ant-avatar-image css-var-root ant-avatar-css-var css-dev-only-do-not-override-1enej14\"\n              style=\"width: 48px; height: 48px; font-size: 18px;\"\n            >\n              <img\n                alt=\"Congratulations badge\"\n                src=\"/static/svg/badges/Congratulations.svg\"\n              />\n            </span>\n            <sup\n              class=\"ant-scroll-number ant-badge-count\"\n              data-show=\"true\"\n              title=\"1\"\n            >\n              <bdi>\n                <span\n                  class=\"ant-scroll-number-only\"\n                  style=\"transition: none;\"\n                >\n                  <span\n                    class=\"ant-scroll-number-only-unit current\"\n                  >\n                    1\n                  </span>\n                </span>\n              </bdi>\n            </sup>\n          </span>\n        </div>\n        <div\n          style=\"margin: 5px; display: inline-block;\"\n        >\n          <span\n            class=\"ant-badge css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <span\n              class=\"ant-avatar ant-avatar-circle ant-avatar-image css-var-root ant-avatar-css-var css-dev-only-do-not-override-1enej14\"\n              style=\"width: 48px; height: 48px; font-size: 18px;\"\n            >\n              <img\n                alt=\"Great_speaker badge\"\n                src=\"/static/svg/badges/GreatSpeaker.svg\"\n              />\n            </span>\n            <sup\n              class=\"ant-scroll-number ant-badge-count\"\n              data-show=\"true\"\n              title=\"3\"\n            >\n              <bdi>\n                <span\n                  class=\"ant-scroll-number-only\"\n                  style=\"transition: none;\"\n                >\n                  <span\n                    class=\"ant-scroll-number-only-unit current\"\n                  >\n                    3\n                  </span>\n                </span>\n              </bdi>\n            </sup>\n          </span>\n        </div>\n        <div\n          style=\"margin: 5px; display: inline-block;\"\n        >\n          <span\n            class=\"ant-badge css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <span\n              class=\"ant-avatar ant-avatar-circle ant-avatar-image css-var-root ant-avatar-css-var css-dev-only-do-not-override-1enej14\"\n              style=\"width: 48px; height: 48px; font-size: 18px;\"\n            >\n              <img\n                alt=\"Thank_you badge\"\n                src=\"/static/svg/badges/ThankYou.svg\"\n              />\n            </span>\n            <sup\n              class=\"ant-scroll-number ant-badge-count\"\n              data-show=\"true\"\n              title=\"2\"\n            >\n              <bdi>\n                <span\n                  class=\"ant-scroll-number-only\"\n                  style=\"transition: none;\"\n                >\n                  <span\n                    class=\"ant-scroll-number-only-unit current\"\n                  >\n                    2\n                  </span>\n                </span>\n              </bdi>\n            </sup>\n          </span>\n        </div>\n      </div>\n      <div\n        style=\"margin-bottom: 0px;\"\n      >\n        <span\n          class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <strong>\n            Last feedback:\n          </strong>\n        </span>\n      </div>\n      <div\n        style=\"display: flex; gap: 8px;\"\n      >\n        <div\n          style=\"flex-shrink: 0;\"\n        >\n          <span\n            class=\"ant-avatar ant-avatar-circle ant-avatar-image css-var-root ant-avatar-css-var css-dev-only-do-not-override-1enej14\"\n            style=\"width: 48px; height: 48px; font-size: 18px;\"\n          >\n            <img\n              src=\"https://cdn.rs.school/avatars/apetr.png?size=96\"\n            />\n          </span>\n        </div>\n        <div\n          style=\"flex: 1 1 0%; min-width: 0;\"\n        >\n          <div\n            style=\"margin-bottom: 4px;\"\n          >\n            <span\n              style=\"font-weight: 500; margin-right: 8px;\"\n            >\n              <a\n                href=\"/profile?githubId=apetr\"\n              >\n                Anton Petrov\n              </a>\n            </span>\n            <span\n              style=\"color: rgba(0, 0, 0, 0.65); font-size: 12px;\"\n            >\n              <span>\n                a month ago\n              </span>\n            </span>\n          </div>\n          <div>\n            <span\n              class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n              style=\"font-size: 12px;\"\n            >\n              <strong>\n                Congratulations\n              </strong>\n            </span>\n            <div\n              aria-label=\"Test\"\n              class=\"ant-typography ant-typography-ellipsis css-dev-only-do-not-override-1enej14 css-var-root\"\n              style=\"\"\n            >\n              Test\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <ul\n      class=\"ant-card-actions\"\n    >\n      <li\n        style=\"width: 100%;\"\n      >\n        <span>\n          <span\n            aria-label=\"fullscreen\"\n            class=\"anticon anticon-fullscreen\"\n            role=\"img\"\n            tabindex=\"-1\"\n          >\n            <svg\n              aria-hidden=\"true\"\n              data-icon=\"fullscreen\"\n              fill=\"currentColor\"\n              focusable=\"false\"\n              height=\"1em\"\n              viewBox=\"64 64 896 896\"\n              width=\"1em\"\n            >\n              <path\n                d=\"M290 236.4l43.9-43.9a8.01 8.01 0 00-4.7-13.6L169 160c-5.1-.6-9.5 3.7-8.9 8.9L179 329.1c.8 6.6 8.9 9.4 13.6 4.7l43.7-43.7L370 423.7c3.1 3.1 8.2 3.1 11.3 0l42.4-42.3c3.1-3.1 3.1-8.2 0-11.3L290 236.4zm352.7 187.3c3.1 3.1 8.2 3.1 11.3 0l133.7-133.6 43.7 43.7a8.01 8.01 0 0013.6-4.7L863.9 169c.6-5.1-3.7-9.5-8.9-8.9L694.8 179c-6.6.8-9.4 8.9-4.7 13.6l43.9 43.9L600.3 370a8.03 8.03 0 000 11.3l42.4 42.4zM845 694.9c-.8-6.6-8.9-9.4-13.6-4.7l-43.7 43.7L654 600.3a8.03 8.03 0 00-11.3 0l-42.4 42.3a8.03 8.03 0 000 11.3L734 787.6l-43.9 43.9a8.01 8.01 0 004.7 13.6L855 864c5.1.6 9.5-3.7 8.9-8.9L845 694.9zm-463.7-94.6a8.03 8.03 0 00-11.3 0L236.3 733.9l-43.7-43.7a8.01 8.01 0 00-13.6 4.7L160.1 855c-.6 5.1 3.7 9.5 8.9 8.9L329.2 845c6.6-.8 9.4-8.9 4.7-13.6L290 787.6 423.7 654c3.1-3.1 3.1-8.2 0-11.3l-42.4-42.4z\"\n              />\n            </svg>\n          </span>\n        </span>\n      </li>\n    </ul>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/PublicFeedbackModal.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`PublicFeedbackModal > Should render correctly 1`] = `<div />`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/StudentStatsCard.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`StudentStatsCard > should render correctly 1`] = `\n<div>\n  <div\n    class=\"ant-card ant-card-bordered css-dev-only-do-not-override-1enej14 css-var-root\"\n  >\n    <div\n      class=\"ant-card-head\"\n    >\n      <div\n        class=\"ant-card-head-wrapper\"\n      >\n        <div\n          class=\"ant-card-head-title\"\n        >\n          <h2\n            class=\"ant-typography ant-typography-ellipsis ant-typography-ellipsis-single-line css-dev-only-do-not-override-1enej14 css-var-root\"\n            style=\"font-size: 16px; margin-bottom: 0px; display: flex; align-items: center; justify-content: space-between;\"\n          >\n            <span>\n              <span\n                aria-label=\"book\"\n                class=\"anticon anticon-book\"\n                role=\"img\"\n              >\n                <svg\n                  aria-hidden=\"true\"\n                  data-icon=\"book\"\n                  fill=\"currentColor\"\n                  focusable=\"false\"\n                  height=\"1em\"\n                  viewBox=\"64 64 896 896\"\n                  width=\"1em\"\n                >\n                  <path\n                    d=\"M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-260 72h96v209.9L621.5 312 572 347.4V136zm220 752H232V136h280v296.9c0 3.3 1 6.6 3 9.3a15.9 15.9 0 0022.3 3.7l83.8-59.9 81.4 59.4c2.7 2 6 3.1 9.4 3.1 8.8 0 16-7.2 16-16V136h64v752z\"\n                  />\n                </svg>\n              </span>\n               \n              Student Statistics\n            </span>\n          </h2>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-card-body\"\n    >\n      <div\n        class=\"ant-list ant-list-split css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          aria-busy=\"false\"\n          aria-live=\"polite\"\n          class=\"ant-spin css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-spin-container\"\n          >\n            <ul\n              class=\"ant-list-items ant-list-container css-var-root\"\n            >\n              <li\n                class=\"ant-list-item\"\n                style=\"display: flex; justify-content: space-between;\"\n              >\n                <div\n                  style=\"flex-grow: 2;\"\n                >\n                  <p\n                    style=\"margin-bottom: 0px;\"\n                  >\n                    <span\n                      class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                    >\n                      <strong>\n                        rs-2018-q1\n                         / Minsk\n                      </strong>\n                    </span>\n                  </p>\n                  <div\n                    style=\"width: 80%; margin-bottom: 5px;\"\n                  >\n                    <div\n                      aria-valuemax=\"100\"\n                      aria-valuemin=\"0\"\n                      aria-valuenow=\"100\"\n                      class=\"ant-progress ant-progress-status-success ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-small css-dev-only-do-not-override-1enej14 css-var-root\"\n                      role=\"progressbar\"\n                    >\n                      <div\n                        class=\"ant-progress-body\"\n                        style=\"width: 100%;\"\n                      >\n                        <div\n                          class=\"ant-progress-rail\"\n                          style=\"height: 6px;\"\n                        >\n                          <div\n                            class=\"ant-progress-track\"\n                            style=\"width: 100%; height: 6px;\"\n                          />\n                        </div>\n                        <span\n                          class=\"ant-progress-indicator ant-progress-indicator-end ant-progress-indicator-outer\"\n                        >\n                          <span\n                            aria-label=\"check-circle\"\n                            class=\"anticon anticon-check-circle\"\n                            role=\"img\"\n                          >\n                            <svg\n                              aria-hidden=\"true\"\n                              data-icon=\"check-circle\"\n                              fill=\"currentColor\"\n                              focusable=\"false\"\n                              height=\"1em\"\n                              viewBox=\"64 64 896 896\"\n                              width=\"1em\"\n                            >\n                              <path\n                                d=\"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z\"\n                              />\n                            </svg>\n                          </span>\n                        </span>\n                      </div>\n                    </div>\n                  </div>\n                  <p\n                    style=\"font-size: 12px; margin-bottom: 5px;\"\n                  >\n                    Mentor: \n                    <a\n                      href=\"/profile?githubId=andrew123\"\n                    >\n                      Andrey Andreev\n                    </a>\n                  </p>\n                  <p\n                    style=\"font-size: 12px; margin-bottom: 5px;\"\n                  >\n                    Position: \n                    32\n                  </p>\n                  <p\n                    style=\"font-size: 12px; margin-bottom: 5px;\"\n                  >\n                    Score: \n                    1201\n                  </p>\n                </div>\n                <button\n                  aria-label=\"Open details\"\n                  class=\"ant-btn css-dev-only-do-not-override-1enej14 css-var-root ant-btn-dashed ant-btn-color-default ant-btn-variant-dashed\"\n                  data-testid=\"expand-button\"\n                  style=\"padding: 0.3em 0.5em;\"\n                  title=\"Open details\"\n                  type=\"button\"\n                >\n                  <span\n                    aria-label=\"fullscreen\"\n                    class=\"anticon anticon-fullscreen\"\n                    role=\"img\"\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      data-icon=\"fullscreen\"\n                      fill=\"currentColor\"\n                      focusable=\"false\"\n                      height=\"1em\"\n                      viewBox=\"64 64 896 896\"\n                      width=\"1em\"\n                    >\n                      <path\n                        d=\"M290 236.4l43.9-43.9a8.01 8.01 0 00-4.7-13.6L169 160c-5.1-.6-9.5 3.7-8.9 8.9L179 329.1c.8 6.6 8.9 9.4 13.6 4.7l43.7-43.7L370 423.7c3.1 3.1 8.2 3.1 11.3 0l42.4-42.3c3.1-3.1 3.1-8.2 0-11.3L290 236.4zm352.7 187.3c3.1 3.1 8.2 3.1 11.3 0l133.7-133.6 43.7 43.7a8.01 8.01 0 0013.6-4.7L863.9 169c.6-5.1-3.7-9.5-8.9-8.9L694.8 179c-6.6.8-9.4 8.9-4.7 13.6l43.9 43.9L600.3 370a8.03 8.03 0 000 11.3l42.4 42.4zM845 694.9c-.8-6.6-8.9-9.4-13.6-4.7l-43.7 43.7L654 600.3a8.03 8.03 0 00-11.3 0l-42.4 42.3a8.03 8.03 0 000 11.3L734 787.6l-43.9 43.9a8.01 8.01 0 004.7 13.6L855 864c5.1.6 9.5-3.7 8.9-8.9L845 694.9zm-463.7-94.6a8.03 8.03 0 00-11.3 0L236.3 733.9l-43.7-43.7a8.01 8.01 0 00-13.6 4.7L160.1 855c-.6 5.1 3.7 9.5 8.9 8.9L329.2 845c6.6-.8 9.4-8.9 4.7-13.6L290 787.6 423.7 654c3.1-3.1 3.1-8.2 0-11.3l-42.4-42.4z\"\n                      />\n                    </svg>\n                  </span>\n                </button>\n              </li>\n              <li\n                class=\"ant-list-item\"\n                style=\"display: flex; justify-content: space-between;\"\n              >\n                <div\n                  style=\"flex-grow: 2;\"\n                >\n                  <p\n                    style=\"margin-bottom: 0px;\"\n                  >\n                    <span\n                      class=\"ant-typography css-dev-only-do-not-override-1enej14 css-var-root\"\n                    >\n                      <strong>\n                        rs-2019-q1\n                         / Minsk\n                      </strong>\n                    </span>\n                  </p>\n                  <div\n                    style=\"width: 80%; margin-bottom: 5px;\"\n                  >\n                    <div\n                      aria-valuemax=\"100\"\n                      aria-valuemin=\"0\"\n                      aria-valuenow=\"50\"\n                      class=\"ant-progress ant-progress-status-exception ant-progress-line ant-progress-line-align-end ant-progress-line-position-outer ant-progress-show-info ant-progress-small css-dev-only-do-not-override-1enej14 css-var-root\"\n                      role=\"progressbar\"\n                    >\n                      <div\n                        class=\"ant-progress-body\"\n                        style=\"width: 100%;\"\n                      >\n                        <div\n                          class=\"ant-progress-rail\"\n                          style=\"height: 6px;\"\n                        >\n                          <div\n                            class=\"ant-progress-track\"\n                            style=\"width: 50%; height: 6px;\"\n                          />\n                        </div>\n                        <span\n                          class=\"ant-progress-indicator ant-progress-indicator-end ant-progress-indicator-outer\"\n                        >\n                          <span\n                            aria-label=\"close-circle\"\n                            class=\"anticon anticon-close-circle\"\n                            role=\"img\"\n                          >\n                            <svg\n                              aria-hidden=\"true\"\n                              data-icon=\"close-circle\"\n                              fill=\"currentColor\"\n                              fill-rule=\"evenodd\"\n                              focusable=\"false\"\n                              height=\"1em\"\n                              viewBox=\"64 64 896 896\"\n                              width=\"1em\"\n                            >\n                              <path\n                                d=\"M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z\"\n                              />\n                            </svg>\n                          </span>\n                        </span>\n                      </div>\n                    </div>\n                  </div>\n                  <p\n                    style=\"font-size: 12px; margin-bottom: 5px;\"\n                  >\n                    Mentor: \n                    <a\n                      href=\"/profile?githubId=dimon12\"\n                    >\n                      Dima Testovich\n                    </a>\n                  </p>\n                  <p\n                    style=\"font-size: 12px; margin-bottom: 5px;\"\n                  >\n                    Position: \n                    32\n                  </p>\n                  <p\n                    style=\"font-size: 12px; margin-bottom: 5px;\"\n                  >\n                    Score: \n                    101\n                  </p>\n                </div>\n                <button\n                  aria-label=\"Open details\"\n                  class=\"ant-btn css-dev-only-do-not-override-1enej14 css-var-root ant-btn-dashed ant-btn-color-default ant-btn-variant-dashed\"\n                  data-testid=\"expand-button\"\n                  style=\"padding: 0.3em 0.5em;\"\n                  title=\"Open details\"\n                  type=\"button\"\n                >\n                  <span\n                    aria-label=\"fullscreen\"\n                    class=\"anticon anticon-fullscreen\"\n                    role=\"img\"\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      data-icon=\"fullscreen\"\n                      fill=\"currentColor\"\n                      focusable=\"false\"\n                      height=\"1em\"\n                      viewBox=\"64 64 896 896\"\n                      width=\"1em\"\n                    >\n                      <path\n                        d=\"M290 236.4l43.9-43.9a8.01 8.01 0 00-4.7-13.6L169 160c-5.1-.6-9.5 3.7-8.9 8.9L179 329.1c.8 6.6 8.9 9.4 13.6 4.7l43.7-43.7L370 423.7c3.1 3.1 8.2 3.1 11.3 0l42.4-42.3c3.1-3.1 3.1-8.2 0-11.3L290 236.4zm352.7 187.3c3.1 3.1 8.2 3.1 11.3 0l133.7-133.6 43.7 43.7a8.01 8.01 0 0013.6-4.7L863.9 169c.6-5.1-3.7-9.5-8.9-8.9L694.8 179c-6.6.8-9.4 8.9-4.7 13.6l43.9 43.9L600.3 370a8.03 8.03 0 000 11.3l42.4 42.4zM845 694.9c-.8-6.6-8.9-9.4-13.6-4.7l-43.7 43.7L654 600.3a8.03 8.03 0 00-11.3 0l-42.4 42.3a8.03 8.03 0 000 11.3L734 787.6l-43.9 43.9a8.01 8.01 0 004.7 13.6L855 864c5.1.6 9.5-3.7 8.9-8.9L845 694.9zm-463.7-94.6a8.03 8.03 0 00-11.3 0L236.3 733.9l-43.7-43.7a8.01 8.01 0 00-13.6 4.7L160.1 855c-.6 5.1 3.7 9.5 8.9 8.9L329.2 845c6.6-.8 9.4-8.9 4.7-13.6L290 787.6 423.7 654c3.1-3.1 3.1-8.2 0-11.3l-42.4-42.4z\"\n                      />\n                    </svg>\n                  </span>\n                </button>\n              </li>\n            </ul>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n`;\n"
  },
  {
    "path": "client/src/components/Profile/__test__/__snapshots__/StudentStatsModal.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`StudentStatsModal > Should render correctly 1`] = `<div />`;\n"
  },
  {
    "path": "client/src/components/Profile/ui/DateWidget.tsx",
    "content": "import { Flex, Space, theme, Typography } from 'antd';\nimport { formatDate } from '@client/services/formatter';\nimport CalendarOutlined from '@ant-design/icons/CalendarOutlined';\n\nconst { Text } = Typography;\n\nexport function DateWidget({ date }: { date?: string }) {\n  if (!date) {\n    return null;\n  }\n  const { token } = theme.useToken();\n  return (\n    <Flex gap=\"0.2em\" vertical>\n      <Text style={{ color: token.colorTextTertiary, fontSize: '1em' }}>Date</Text>\n      <Space>\n        <CalendarOutlined size={24} />\n        <Text data-testid=\"date-widget\">{formatDate(date)}</Text>\n      </Space>\n    </Flex>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/ui/ExpandButtonWidget.tsx",
    "content": "import { FullscreenOutlined } from '@ant-design/icons';\nimport { Button } from 'antd';\n\nexport function ExpandButtonWidget({ onClick }: { onClick: () => void }) {\n  return (\n    <Button\n      type=\"dashed\"\n      style={{ padding: '0.3em 0.5em' }}\n      onClick={onClick}\n      data-testid=\"expand-button\"\n      aria-label=\"Open details\"\n      title=\"Open details\"\n    >\n      <FullscreenOutlined />\n    </Button>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/ui/InterviewerWidget.tsx",
    "content": "import { Flex, Space, theme, Typography } from 'antd';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\n\nconst { Text } = Typography;\n\nexport function InterviewerWidget({\n  interviewer,\n  vertical,\n}: {\n  interviewer: { name: string; githubId: string };\n  vertical?: boolean;\n}) {\n  const { token } = theme.useToken();\n\n  return (\n    <a href={`/profile?githubId=${interviewer.githubId}`}>\n      <Flex gap=\"0.2em\" vertical={vertical}>\n        <Text\n          strong={!vertical}\n          style={{\n            color: vertical ? token.colorTextTertiary : token.colorTextBase,\n            fontSize: '1em',\n            marginInlineEnd: '0.5ch',\n          }}\n        >\n          Interviewer {vertical ? '' : ':'}\n        </Text>\n        <Space style={{ flexDirection: vertical ? 'row' : 'row-reverse' }}>\n          <GithubAvatar size={24} githubId={interviewer.githubId} />\n          <Text>{interviewer.name}</Text>\n        </Space>\n      </Flex>\n    </a>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/ui/IsGoodCandidateWidget.tsx",
    "content": "import { Tag, Typography } from 'antd';\n\nconst { Text } = Typography;\n\nexport function IsGoodCandidateWidget({ isGoodCandidate }: { isGoodCandidate: boolean | null }) {\n  if (!isGoodCandidate) {\n    return null;\n  }\n\n  return (\n    <Text>\n      <Text style={{ fontWeight: 'bold' }}>Good candidate:</Text> <Tag color=\"green\">Yes</Tag>\n    </Text>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/ui/LegacyScreeningFeedback.tsx",
    "content": "import { Tag, Typography, Table } from 'antd';\nimport { LegacyFeedback } from '@common/models/profile';\nimport { ENGLISH_LEVELS } from '@client/data/english';\nimport { CODING_LEVELS, SKILLS_LEVELS } from '@client/data/interviews/technical-screening';\nimport { Rating } from '@client/shared/components/Rating';\n\nconst { Text } = Typography;\n\nenum SKILL_NAME {\n  htmlCss = 'HTML/CSS',\n  dataStructures = 'Data structures',\n  common = 'Common of CS / Programming',\n}\n\n/**\n * this feedback template will live here until we will migrate all feedbacks to new template\n */\nexport function LegacyScreeningFeedback({ feedback }: { feedback: LegacyFeedback }) {\n  const { comment, skills, programmingTask, english } = feedback;\n\n  const skillSet = [\n    ...(Object.keys(skills) as any[]).map((key: keyof typeof skills) => ({\n      rating: skills[key],\n      name: SKILL_NAME[key],\n      key,\n      isNotCodeWritingLevel: true,\n    })),\n    {\n      rating: programmingTask.codeWritingLevel,\n      name: 'Code writing level',\n      key: 'codeWritingLevel',\n      isNotCodeWritingLevel: false,\n    },\n  ];\n  const englishLevel = typeof english === 'number' ? ENGLISH_LEVELS[english] : english;\n\n  return (\n    <>\n      {comment && (\n        <p style={{ marginBottom: 20 }}>\n          <Text strong>Comment: </Text>\n          {comment}\n        </p>\n      )}\n      <p style={{ marginBottom: 5 }}>\n        Programming task(s): <br /> <Text code>{programmingTask.task}</Text>\n      </p>\n      <p style={{ marginBottom: 5 }}>\n        Has the student solved the task(s)?{' '}\n        {programmingTask.resolved === 1 ? (\n          <Tag color=\"green\">Yes</Tag>\n        ) : programmingTask.resolved === 2 ? (\n          <Tag color=\"orange\">Yes (with tips)</Tag>\n        ) : (\n          <Tag color=\"red\">No</Tag>\n        )}\n      </p>\n      <p style={{ marginBottom: 5 }}>Comments about coding level: {programmingTask.comment}</p>\n      <p style={{ marginBottom: 5 }}>Estimated English level: {englishLevel?.toString().toUpperCase()}</p>\n      <Table\n        dataSource={skillSet}\n        size=\"small\"\n        rowKey=\"key\"\n        pagination={false}\n        columns={[\n          {\n            dataIndex: 'name',\n            ellipsis: true,\n            width: '30%',\n          },\n          {\n            dataIndex: 'rating',\n            render: (rating, record) => (\n              <Rating rating={rating} tooltips={record.isNotCodeWritingLevel ? SKILLS_LEVELS : CODING_LEVELS} />\n            ),\n          },\n        ]}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/ui/PrescreeningFeedback.tsx",
    "content": "import { Row, Space, Table, Typography } from 'antd';\nimport { StageInterviewDetailedFeedback } from '@common/models/profile';\nimport { CODING_LEVELS, FeedbackStepId, SKILLS_LEVELS } from '@client/data/interviews/technical-screening';\nimport { InterviewFeedbackStepData, InterviewFeedbackValues, InterviewQuestion } from '@common/models';\nimport { Rating } from '@client/shared/components/Rating';\nimport { useMemo } from 'react';\n\nconst { Text, Title } = Typography;\n\nconst STYLES = {\n  feedbackItemWidth: '80%',\n  skillCommentWidth: '60%',\n};\n\nconst FEEDBACK_CONFIG = [\n  {\n    id: 'decision_redFlags',\n    label: 'Red flags',\n    getValue: (steps: Record<FeedbackStepId, InterviewFeedbackStepData>) => steps.decision.values?.redFlags,\n    isRejectedInterviewItem: false,\n  },\n  {\n    id: 'decision_comment',\n    label: 'Comment',\n    getValue: (steps: Record<FeedbackStepId, InterviewFeedbackStepData>) => steps.decision.values?.comment,\n    isRejectedInterviewItem: false,\n  },\n  {\n    id: 'english_certificate',\n    label: 'Certified level of English',\n    getValue: (steps: Record<FeedbackStepId, InterviewFeedbackStepData>) => steps.english.values?.englishCertificate,\n    isRejectedInterviewItem: false,\n  },\n  {\n    id: 'english_selfAssessment',\n    label: 'English level by interviewers opinion',\n    getValue: (steps: Record<FeedbackStepId, InterviewFeedbackStepData>) => steps.english.values?.selfAssessment,\n    isRejectedInterviewItem: false,\n  },\n  {\n    id: 'english_comment',\n    label: 'Where did the student learn English',\n    getValue: (steps: Record<FeedbackStepId, InterviewFeedbackStepData>) => steps.english.values?.comment,\n    isRejectedInterviewItem: false,\n  },\n  {\n    id: 'intro_comment',\n    label: 'Comment',\n    getValue: (steps: Record<FeedbackStepId, InterviewFeedbackStepData>) => steps.intro.values?.comment,\n    isRejectedInterviewItem: true,\n  },\n];\n\nconst FeedbackItem = ({\n  label,\n  value,\n  width = STYLES.feedbackItemWidth,\n}: {\n  label: string;\n  value: unknown;\n  width?: string;\n}) => {\n  if (typeof value === 'string' && value) {\n    return (\n      <Space direction=\"vertical\" style={{ width }}>\n        <Text strong>{label}: </Text>\n        <Text>{value}</Text>\n      </Space>\n    );\n  }\n  return null;\n};\n\nexport function PrescreeningFeedback({ feedback }: { feedback: StageInterviewDetailedFeedback['feedback'] }) {\n  const { steps } = feedback as { steps: Record<FeedbackStepId, InterviewFeedbackStepData> };\n  const { theory, practice } = steps;\n\n  const isRejected = steps.intro.values?.interviewResult === 'missed';\n\n  const displayItems = useMemo(\n    () =>\n      FEEDBACK_CONFIG.filter(item => item.isRejectedInterviewItem === isRejected).map(({ id, label, getValue }) => ({\n        id,\n        label,\n        value: getValue(steps),\n      })),\n\n    [steps, isRejected],\n  );\n\n  return (\n    <Space direction=\"vertical\" size={20}>\n      {displayItems.map(item => (\n        <FeedbackItem key={item.id} label={item.label} value={item.value} />\n      ))}\n      {!isRejected && (\n        <>\n          <SkillSection skills={theory.values} title=\"Theory\" tooltips={SKILLS_LEVELS} />\n          <SkillSection skills={practice.values} title=\"Practice\" tooltips={CODING_LEVELS} />\n        </>\n      )}\n    </Space>\n  );\n}\n\nfunction SkillSection({\n  skills,\n  title,\n  tooltips,\n}: {\n  skills: InterviewFeedbackValues | undefined;\n  title: string;\n  tooltips: string[];\n}) {\n  if (!skills) return null;\n\n  return (\n    <Space direction=\"vertical\">\n      <Title level={4}>{title}</Title>\n      <SkillTable skills={skills.questions as InterviewQuestion[]} tooltips={tooltips} />\n      <FeedbackItem label=\"Comment\" value={skills?.comment} width={STYLES.skillCommentWidth} />\n    </Space>\n  );\n}\n\nfunction SkillTable({ skills, tooltips }: { skills: InterviewQuestion[]; tooltips: string[] }) {\n  return (\n    <Table\n      dataSource={skills}\n      size=\"small\"\n      rowKey=\"id\"\n      pagination={false}\n      columns={[\n        {\n          width: '60%',\n          render: (_, record) => (\n            <>\n              {record.topic && (\n                <Row>\n                  <Text type=\"secondary\">{record.topic}</Text>\n                </Row>\n              )}\n              <Text>{record.title}</Text>\n            </>\n          ),\n        },\n        {\n          dataIndex: 'value',\n          render: rating => <Rating rating={rating} tooltips={tooltips} />,\n        },\n      ]}\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/ui/ScoreWidget.tsx",
    "content": "import { Tag, theme, Typography } from 'antd';\n\nconst { Text } = Typography;\n\nexport function ScoreWidget({ score }: { score: number }) {\n  const { token } = theme.useToken();\n  return (\n    <Text>\n      <Text strong>Score:</Text> <Tag color={token.colorBgSpotlight}>{score}</Tag>\n    </Text>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Profile/ui/__tests__/DateWidget.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { DateWidget } from '@client/components/Profile/ui';\n\ndescribe('DateWidget', () => {\n  it('returns null when no date provided', () => {\n    render(<DateWidget />);\n    const element = screen.queryByTestId('date-widget');\n    expect(element).not.toBeInTheDocument();\n  });\n\n  it('renders formatted date and label when date provided', () => {\n    render(<DateWidget date={'2025-01-15T12:34:56Z'} />);\n    expect(screen.getByTestId('date-widget')).toBeInTheDocument();\n    expect(screen.getByText('Date')).toBeInTheDocument();\n    expect(screen.getByText('2025-01-15')).toBeInTheDocument();\n  });\n\n  it('renders calendar icon', () => {\n    render(<DateWidget date={'2025-01-15T12:34:56Z'} />);\n    expect(screen.getByTestId('date-widget')).toBeInTheDocument();\n    expect(screen.getByRole('img', { name: /calendar/i })).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/ui/__tests__/ExpandButtonWidget.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ExpandButtonWidget } from '@client/components/Profile/ui';\nimport userEvent from '@testing-library/user-event';\n\ndescribe('ExpandButtonWidget', () => {\n  it('should render correctly', () => {\n    render(<ExpandButtonWidget onClick={() => console.log('test')} />);\n    const button = screen.getByRole('button');\n    expect(button).toBeInTheDocument();\n    expect(button.title).toBe('Open details');\n  });\n\n  it('should call onClick callback', async () => {\n    const onClick = vi.fn();\n    render(<ExpandButtonWidget onClick={onClick} />);\n    const button = screen.getByRole('button');\n    await userEvent.click(button);\n    expect(onClick).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/ui/__tests__/InterviewerWidget.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { InterviewerWidget } from '@client/components/Profile/ui';\n\ndescribe('InterviewerWidget', () => {\n  it('renders interviewer name with link to profile', () => {\n    const interviewer = { name: 'Alice', githubId: 'alice' };\n\n    render(<InterviewerWidget interviewer={interviewer} />);\n\n    expect(screen.getByText(/Interviewer/)).toBeInTheDocument();\n\n    const link = screen.getByRole('link', { name: /Alice/ });\n    expect(link).toBeInTheDocument();\n    expect(link).toHaveAttribute('href', expect.stringContaining('/profile?githubId=alice'));\n  });\n\n  it('renders vertical layout without colon in the label', () => {\n    const interviewer = { name: 'Bob', githubId: 'bob' };\n\n    render(<InterviewerWidget interviewer={interviewer} vertical />);\n\n    expect(screen.getByText('Interviewer')).toBeInTheDocument();\n    expect(screen.queryByText('Interviewer:')).not.toBeInTheDocument();\n\n    const link = screen.getByRole('link', { name: /Bob/ });\n    expect(link).toHaveAttribute('href', expect.stringContaining('/profile?githubId=bob'));\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/ui/__tests__/IsGoodCandidateWidget.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { IsGoodCandidateWidget } from '@client/components/Profile/ui';\n\ndescribe('IsGoodCandidateWidget', () => {\n  it('renders Yes tag when isGoodCandidate is true', () => {\n    render(<IsGoodCandidateWidget isGoodCandidate={true} />);\n\n    expect(screen.getByText('Good candidate:')).toBeInTheDocument();\n    expect(screen.getByText('Yes')).toBeInTheDocument();\n  });\n\n  it('renders nothing when isGoodCandidate is false', () => {\n    render(<IsGoodCandidateWidget isGoodCandidate={false} />);\n    expect(screen.queryByText(/Good candidate:/i)).not.toBeInTheDocument();\n  });\n\n  it('renders nothing when isGoodCandidate is null', () => {\n    render(<IsGoodCandidateWidget isGoodCandidate={null} />);\n    expect(screen.queryByText(/Good candidate:/i)).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/ui/__tests__/ScoreWidget.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ScoreWidget } from '@client/components/Profile/ui';\n\ndescribe('ScoreWidget', () => {\n  it('renders label and score value', () => {\n    render(<ScoreWidget score={85} />);\n\n    expect(screen.getByText('Score:')).toBeInTheDocument();\n    expect(screen.getByText('85')).toBeInTheDocument();\n  });\n\n  it('renders zero score', () => {\n    render(<ScoreWidget score={0} />);\n\n    expect(screen.getByText('Score:')).toBeInTheDocument();\n    expect(screen.getByText('0')).toBeInTheDocument();\n  });\n\n  it('renders large score values', () => {\n    render(<ScoreWidget score={123456} />);\n\n    expect(screen.getByText('123456')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/Profile/ui/index.ts",
    "content": "export { ExpandButtonWidget } from './ExpandButtonWidget';\nexport { InterviewerWidget } from './InterviewerWidget';\nexport { IsGoodCandidateWidget } from './IsGoodCandidateWidget';\nexport { LegacyScreeningFeedback } from './LegacyScreeningFeedback';\nexport { PrescreeningFeedback } from './PrescreeningFeedback';\nexport { DateWidget } from './DateWidget';\nexport { ScoreWidget } from './ScoreWidget';\n"
  },
  {
    "path": "client/src/components/RegistrationPageLayout.tsx",
    "content": "import { Layout, Spin } from 'antd';\nimport { mapsApiKey } from '@client/configs/gcp';\nimport Script from 'next/script';\nimport { ReactNode } from 'react';\nimport { Header } from '@client/shared/components/Header';\n\nconst { Content } = Layout;\n\ntype Props = { loading: boolean; title?: string; children?: ReactNode };\n\nconst url = `https://maps.googleapis.com/maps/api/js?key=${mapsApiKey}&libraries=places&language=en`;\n\nexport function RegistrationPageLayout(props: Props) {\n  return (\n    <>\n      {mapsApiKey ? <Script src={url} /> : null}\n      <Layout style={{ minHeight: '100vh' }}>\n        <Header />\n        <Content style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>\n          <Spin spinning={props.loading}>{props.children}</Spin>\n        </Content>\n      </Layout>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/SelectLanguages.tsx",
    "content": "import { Select, SelectProps } from 'antd';\nimport { UpdateUserDtoLanguagesEnum } from '@client/api';\n\nconst languages = Object.values(UpdateUserDtoLanguagesEnum).sort(languagesSorter);\n\nexport function SelectLanguages({ placeholder = 'Select languages', ...props }: SelectProps) {\n  return (\n    <Select\n      mode=\"multiple\"\n      placeholder={placeholder}\n      optionFilterProp=\"label\"\n      options={languages.map(language => ({\n        label: getLanguageName(language),\n        value: language,\n      }))}\n      {...props}\n    />\n  );\n}\n\nexport function getLanguageName(language: UpdateUserDtoLanguagesEnum) {\n  const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });\n  return languageNames.of(language);\n}\n\nfunction languagesSorter(a: UpdateUserDtoLanguagesEnum, b: UpdateUserDtoLanguagesEnum) {\n  const aName = getLanguageName(a);\n  const bName = getLanguageName(b);\n\n  if (!aName || !bName) {\n    return 0;\n  }\n\n  return aName.localeCompare(bName, 'en');\n}\n"
  },
  {
    "path": "client/src/components/SettingsItem.tsx",
    "content": "import { PropsWithChildren, ReactNode, CSSProperties, ComponentType } from 'react';\nimport { Collapse, Divider, Flex, theme, Typography } from 'antd';\n\nconst { Text } = Typography;\nconst { Panel } = Collapse;\n\ntype SettingsItemProps = PropsWithChildren & {\n  header: string;\n  /** Any antd icon */\n  IconComponent: ComponentType<{ style?: CSSProperties }>;\n  actions?: ReactNode[];\n};\n\nconst SettingsItem = ({ children, header, IconComponent, actions }: SettingsItemProps) => {\n  const { token } = theme.useToken();\n  return (\n    <Collapse\n      style={{ marginBottom: 10 }}\n      expandIcon={() => <IconComponent style={{ fontSize: '1.2rem', color: token.blue7 }} />}\n    >\n      <Panel header={<Text strong>{header}</Text>} key={header}>\n        <Flex vertical>\n          <Flex vertical style={{ maxHeight: 'calc(100cqh - 11rem)', overflow: 'auto' }}>\n            {children}\n          </Flex>\n          {actions && <Divider />}\n          <Flex>{actions?.map(action => action)}</Flex>\n        </Flex>\n      </Panel>\n    </Collapse>\n  );\n};\n\nexport default SettingsItem;\n"
  },
  {
    "path": "client/src/components/SlothImage.tsx",
    "content": "import { Image, ImageProps } from 'antd';\n\nexport const slothNames = [\n  'activist',\n  'aws-teamwork',\n  'codewars',\n  'congrats',\n  'congratulations',\n  'congratulations-russian',\n  'deadline',\n  'error',\n  'error-denied',\n  'everything-works',\n  'everything-works-russian',\n  'expert',\n  'feature-in-disguise',\n  'finished-work',\n  'git-problem',\n  'git-remote',\n  'github-friends',\n  'glory-to-ukraine',\n  'google',\n  'google-russian',\n  'helper',\n  'hero',\n  'how-i-see-russian',\n  'hugs-with-aws',\n  'i-saw-your-pr',\n  'interview-with-mentor',\n  'its-a-good-job',\n  'junior',\n  'junior-russian',\n  'lecture-with-mentor',\n  'llike-aws',\n  'mentor',\n  'mentor-with-his-students',\n  'no-mentor-russian',\n  'not-a-bug-russian',\n  'one-hour-before-deadline',\n  'reading-the-chat',\n  'shocked',\n  'slothzy',\n  'slothzy-russian',\n  'so-little-work-i-done',\n  'so-little-work-i-done-russian',\n  'student-without-mentor',\n  'student1',\n  'super-mentor',\n  'team-work-with-boxes',\n  'thanks',\n  'thanks-russian',\n  'this-is-love',\n  'this-is-love-russian',\n  'time-to-pay',\n  'train',\n  'wanted-mentors',\n  'welcome',\n  'what-do-you-want-russian',\n  'what-is-it',\n  'with-boxes',\n  'wtf',\n] as const;\n\nexport type SlothNames = (typeof slothNames)[number];\n\ninterface Props extends ImageProps {\n  name: SlothNames;\n  imgExtension?: 'svg' | 'png';\n}\n\nconst slothsBaseURL = 'https://cdn.rs.school/sloths/stickers/';\n\nexport function SlothImage({ name, imgExtension = 'svg', ...props }: Props) {\n  const src = `${slothsBaseURL}${name}/image.${imgExtension}`;\n\n  return <Image src={src} alt={name} preview={false} {...props} />;\n}\n"
  },
  {
    "path": "client/src/components/Student/AssignStudentModal.tsx",
    "content": "import { Modal, Typography } from 'antd';\nimport { StudentSearch } from '@client/shared/components/StudentSearch';\nimport { useCallback, useState } from 'react';\nimport { CourseService } from '@client/services/course';\nimport { useMessage } from '@client/hooks';\n\nconst { Text } = Typography;\n\ntype Props = {\n  mentorGithuId: string | null;\n  courseId: number;\n  open: boolean;\n  onClose: () => void;\n};\n\nexport function AssignStudentModal(props: Props) {\n  const [studentGithubId, setStudentGithubId] = useState<string | null>(null);\n  const { message } = useMessage();\n\n  const addStudent = useCallback(async () => {\n    if (!studentGithubId) {\n      return;\n    }\n    try {\n      await new CourseService(props.courseId).updateStudent(studentGithubId, { mentorGithuId: props.mentorGithuId });\n      props.onClose();\n      message.success('Student has been added to mentor');\n    } catch (e) {\n      message.error(`${e}`);\n    }\n  }, [props.mentorGithuId, studentGithubId]);\n\n  return (\n    <Modal\n      title={\n        <>\n          <Text>Assign Student to</Text> <Text underline>{props.mentorGithuId}</Text>\n        </>\n      }\n      open={props.open}\n      onOk={addStudent}\n      onCancel={props.onClose}\n    >\n      <StudentSearch\n        style={{ width: '100%' }}\n        keyField=\"githubId\"\n        onChange={setStudentGithubId}\n        courseId={props.courseId}\n      />\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Student/DashboardDetails.module.css",
    "content": ".studentDetailsActions :global(.ant-btn) {\n  margin: 0 8px 8px 0;\n}\n"
  },
  {
    "path": "client/src/components/Student/DashboardDetails.tsx",
    "content": "import {\n  BranchesOutlined,\n  CloseCircleOutlined,\n  FileExcelOutlined,\n  SolutionOutlined,\n  UndoOutlined,\n} from '@ant-design/icons';\nimport { Button, Descriptions, Drawer, Popconfirm, theme } from 'antd';\nimport { MentorBasic } from '@common/models';\nimport { CommentModal } from '@client/shared/components/CommentModal';\nimport { MentorSearch } from '@client/shared/components/MentorSearch';\nimport { useState } from 'react';\nimport { StudentDetails } from '@client/services/course';\nimport styles from './DashboardDetails.module.css';\n\ntype Props = {\n  details: StudentDetails | null;\n  courseId: number;\n  isLoading: boolean;\n  isAdmin: boolean;\n  onClose: () => void;\n  onCreateRepository: () => void;\n  onRestoreStudent: () => void;\n  onExpelStudent: (comment: string) => void;\n  onIssueCertificate: () => void;\n  onRemoveCertificate: () => void;\n  onUpdateMentor: (githubId: string) => void;\n  courseManagerOrSupervisor: boolean;\n};\n\nexport function DashboardDetails(props: Props) {\n  const [expelMode, setExpelMode] = useState(false);\n  const { details } = props;\n  const { token } = theme.useToken();\n  if (details == null) {\n    return null;\n  }\n\n  return (\n    <>\n      <Drawer\n        width={props.isAdmin ? 660 : 600}\n        title={`${details.name} , ${details.githubId}`}\n        placement=\"right\"\n        closable={false}\n        onClose={props.onClose}\n        open={!!details}\n      >\n        <div className={styles.studentDetailsActions}>\n          {props.courseManagerOrSupervisor && (\n            <>\n              <Button\n                disabled={!details.isActive || !!details.repository}\n                icon={<BranchesOutlined />}\n                onClick={props.onCreateRepository}\n              >\n                Create Repository\n              </Button>\n              <Popconfirm title=\"Are you sure you want to issue the certificate?\" onConfirm={props.onIssueCertificate}>\n                <Button disabled={!details.isActive} icon={<SolutionOutlined />} loading={props.isLoading}>\n                  Issue Certificate\n                </Button>\n              </Popconfirm>\n              {props.isAdmin && (\n                <Popconfirm\n                  title=\"Are you sure you want to remove the certificate?\"\n                  onConfirm={props.onRemoveCertificate}\n                >\n                  <Button\n                    danger\n                    icon={<FileExcelOutlined style={{ color: token.colorError }} />}\n                    loading={props.isLoading}\n                  >\n                    Remove Certificate\n                  </Button>\n                </Popconfirm>\n              )}\n            </>\n          )}\n          <Button\n            danger\n            hidden={!details.isActive}\n            icon={<CloseCircleOutlined style={{ color: token.colorError }} />}\n            onClick={() => setExpelMode(true)}\n          >\n            Expel\n          </Button>\n          <Button hidden={details.isActive} icon={<UndoOutlined />} onClick={props.onRestoreStudent}>\n            Restore\n          </Button>\n          {props.courseManagerOrSupervisor && (\n            <Descriptions bordered layout=\"vertical\" size=\"small\" column={1}>\n              <Descriptions.Item label=\"Mentor\">\n                <MentorSearch\n                  disabled={!details.isActive}\n                  style={{ width: '100%' }}\n                  onChange={props.onUpdateMentor}\n                  courseId={props.courseId}\n                  keyField=\"githubId\"\n                  value={(details.mentor as MentorBasic)?.githubId}\n                  defaultValues={details.mentor ? [details.mentor as any] : []}\n                />\n              </Descriptions.Item>\n            </Descriptions>\n          )}\n        </div>\n      </Drawer>\n      <CommentModal\n        title=\"Expelling Reason\"\n        visible={expelMode}\n        onCancel={() => setExpelMode(false)}\n        onOk={(text: string) => {\n          props.onExpelStudent(text);\n          setExpelMode(false);\n        }}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/components/Student/index.ts",
    "content": "export { AssignStudentModal } from './AssignStudentModal';\nexport { DashboardDetails } from './DashboardDetails';\n"
  },
  {
    "path": "client/src/components/StudentDiscord.tsx",
    "content": "import { Typography } from 'antd';\nimport { Discord } from '@client/api';\nimport CopyToClipboardButton from '@client/shared/components/CopyToClipboardButton';\n\ntype Props = {\n  discord: Discord | null;\n  textPrefix?: string;\n};\n\nexport function StudentDiscord({ discord, textPrefix }: Props) {\n  const discordDiscriminator = discord?.discriminator !== '0' ? `#${discord?.discriminator}` : '';\n  const discordUsername = discord ? `@${discord.username}${discordDiscriminator}` : null;\n\n  return (\n    <Typography.Paragraph style={{ margin: 0 }}>\n      {textPrefix && `${textPrefix} `}\n      {discordUsername ? (\n        <>\n          <Typography.Link target=\"_blank\" href={`https://discordapp.com/users/${discord?.id}`}>\n            {discordUsername}\n          </Typography.Link>{' '}\n          <CopyToClipboardButton value={discordUsername} />\n        </>\n      ) : (\n        'unknown'\n      )}\n    </Typography.Paragraph>\n  );\n}\n"
  },
  {
    "path": "client/src/components/TabsWithCounter/renderers.tsx",
    "content": "import { ReactNode } from 'react';\nimport { Space } from 'antd';\nimport { CountBadge } from '@client/components/CountBadge';\n\nexport type LabelItem = {\n  label: string;\n  key: string;\n  count: number;\n};\n\ntype TabItem = {\n  label: ReactNode;\n  key: string;\n};\n\nexport const tabRenderer = ({ count, label, key }: LabelItem, activeTab?: string): TabItem => {\n  const isTabActive = activeTab === key;\n  return {\n    key,\n    label: count ? (\n      <Space>\n        {label}\n        <CountBadge showZero count={count} status={isTabActive ? 'processing' : 'default'} />\n      </Space>\n    ) : (\n      label\n    ),\n  };\n};\n"
  },
  {
    "path": "client/src/components/Warning/index.tsx",
    "content": "import { Row } from 'antd';\nimport Image from 'next/image';\nimport { PageLayout } from '@client/shared/components/PageLayout';\n\nconst defaultMessage = 'Something went wrong, please try reloading the page later';\nconst defaultImageName = 'Image error';\n\ntype Props = {\n  imagePath: string;\n  imageName: string;\n  textMessage: string | JSX.Element;\n  loading?: boolean;\n};\n\nexport function Warning(props: Props) {\n  const { loading = false, imagePath, imageName = defaultImageName, textMessage = defaultMessage } = props;\n  return (\n    <PageLayout loading={loading}>\n      <Row justify=\"center\" style={{ margin: '65px 0 25px 0' }}>\n        <Image src={`/static${imagePath}`} alt={imageName} width={175} height={175} />\n      </Row>\n      <Row justify=\"center\">\n        <h1 style={{ fontSize: '36px', marginBottom: 0 }}>{textMessage}</h1>\n      </Row>\n    </PageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/components/WelcomeCard.tsx",
    "content": "import Link from 'next/link';\nimport { Row, Image, Layout, Space, Typography, Alert, Button, Divider } from 'antd';\n\nconst { Text } = Typography;\n\nexport function WelcomeCard() {\n  return (\n    <Layout style={{ background: 'transparent', minHeight: '100vh' }}>\n      <Row justify=\"center\" style={{ margin: '65px 0 25px 0' }}>\n        <Image\n          src=\"https://cdn.rs.school/sloths/stickers/welcome/image.png\"\n          preview={false}\n          alt=\"welcome\"\n          width={240}\n          height={240}\n        />\n      </Row>\n      <Row justify=\"center\">\n        <Space direction=\"vertical\" align=\"center\" size=\"middle\" style={{ padding: '20px' }}>\n          <Alert message=\"Welcome to RS School App! Please register to continue\" type=\"info\" showIcon />\n          <Space direction=\"vertical\" align=\"center\">\n            <Space wrap>\n              <Link href=\"/registry/student\">\n                <Button type=\"default\">Register as a student 🎓</Button>\n              </Link>\n              <Link href=\"/registry/mentor\">\n                <Button type=\"default\">Register as a mentor 🌟</Button>\n              </Link>\n            </Space>\n\n            <Divider />\n\n            <Text>If you made a mistake and used the wrong GitHub account, you can log in with another one:</Text>\n            <Link href=\"/login\">\n              <Button type=\"default\">Log in with another GitHub account 🔄</Button>\n            </Link>\n          </Space>\n        </Space>\n      </Row>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "client/src/components/__tests__/CopyToClipboardButton.test.tsx",
    "content": "import { act, render, screen } from '@testing-library/react';\nimport CopyToClipboardButton from '@client/shared/components/CopyToClipboardButton';\nimport { useCopyToClipboard } from 'react-use';\n\nvi.mock('@client/hooks', () => ({\n  useMessage: () => ({\n    message: { success: mockSuccess },\n  }),\n}));\n\nvi.mock('react-use', () => ({\n  useCopyToClipboard: vi.fn(),\n}));\n\nconst TEST_VALUE = 'test-value';\n\nconst mockSuccess = vi.fn();\nconst mockCopyToClipboard = vi.fn();\n\ndescribe('CopyToClipboardButton', () => {\n  beforeEach(() => {\n    vi.mocked(useCopyToClipboard).mockReturnValue([null, mockCopyToClipboard]);\n  });\n\n  it('should render with default style', () => {\n    render(<CopyToClipboardButton value={TEST_VALUE} />);\n    const button = screen.getByTestId('copy-to-clipboard');\n    expect(button).toBeInTheDocument();\n    expect(button).toHaveClass('ant-btn-dashed');\n  });\n\n  it('should render button with copy icon', () => {\n    render(<CopyToClipboardButton value={TEST_VALUE} />);\n    const icon = screen.getByRole('img');\n    expect(icon).toBeInTheDocument();\n    expect(icon).toHaveClass('anticon anticon-copy');\n  });\n\n  it('should copy text to clipboard on click', async () => {\n    render(<CopyToClipboardButton value={TEST_VALUE} />);\n    const button = screen.getByTestId('copy-to-clipboard');\n\n    act(() => button.click());\n\n    expect(mockCopyToClipboard).toHaveBeenCalledWith(TEST_VALUE);\n  });\n\n  it('should render with custom button type', () => {\n    render(<CopyToClipboardButton value={TEST_VALUE} type=\"primary\" />);\n    const button = screen.getByTestId('copy-to-clipboard');\n    expect(button).toHaveAttribute('type', 'button');\n    expect(button).toHaveClass('ant-btn-primary');\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/GithubUserLink.test.tsx",
    "content": "import { act, render, screen } from '@testing-library/react';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { useCopyToClipboard } from 'react-use';\n\nvi.mock('@client/hooks', () => ({\n  useMessage: () => ({\n    message: { success: mockSuccess },\n  }),\n}));\n\nvi.mock('react-use', () => ({\n  useCopyToClipboard: vi.fn(),\n}));\n\nconst TEST_VALUE = 'test-value';\nconst TEST_FULL_NAME = 'test-full-name';\n\nconst mockSuccess = vi.fn();\nconst mockCopyToClipboard = vi.fn();\n\ndescribe('GithubUserLink', () => {\n  beforeEach(() => {\n    vi.mocked(useCopyToClipboard).mockReturnValue([null, mockCopyToClipboard]);\n  });\n\n  it('should render correct links', () => {\n    const DEFAULT_NUMBER_OF_LINKS = 2;\n    render(<GithubUserLink value={TEST_VALUE} />);\n    const links = screen.getAllByRole('link');\n    expect(links.length).toBe(DEFAULT_NUMBER_OF_LINKS);\n    expect(links.some(l => l.getAttribute('href') === `/profile?githubId=${TEST_VALUE}`)).toBe(true);\n    expect(links.some(l => l.getAttribute('href') === `https://github.com/${TEST_VALUE}`)).toBe(true);\n  });\n\n  it('should render provided full name', () => {\n    render(<GithubUserLink value={TEST_VALUE} fullName={TEST_FULL_NAME} />);\n    expect(screen.getByText(TEST_FULL_NAME)).toBeInTheDocument();\n  });\n\n  it('should not render user avatar by default', () => {\n    render(<GithubUserLink value={TEST_VALUE} />);\n    const imgs = screen.getAllByRole('img');\n    const avatar = imgs.some(img => img.getAttribute('src')?.includes('avatars'));\n    expect(avatar).toBe(true);\n  });\n\n  it('should not render user avatar if isUserIconHidden === false', () => {\n    render(<GithubUserLink value={TEST_VALUE} isUserIconHidden={true} />);\n    const imgs = screen.getAllByRole('img');\n    const avatar = imgs.some(img => img.getAttribute('src')?.includes('avatars'));\n    expect(avatar).toBe(false);\n  });\n\n  it('should render copy button be default', () => {\n    render(<GithubUserLink value={TEST_VALUE} />);\n    const copyButton = screen.getByTitle('Copy GitHub name to clipboard');\n    expect(copyButton).toBeInTheDocument();\n  });\n\n  it('should no render copy button if copyable === false', () => {\n    render(<GithubUserLink value={TEST_VALUE} copyable={false} />);\n    const copyButton = screen.queryByTitle('Copy GitHub name to clipboard');\n    expect(copyButton).not.toBeInTheDocument();\n  });\n\n  it('should copy value to clipboard on click', async () => {\n    render(<GithubUserLink value={TEST_VALUE} />);\n    const copyButton = screen.getByTitle('Copy GitHub name to clipboard');\n    await act(async () => copyButton.click());\n    expect(mockCopyToClipboard).toHaveBeenCalledWith(TEST_VALUE);\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/Rating.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Rating } from '@client/shared/components/Rating';\n\ndescribe('Rating', () => {\n  it('renders tooltip label based on rounded integer value when tooltips provided', () => {\n    const tooltips = ['terrible', 'bad', 'normal', 'good', 'wonderful'];\n\n    render(<Rating rating={3.7} tooltips={tooltips} />);\n\n    expect(screen.getByText('good')).toBeInTheDocument();\n  });\n\n  it('renders numeric value with two decimals when tooltips are not provided', () => {\n    render(<Rating rating={4.166} />);\n\n    expect(screen.getByText('4.17')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/__tests__/StudenDiscrod.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Discord } from '@client/api';\nimport { StudentDiscord } from '@client/components/StudentDiscord';\n\ndescribe('StudentDiscord', () => {\n  test('renders a Discord user correctly', () => {\n    const discord: Discord = {\n      id: '123456',\n      username: 'TestUser',\n      discriminator: '1234',\n    };\n\n    render(<StudentDiscord discord={discord} />);\n\n    const userLink = screen.getByText('@TestUser#1234');\n    expect(userLink).toBeInTheDocument();\n    expect(userLink).toHaveAttribute('href', 'https://discordapp.com/users/123456');\n  });\n\n  test('renders \"unknown\" for null Discord data', () => {\n    render(<StudentDiscord discord={null} />);\n    expect(screen.getByText('unknown')).toBeInTheDocument();\n  });\n\n  test('renders the text prefix if provided', () => {\n    const discord: Discord = {\n      id: '123456',\n      username: 'TestUser',\n      discriminator: '1234',\n    };\n\n    render(<StudentDiscord discord={discord} textPrefix=\"Discord user\" />);\n    expect(screen.getByText('Discord user')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/components/common/CustomPopconfirm.tsx",
    "content": "import { Popconfirm, PopconfirmProps } from 'antd';\n\nexport const CustomPopconfirm = ({ placement, ...props }: PopconfirmProps) => {\n  return <Popconfirm placement={placement ?? 'topRight'} {...props} />;\n};\n"
  },
  {
    "path": "client/src/components/useLoading.tsx",
    "content": "import { useState } from 'react';\nimport { message } from 'antd';\n\ntype CatchHandler = (e?: unknown) => void;\n\nexport function useLoading(\n  value = false,\n  catchHandler: CatchHandler = () => {\n    message.error('An unexpected error occurred. Please try later.');\n  },\n) {\n  const [loading, setLoading] = useState(value);\n  const wrapper =\n    <T extends unknown[], K = unknown>(action: (...args: T) => Promise<K>) =>\n    async (...args: Parameters<typeof action>) => {\n      try {\n        setLoading(true);\n        return await action(...args);\n      } catch (e) {\n        catchHandler(e);\n      } finally {\n        setLoading(false);\n      }\n    };\n  return [loading, wrapper] as const;\n}\n"
  },
  {
    "path": "client/src/components/withGoogleMaps.tsx",
    "content": "import * as React from 'react';\nimport Head from 'next/head';\nimport { mapsApiKey } from '@client/configs/gcp';\n\nconst url = `https://maps.googleapis.com/maps/api/js?key=${mapsApiKey}&libraries=places&language=en`;\n\nfunction withGoogleMaps<T = object>(WrappedComponent: React.ComponentType<T>) {\n  return class extends React.Component<T> {\n    render() {\n      return (\n        <>\n          {mapsApiKey && (\n            <Head>\n              <script async src={url}></script>\n            </Head>\n          )}\n          <WrappedComponent {...this.props} />\n        </>\n      );\n    }\n  };\n}\n\nexport { withGoogleMaps };\n"
  },
  {
    "path": "client/src/components/withSession.tsx",
    "content": "import { CourseRole } from '@client/services/models';\n\nexport interface CourseInfo {\n  mentorId?: number;\n  studentId?: number;\n  roles: CourseRole[];\n  isExpelled?: boolean;\n}\n\nexport interface Session {\n  id: number;\n  githubId: string;\n  isAdmin: boolean;\n  isHirer: boolean;\n  courses: { [courseId: string]: CourseInfo | undefined };\n}\n"
  },
  {
    "path": "client/src/configs/cdn.ts",
    "content": "export const CDN_AVATARS_URL = 'https://cdn.rs.school/avatars';\n"
  },
  {
    "path": "client/src/configs/course-icons.ts",
    "content": "export const DEFAULT_COURSE_ICONS: Record<string, { active: string; archived: string; label: string }> = {\n  javascript: {\n    active: '/static/svg/disciplines/javascript.svg',\n    archived: '/static/svg/disciplines/javascript-archived.svg',\n    label: 'Javascript',\n  },\n  angular: {\n    active: '/static/svg/disciplines/angular.svg',\n    archived: '/static/svg/disciplines/angular-archived.svg',\n    label: 'Angular',\n  },\n  reactjs: {\n    active: '/static/svg/disciplines/reactjs.svg',\n    archived: '/static/svg/disciplines/reactjs-archived.svg',\n    label: 'Reactjs',\n  },\n  android: {\n    active: '/static/svg/disciplines/android.svg',\n    archived: '/static/svg/disciplines/android-archived.svg',\n    label: 'Android',\n  },\n  ios: {\n    active: '/static/svg/disciplines/ios.svg',\n    archived: '/static/svg/disciplines/ios-archived.svg',\n    label: 'iOS',\n  },\n  nodejs: {\n    active: '/static/svg/disciplines/nodejs.svg',\n    archived: '/static/svg/disciplines/nodejs-archived.svg',\n    label: 'Nodejs',\n  },\n  ml: {\n    active: '/static/svg/disciplines/machine-learning.svg',\n    archived: '/static/svg/disciplines/machine-learning-archived.svg',\n    label: 'Machine learning',\n  },\n  nodejsAws: {\n    active: '/static/svg/disciplines/nodejs-aws.svg',\n    archived: '/static/svg/disciplines/nodejs-aws-archived.svg',\n    label: 'Nodejs In AWS',\n  },\n};\n"
  },
  {
    "path": "client/src/configs/discord-integration.ts",
    "content": "const isDevMode = process.env.NODE_ENV !== 'production';\nconst clientId = isDevMode ? '625945676009963521' : '978920245743976448';\nconst redirectUrl = isDevMode ? 'http://localhost:3000/profile' : 'https://app.rs.school/profile';\n\nexport default {\n  api: {\n    auth: `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(\n      redirectUrl,\n    )}&response_type=token&scope=identify`,\n    me: 'https://discordapp.com/api/users/@me',\n  },\n};\n"
  },
  {
    "path": "client/src/configs/gcp.ts",
    "content": "export const mapsApiKey = process.env.RSSHCOOL_UI_GCP_MAPS_API_KEY;\n"
  },
  {
    "path": "client/src/configs/heroes-badges.ts",
    "content": "const heroesBadges: Record<string, { name: string; pictureId: number; url: string }> = {\n  Congratulations: {\n    name: 'Congratulations',\n    pictureId: 23,\n    url: 'Congratulations.svg',\n  },\n  Expert_help: {\n    name: 'Expert help',\n    pictureId: 39,\n    url: 'ExpertHelp.svg',\n  },\n  Great_speaker: {\n    name: 'Great speaker',\n    pictureId: 29,\n    url: 'GreatSpeaker.svg',\n  },\n  Good_job: {\n    name: 'Good job',\n    pictureId: 17,\n    url: 'GoodJob.svg',\n  },\n  Helping_hand: {\n    name: 'Helping hand',\n    pictureId: 14,\n    url: 'HelpingHandSloth.svg',\n  },\n  Hero: {\n    name: 'Hero',\n    pictureId: 25,\n    url: 'Hero.svg',\n  },\n  Thank_you: {\n    name: 'Thank you',\n    pictureId: 12,\n    url: 'ThankYou.svg',\n  },\n  Outstanding_work: {\n    name: 'Outstanding work',\n    pictureId: 33,\n    url: 'OutstandingWork.svg',\n  },\n  Top_performer: {\n    name: 'Top performer',\n    pictureId: 34,\n    url: 'TopPerformer.svg',\n  },\n  Job_Offer: {\n    name: 'Job Offer',\n    pictureId: 35,\n    url: 'JobOffer.svg',\n  },\n  RS_activist: {\n    name: 'RS activist',\n    pictureId: 36,\n    url: 'RSActivist.svg',\n  },\n  Jury_Team: {\n    name: 'Jury team',\n    pictureId: 36,\n    url: 'JuryTeam.svg',\n  },\n  Mentor: {\n    name: 'Mentor',\n    pictureId: 37,\n    url: 'Mentor.svg',\n  },\n  Coordinator: {\n    name: 'Coordinator',\n    pictureId: 38,\n    url: 'Coordinator.svg',\n  },\n  Contributor: {\n    name: 'Contributor',\n    pictureId: 40,\n    url: 'Contributor.svg',\n  },\n  Thanks: {\n    name: 'Thanks',\n    pictureId: 41,\n    url: 'Thanks.svg',\n  },\n};\n\nexport default heroesBadges;\n"
  },
  {
    "path": "client/src/configs/registry.ts",
    "content": "import { Session } from '@client/components/withSession';\nimport { Course } from '@client/services/models';\n\nexport const TYPES = {\n  MENTOR: 'mentor',\n  STUDENT: 'student',\n};\n\nexport type Props = {\n  courses?: Course[];\n  session: Session;\n};\n"
  },
  {
    "path": "client/src/configs/timezones.ts",
    "content": "export const TIMEZONES = [\n  'UTC',\n  'Europe/London',\n  'Europe/Warsaw',\n  'Europe/Kiev',\n  'Europe/Minsk',\n  'Europe/Moscow',\n  'Europe/Volgograd',\n  'Asia/Yekaterinburg',\n  'Asia/Tashkent',\n  'Asia/Tbilisi',\n];\n\nconst europeTimezones = [\n  'Europe/Amsterdam',\n  'Europe/Andorra',\n  'Europe/Athens',\n  'Europe/Belgrade',\n  'Europe/Berlin',\n  'Europe/Bratislava',\n  'Europe/Brussels',\n  'Europe/Bucharest',\n  'Europe/Budapest',\n  'Europe/Chisinau',\n  'Europe/Copenhagen',\n  'Europe/Helsinki',\n  'Europe/Istanbul',\n  'Europe/Kiev',\n  'Europe/Lisbon',\n  'Europe/Ljubljana',\n  'Europe/Luxembourg',\n  'Europe/Madrid',\n  'Europe/Malta',\n  'Europe/Minsk',\n  'Europe/Monaco',\n  'Europe/Moscow',\n  'Europe/Oslo',\n  'Europe/Paris',\n  'Europe/Prague',\n  'Europe/Riga',\n  'Europe/Rome',\n  'Europe/San_Marino',\n  'Europe/Sofia',\n  'Europe/Stockholm',\n  'Europe/Tallinn',\n  'Europe/Vienna',\n  'Europe/Vilnius',\n  'Europe/Warsaw',\n  'Europe/Zaporozhye',\n  'Europe/Zurich',\n];\n\nconst asiaTimezones = [\n  'Asia/Almaty',\n  'Asia/Amman',\n  'Asia/Anadyr',\n  'Asia/Aqtau',\n  'Asia/Aqtobe',\n  'Asia/Ashgabat',\n  'Asia/Baghdad',\n  'Asia/Bahrain',\n  'Asia/Baku',\n  'Asia/Bangkok',\n  'Asia/Beirut',\n  'Asia/Bishkek',\n  'Asia/Brunei',\n  'Asia/Choibalsan',\n  'Asia/Chongqing',\n  'Asia/Colombo',\n  'Asia/Damascus',\n  'Asia/Dhaka',\n  'Asia/Dili',\n  'Asia/Dubai',\n  'Asia/Dushanbe',\n  'Asia/Tashkent',\n  'Asia/Tbilisi',\n  'Asia/Tehran',\n  'Asia/Thimphu',\n  'Asia/Tokyo',\n  'Asia/Ulaanbaatar',\n  'Asia/Urumqi',\n  'Asia/Ust-Nera',\n  'Asia/Vientiane',\n  'Asia/Vladivostok',\n  'Asia/Yakutsk',\n  'Asia/Yangon',\n  'Asia/Yekaterinburg',\n];\n\nconst americaTimezones = [\n  'America/Araguaina',\n  'America/Argentina/Buenos_Aires',\n  'America/Barbados',\n  'America/Belem',\n  'America/Belize',\n  'America/Bogota',\n  'America/Cancun',\n  'America/Caracas',\n  'America/Cayenne',\n  'America/Chicago',\n  'America/Chihuahua',\n  'America/Costa_Rica',\n  'America/Denver',\n  'America/Detroit',\n  'America/Edmonton',\n  'America/Guatemala',\n  'America/Jamaica',\n  'America/Los_Angeles',\n  'America/Mexico_City',\n  'America/Monterrey',\n  'America/Montevideo',\n  'America/Montreal',\n  'America/New_York',\n  'America/Santiago',\n  'America/Sao_Paulo',\n];\n\nexport const ALL_TIMEZONES = ['UTC', ...europeTimezones, ...asiaTimezones, ...americaTimezones];\n\nexport const DEFAULT_TIMEZONE = 'Europe/Minsk';\n"
  },
  {
    "path": "client/src/data/course-leave-reasons.ts",
    "content": "export const CourseLeaveReason = {\n  TooDifficult: 'too_difficult',\n  NotUseful: 'not_useful',\n  LackOfTime: 'lack_of_time',\n  Other: 'other',\n  NoInterest: 'no_interest',\n  PoorQuality: 'poor_quality',\n  FoundAlternative: 'found_alternative',\n  PersonalReasons: 'personal_reasons',\n  GotJob: 'got_job',\n  GotInternship: 'got_internship',\n} as const;\n\nexport type CourseLeaveReason = (typeof CourseLeaveReason)[keyof typeof CourseLeaveReason];\n"
  },
  {
    "path": "client/src/data/english.ts",
    "content": "export const ENGLISH_LEVELS = ['A0', 'A1', 'A1+', 'A2', 'A2+', 'B1', 'B1+', 'B2', 'B2+', 'C1', 'C1+', 'C2'] as const;\n"
  },
  {
    "path": "client/src/data/eventTypes.ts",
    "content": "import { CourseEventDtoTypeEnum } from '@client/api';\n\nexport const EVENT_TYPES: { id: CourseEventDtoTypeEnum; name: string }[] = [\n  { name: 'Online Lecture', id: CourseEventDtoTypeEnum.LectureOnline },\n  { name: 'Offline Lecture', id: CourseEventDtoTypeEnum.LectureOffline },\n  { name: 'Online/Offline Lecture', id: CourseEventDtoTypeEnum.LectureMixed },\n  { name: 'Self-studying', id: CourseEventDtoTypeEnum.LectureSelfStudy },\n  { name: 'Warm-up', id: CourseEventDtoTypeEnum.Warmup },\n  { name: 'Info', id: CourseEventDtoTypeEnum.Info },\n  { name: 'Webinar', id: CourseEventDtoTypeEnum.Webinar },\n  { name: 'Workshop', id: CourseEventDtoTypeEnum.Workshop },\n  { name: 'Meetup', id: CourseEventDtoTypeEnum.Meetup },\n  { name: 'Special', id: CourseEventDtoTypeEnum.Special },\n  { name: 'Cross-Check deadline', id: CourseEventDtoTypeEnum.CrossCheckDeadline },\n];\n\nexport const EVENT_TYPES_MAP = EVENT_TYPES.reduce(\n  (acc, { id, name }) => ({ ...acc, [id]: name }),\n  {} as Record<string, string>,\n);\n"
  },
  {
    "path": "client/src/data/index.ts",
    "content": "import { EVENT_TYPES_MAP } from './eventTypes';\nimport { TASK_TYPES_MAP } from './taskTypes';\n\nexport const TASK_EVENT_TYPES_MAP = {\n  ...TASK_TYPES_MAP,\n  ...EVENT_TYPES_MAP,\n};\n"
  },
  {
    "path": "client/src/data/interviews/__tests__/templateValidator.test.ts",
    "content": "import { InterviewTemplate, Question, QuestionCategory } from '@client/data/interviews';\nimport { validateInterviewTemplate, validateQuestionCategory, TemplateValidationError } from '../templateValidator';\n\nconst mockQuestion1: Question = { id: 101, name: 'Q1' };\nconst mockQuestion2: Question = { id: 102, name: 'Q2' };\nconst mockQuestionDuplicateId: Question = { id: 101, name: 'Q_DUP' };\n\nconst mockCategory1: QuestionCategory = {\n  id: 1,\n  name: 'Behavioral',\n  questions: [mockQuestion1, mockQuestion2],\n};\n\nconst mockCategory2: QuestionCategory = {\n  id: 2,\n  name: 'Technical',\n  description: 'Deep dive questions',\n  questions: [],\n};\n\nconst mockCategoryDuplicateId: QuestionCategory = {\n  id: 1,\n  name: 'Duplicate Category',\n  questions: [],\n};\n\nconst mockValidTemplate: InterviewTemplate = {\n  name: 'Valid Tech Interview',\n  categories: [mockCategory1, mockCategory2],\n  examplesUrl: 'http://example.com/examples',\n  descriptionHtml: '<p>A description</p>',\n};\n\nconst mockTemplateWithDuplicateCategory: InterviewTemplate = {\n  name: 'Invalid Template - Dup Cat ID',\n  categories: [mockCategory1, mockCategoryDuplicateId],\n  examplesUrl: 'http://example.com/examples',\n};\n\nconst mockCategoryWithDuplicateQuestions: QuestionCategory = {\n  id: 3,\n  name: 'Invalid Category - Dup Q ID',\n  questions: [mockQuestion1, mockQuestionDuplicateId], // IDs are 101, 101\n};\n\ndescribe('TemplateValidationError', () => {\n  it('should be an instance of Error', () => {\n    const error = new TemplateValidationError('Test');\n    expect(error).toBeInstanceOf(Error);\n  });\n\n  it('should have the correct name property', () => {\n    const error = new TemplateValidationError('Test');\n    expect(error.name).toBe('TemplateValidationError');\n    expect(error.message).toBe('Test');\n  });\n\n  it('should have the correct message property', () => {\n    const error = new TemplateValidationError('Test');\n    expect(error.message).toBe('Test');\n  });\n});\n\ndescribe('validateQuestionCategory', () => {\n  it('should successfully validate a category with unique question IDs', () => {\n    expect(() => validateQuestionCategory(mockCategory1)).not.toThrow();\n  });\n\n  it('should successfully validate a category with no questions', () => {\n    expect(() => validateQuestionCategory(mockCategory2)).not.toThrow();\n  });\n\n  it('should throw TemplateValidationError for duplicate question IDs', () => {\n    expect(() => validateQuestionCategory(mockCategoryWithDuplicateQuestions)).toThrow(TemplateValidationError);\n    expect(() => validateQuestionCategory(mockCategoryWithDuplicateQuestions)).toThrow(\n      'Questions must have unique ids. Category ID: 3',\n    );\n  });\n});\n\ndescribe('validateInterviewTemplate', () => {\n  it('should successfully validate a valid template with unique category and question IDs', () => {\n    expect(() => validateInterviewTemplate(mockValidTemplate)).not.toThrow();\n  });\n\n  it('should throw TemplateValidationError for duplicate category IDs', () => {\n    expect(() => validateInterviewTemplate(mockTemplateWithDuplicateCategory)).toThrow(TemplateValidationError);\n    expect(() => validateInterviewTemplate(mockTemplateWithDuplicateCategory)).toThrow(\n      'Categories must have unique ids. Template name: Invalid Template - Dup Cat ID',\n    );\n  });\n\n  it('should throw TemplateValidationError when a nested category has duplicate question IDs, and prepend template name', () => {\n    const templateWithNestedError: InterviewTemplate = {\n      name: 'Template-WithError',\n      categories: [mockCategory1, mockCategoryWithDuplicateQuestions],\n      examplesUrl: 'url',\n    };\n\n    expect(() => validateInterviewTemplate(templateWithNestedError)).toThrow(TemplateValidationError);\n    expect(() => validateInterviewTemplate(templateWithNestedError)).toThrow(\n      'Template name Template-WithError. Questions must have unique ids. Category ID: 3',\n    );\n  });\n});\n"
  },
  {
    "path": "client/src/data/interviews/angular.ts",
    "content": "import { InterviewTemplate } from './types';\n\nexport const angularTemplate: InterviewTemplate = {\n  name: 'Angular interview ',\n  examplesUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/angular/modules/interview/questions.md',\n  categories: [\n    {\n      id: 3000,\n      name: 'General',\n      questions: [\n        { id: 3001, name: 'What is Angular and what is it used for?' },\n        { id: 3002, name: 'What is Angular CLI and what are its main features?' },\n        { id: 3003, name: 'What is a module in Angular, and what is its role in an application?' },\n        {\n          id: 3004,\n          name: 'What is inter-component communication in Angular?(e.g., @Input/@Output, services with Observables, etc.).',\n        },\n        {\n          id: 3005,\n          name: 'Shadow DOM in Angular',\n        },\n      ],\n    },\n    {\n      id: 3010,\n      name: 'Components',\n      questions: [\n        {\n          id: 3011,\n          name: 'What are Components in Angular, and how are they the foundation of an application structure?',\n        },\n        {\n          id: 3012,\n          name: \"How do you configure a component's selector, template, and style using the @Component decorator?\",\n        },\n        {\n          id: 3013,\n          name: 'How would you explain the component lifecycle and its main methods (e.g., ngOnInit, ngOnChanges, ngOnDestroy)?',\n        },\n        {\n          id: 3014,\n          name: 'How does two-way data binding work in Angular, and how does it differ from one-way data binding?',\n        },\n        { id: 3015, name: 'Standalone components.' },\n        { id: 3016, name: 'What are ViewChild and ViewChildren?' },\n        { id: 3017, name: 'What is the difference between ElementRef and Renderer2?' },\n        { id: 3018, name: 'How do HostBinding and HostListener decorators work?' },\n      ],\n    },\n    {\n      id: 3020,\n      name: 'Directives',\n      questions: [\n        { id: 3021, name: 'What are \"Directives\" in Angular, and what are they used for?' },\n        {\n          id: 3022,\n          name: 'How do you create and use a custom directive? Explain the use of the \"@Directive\" decorator',\n        },\n        {\n          id: 3023,\n          name: 'What is the difference between structural and attribute directives? Please provide examples.',\n        },\n        { id: 3024, name: 'Explain *ngIf and *ngFor and their usage.' },\n        { id: 3025, name: 'What is the difference between *ngIf and [hidden]?' },\n        {\n          id: 3026,\n          name: 'What is the purpose of *ngSwitch, *ngSwitchCase, and *ngSwitchDefault, and how do you use them?',\n        },\n        { id: 3027, name: 'What is the difference between *ngStyle and *ngClass?' },\n        { id: 3028, name: 'What is *ngContainer and what is it used for? Provide an example.' },\n        { id: 3029, name: 'How do you create custom structural directives using <ng-template>?' },\n      ],\n    },\n    {\n      id: 3030,\n      name: 'Pipe',\n      questions: [\n        { id: 3031, name: 'What is a Pipe, and what is its purpose in Angular?' },\n        { id: 3032, name: 'Can you provide examples of some built-in pipes (e.g., date, uppercase, lowercase)?' },\n        { id: 3033, name: 'What is the difference between Pure and Impure pipes. How do they affect performance?' },\n        { id: 3034, name: 'How do you use multiple pipes simultaneously?' },\n        { id: 3035, name: 'How do you pass parameters to a Pipe to change behavior or format data?' },\n        {\n          id: 3036,\n          name: 'What are the advantages of using Async pipes. How do you apply them with Observable or Promise?',\n        },\n        { id: 3037, name: 'How does the process of registering a custom pipe in a module occur?' },\n      ],\n    },\n    {\n      id: 3040,\n      name: 'Routing',\n      questions: [\n        { id: 3041, name: 'What is Routing in Angular, and what is it used for?' },\n        { id: 3042, name: 'How do you configure a basic routing system using RouterModule and <router-outlet>?' },\n        { id: 3043, name: 'How do you use route parameters and queryParams to pass and retrieve data in routes?' },\n        { id: 3044, name: 'Can you provide an example of using child routes?' },\n        { id: 3045, name: 'What are the preloading strategies, and how do you use them?' },\n        { id: 3046, name: 'How do you use Route Guards (e.g., CanActivate and CanDeactivate) to protect routes?' },\n        {\n          id: 3047,\n          name: 'What is ActivatedRoute, and how do you apply it to get information about the current route?',\n        },\n      ],\n    },\n    {\n      id: 3050,\n      name: 'RxJS',\n      questions: [\n        { id: 3051, name: 'Define the concept of RxJS and its usage in Angular' },\n        { id: 3052, name: 'What are Observable, Observer, and Subscriptions?' },\n        { id: 3053, name: 'What is the difference between Observable and Promise?' },\n        {\n          id: 3054,\n          name: 'Can you provide examples of basic RxJS operators in Angular (e.g., map, filter, catchError, switchMap)?',\n        },\n        { id: 3055, name: 'What are Subject and BehaviorSubject, and how are they used in Angular?' },\n        { id: 3056, name: 'How would you explain the concepts of Hot and Cold Observables?' },\n        { id: 3057, name: 'How do you properly unsubscribe from an Observable?' },\n      ],\n    },\n    {\n      id: 3060,\n      name: 'Dependency Injection',\n      questions: [\n        { id: 3061, name: 'What is Dependency Injection, and what are its objectives in Angular?' },\n        { id: 3062, name: 'How do you create a service and use it in components for dependency injection?' },\n        {\n          id: 3063,\n          name: 'What is the difference between \"providedIn: root\", \"providedIn: any\", and registering a provider in the \"providers\" section of NgModule?',\n        },\n        { id: 3064, name: 'What are useClass, useValue, and useFactory? How are they used when creating providers?' },\n        { id: 3065, name: 'Explain the concept of Injector and provider hierarchy.' },\n        { id: 3066, name: 'What is a DI token, and how do you use it for dependency injection?' },\n        {\n          id: 3067,\n          name: 'How do you use @Optional, @Self, and @SkipSelf decorators to control dependency injection and their handling?',\n        },\n        {\n          id: 3068,\n          name: 'How do you inject dependencies based on conditions or by different provided implementations?',\n        },\n      ],\n    },\n    {\n      id: 3070,\n      name: 'Forms',\n      questions: [\n        { id: 3071, name: 'What is the difference between Template-driven Forms and Reactive Forms?' },\n        { id: 3072, name: 'What are FormControl, FormGroup, and FormArray in the context of Reactive Forms?' },\n        {\n          id: 3073,\n          name: 'What are the differences in working with validation for Template-driven Forms and Reactive Forms?',\n        },\n        { id: 3074, name: 'How do you implement custom validators for forms?' },\n        { id: 3075, name: 'How can you retrieve and process data from forms after submission?' },\n        { id: 3076, name: 'What is two-way data binding in the context of Template-driven Forms?' },\n        { id: 3077, name: 'How do you track the change state of forms or form controls (e.g., touched, dirty)?' },\n      ],\n    },\n    {\n      id: 3080,\n      name: 'Lazy Loading',\n      questions: [\n        { id: 3081, name: 'What is Lazy loading, and what is its purpose in Angular applications?' },\n        { id: 3082, name: 'How do you configure lazy loading for a specific module?' },\n        { id: 3083, name: 'What changes to the routing system are necessary to support lazy loading?' },\n        { id: 3084, name: 'What are the advantages of using lazy loading in your application?' },\n      ],\n    },\n    {\n      id: 3090,\n      name: 'Modules',\n      questions: [\n        { id: 3091, name: 'What is a Module in Angular, and what role does it play in an application?' },\n        { id: 3092, name: 'Can you explain the structure of a module and its metadata?' },\n        {\n          id: 3093,\n          name: 'How can you separate functionality into different modules and connect them to the main application module?',\n        },\n      ],\n    },\n    {\n      id: 3100,\n      name: 'HTTP',\n      questions: [\n        { id: 3101, name: 'What is HttpClientModule, and why is it important in Angular applications?' },\n        { id: 3102, name: \"How can you make HTTP requests using Angular's HttpClient?\" },\n        {\n          id: 3103,\n          name: 'Can you explain the difference between Observables and Promises in handling HTTP responses?',\n        },\n        { id: 3104, name: 'How can you handle errors during HTTP requests in Angular?' },\n        {\n          id: 3105,\n          name: 'What are some techniques to optimize HTTP requests and handle caching considerations for Angular applications?',\n        },\n        { id: 3106, name: 'What is the purpose of HttpInterceptor in Angular, and how does it work?' },\n      ],\n    },\n    {\n      id: 3110,\n      name: 'Tests (Testing)',\n      questions: [\n        {\n          id: 3111,\n          name: 'What types of Testing does Angular support (e.g., unit tests, integration tests, e2e tests)?',\n        },\n        {\n          id: 3112,\n          name: 'What are the main tools and libraries used by Angular for testing (Jasmine, Karma, and Protractor)?',\n        },\n        { id: 3113, name: 'What is TestBed, and how is it used to set up a testing environment?' },\n        { id: 3114, name: 'How do you test Angular components using ComponentFixture and DebugElement?' },\n        { id: 3115, name: 'How do you test directives and pipes in Angular?' },\n        { id: 3116, name: 'How do you mock and stub dependencies in tests for services?' },\n        {\n          id: 3117,\n          name: 'What are async, fakeAsync, and tick, and how are they used when testing asynchronous code?',\n        },\n      ],\n    },\n    {\n      id: 3120,\n      name: 'Coding task',\n      questions: [{ id: 3121, name: 'Small Angular app: component, service, pipe, directives...' }],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/corejs1.ts",
    "content": "import { InputType, InterviewTemplate } from './types';\n\nexport const corejs1Template: InterviewTemplate = {\n  name: 'CoreJS',\n  examplesUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/tasks/interview-basic-coreJS.md',\n  descriptionHtml: `\n    During the interview, students can score up to 100 points. Each topic has its value in points.<br/>\n    Please see the details below.\n    <ul>\n    <li>Advanced Expressions - up to 10 points</li>\n    <li>Function - up to 5 points</li>\n    <li>Functional Scope - up to 10 points</li>\n    <li>Functions Parameters / Arguments - up to 5 points</li>\n    <li>Closures Advanced - up to 15 points</li>\n    <li>Advanced Functions - up to 15 points</li>\n    <li>ECMAScript Intermediate - up to 10 points</li>\n    <li>Objects Built-in Methods - up to 10 points</li>\n    <li>Arrays Built-in Methods - up to 5 points</li>\n    <li>Arrays Iterating, Sorting, Filtering - up to 5 points</li>\n    <li>Events Basics - up to 5 points</li>\n    <li>Events Propagation / Preventing - up to 5 points</li>\n    <li>Timers - up to 5 points</li>\n    <li>Web Storage API & Cookies - up to 5 points</li>\n    <li>Date & Time - up to 5 points</li>\n    <li>Software Development Best Practices - up to 5 points</li>\n    </ul>\n    <br/>\n    `,\n  categories: [\n    {\n      id: 1010,\n      name: 'Advanced Expressions',\n      description: `10 points`,\n      questions: [\n        { id: 1011, name: 'Object.is (optional)' },\n        { id: 1012, name: 'Differences between let, var, and const' },\n        { id: 1013, name: 'Exploring the Temporal Dead Zone', type: InputType.Input },\n        { id: 1015, name: 'Hoisting', type: InputType.Input },\n      ],\n    },\n    {\n      id: 1020,\n      name: 'Function',\n      description: `5 points`,\n      questions: [\n        {\n          id: 1021,\n          name: 'Differences and uses of arrow functions, function expressions, and function declarations',\n          type: InputType.Input,\n        },\n      ],\n    },\n    {\n      id: 1030,\n      name: 'Functional Scope',\n      description: '10 points',\n      questions: [\n        { id: 1031, name: 'Global scope vs. Functional scope' },\n        { id: 1032, name: 'Variable visibility areas' },\n        { id: 1033, name: 'Working with nested scopes' },\n      ],\n    },\n    {\n      id: 1040,\n      name: 'Functions Parameters / Arguments',\n      description: '5 points',\n      questions: [\n        {\n          id: 1041,\n          name: 'Defining function parameters',\n        },\n        {\n          id: 1042,\n          name: 'Differences in parameters passing by value and by reference',\n        },\n        {\n          id: 1043,\n          name: 'Handling a dynamic amount of function parameters',\n        },\n      ],\n    },\n    {\n      id: 1050,\n      name: 'Closures Advanced',\n      description: '15 points',\n      questions: [\n        {\n          id: 1051,\n          name: 'Understanding context and lexical environments',\n          type: InputType.Input,\n        },\n        {\n          id: 1052,\n          name: 'Differences between scope and context',\n          type: InputType.Input,\n        },\n        {\n          id: 1053,\n          name: 'The mechanism of lexical environment traversal',\n        },\n        {\n          id: 1054,\n          name: 'Connection between function and its lexical environment',\n          type: InputType.Input,\n        },\n      ],\n    },\n    {\n      id: 1060,\n      name: 'Advanced Functions',\n      description: '15 points',\n      questions: [\n        {\n          id: 1061,\n          name: '`this` in functions',\n          type: InputType.Input,\n        },\n        {\n          id: 1062,\n          name: 'Reference Type & losing `this`',\n        },\n        {\n          id: 1063,\n          name: 'Understand difference between function and method',\n        },\n        {\n          id: 1064,\n          name: 'Understand how `this` works, realize `this` possible issues',\n          type: InputType.Input,\n        },\n        {\n          id: 1065,\n          name: 'Manage `this`',\n          type: InputType.Input,\n        },\n        {\n          id: 1067,\n          name: 'Be able to use `call` and `apply` Function built-in methods',\n        },\n        {\n          id: 1068,\n          name: 'Know how to bind `this` scope to function',\n        },\n        {\n          id: 1069,\n          name: 'Binding, binding one function twice',\n        },\n      ],\n    },\n    {\n      id: 1070,\n      name: 'ECMAScript Intermediate',\n      description: '10 points',\n      questions: [\n        {\n          id: 1071,\n          name: 'Function default parameters',\n        },\n        {\n          id: 1072,\n          name: 'Using spread operator for function arguments',\n        },\n        {\n          id: 1073,\n          name: 'Comparing `arguments` and `rest parameters`',\n        },\n        {\n          id: 1074,\n          name: 'Array concatenation with spread operator',\n        },\n        {\n          id: 1075,\n          name: 'Destructuring assignments for variables and function arguments',\n        },\n      ],\n    },\n    {\n      id: 1080,\n      name: 'Objects Built-in Methods',\n      description: '10 points',\n      questions: [\n        {\n          id: 1081,\n          name: 'Utilizing `Object.keys` and `Object.values`',\n        },\n        {\n          id: 1082,\n          name: 'Working with static Object methods',\n        },\n        {\n          id: 1083,\n          name: 'Property flags and descriptors',\n        },\n        {\n          id: 1084,\n          name: 'Creating iterable objects and using `Symbol.iterator` (optional)',\n        },\n      ],\n    },\n    {\n      id: 1100,\n      name: 'Arrays Built-in Methods',\n      description: '5 points',\n      questions: [\n        {\n          id: 1101,\n          name: 'Copying and modifying arrays',\n        },\n        {\n          id: 1102,\n          name: 'Flattening nested arrays',\n        },\n      ],\n    },\n    {\n      id: 1110,\n      name: 'Arrays Iterating, Sorting, Filtering',\n      description: '5 points',\n      questions: [\n        {\n          id: 1111,\n          name: 'Sorting and custom sorting arrays',\n        },\n        {\n          id: 1112,\n          name: 'Filtering array elements',\n        },\n      ],\n    },\n    {\n      id: 1130,\n      name: 'Events Basics',\n      description: '5 points',\n      questions: [\n        {\n          id: 1131,\n          name: 'Types of DOM Events',\n        },\n        {\n          id: 1132,\n          name: 'Working with Mouse and Keyboard Events',\n        },\n        {\n          id: 1133,\n          name: 'Handling Form and Input Events',\n        },\n        {\n          id: 1134,\n          name: 'Event Listeners',\n        },\n        {\n          id: 1135,\n          name: 'Event Phases and their differences',\n        },\n        {\n          id: 1136,\n          name: 'Custom events (optional)',\n        },\n      ],\n    },\n    {\n      id: 1140,\n      name: 'Events Propagation / Preventing',\n      description: '5 points',\n      questions: [\n        {\n          id: 1141,\n          name: 'Event propagation cycle',\n        },\n        {\n          id: 1142,\n          name: 'Stopping event propagation',\n        },\n        {\n          id: 1143,\n          name: 'Preventing default browser behavior',\n        },\n        {\n          id: 1144,\n          name: 'Event delegation and its pros/cons',\n        },\n      ],\n    },\n    {\n      id: 1150,\n      name: 'Timers',\n      description: '5 points',\n      questions: [\n        {\n          id: 1151,\n          name: 'Usage of `setTimeout` / `setInterval`',\n        },\n        {\n          id: 1152,\n          name: 'Clearing timers with `clearTimeout` / `clearInterval`',\n        },\n      ],\n    },\n    {\n      id: 1160,\n      name: 'Web Storage API & Cookies',\n      description: '5 points',\n      questions: [\n        {\n          id: 1161,\n          name: 'Differences between LocalStorage, SessionStorage, and Cookies',\n        },\n      ],\n    },\n    {\n      id: 1170,\n      name: 'Date & Time',\n      description: '5 points',\n      questions: [\n        {\n          id: 1171,\n          name: 'Working with the Date object',\n        },\n        {\n          id: 1172,\n          name: 'Timezones and Internationalization in JavaScript (Intl)',\n        },\n      ],\n    },\n    {\n      id: 1180,\n      name: 'Software Development Best Practices',\n      description: '5 points',\n      questions: [\n        {\n          id: 1181,\n          name: 'Understanding and applying KISS, DRY, and YAGNI principles',\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/corejs2.ts",
    "content": "import { InputType, InterviewTemplate } from './types';\n\nexport const corejs2Template: InterviewTemplate = {\n  name: 'CoreJS2',\n  examplesUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/tasks/interview-corejs.md',\n  descriptionHtml: `\n  During the interview, students can score up to 100 points. Each topic has its value in points.<br/>\n  Please see the details below.\n  <ul>\n  <li>Functional Patterns - up to 10 points</li>\n  <li>Prototypal Inheritance Basics - up to 15 points</li>\n  <li>ECMAScript Classes - up to 10 points</li>\n  <li>ECMAScript Data Types & Expressions - up to 10 points</li>\n  <li>ECMAScript Advanced - up to 10 points</li>\n  <li>Network requests - up to 10 points</li>\n  <li>Page Lifecycle - up to 10 points</li>\n  <li>Typescript - up to 10 points</li>\n  <li>Testing (optional) - up to 10 points</li>\n  </ul>\n  <br/>\n  `,\n  categories: [\n    {\n      id: 2060,\n      name: 'Functional Patterns',\n      description: '10 points',\n      questions: [\n        { id: 2061, name: 'Immediately invoked functional expression (IIFE)' },\n        { id: 2062, name: 'Callback (Function as argument)' },\n        { id: 2063, name: 'Binding, binding one function twice' },\n        { id: 2064, name: 'Know how to bind this scope to function' },\n      ],\n    },\n    {\n      id: 2080,\n      name: 'Prototypal Inheritance Basics',\n      description: '15 points',\n      questions: [\n        { id: 2081, name: '__proto__ property' },\n        { id: 2082, name: 'Able to use [Object.create] and define __proto__ explicitly' },\n        { id: 2083, name: 'prototype property' },\n        { id: 2084, name: 'Understand dependency between function constructor prototype and instance __proto__' },\n      ],\n    },\n    {\n      id: 2090,\n      name: 'ECMAScript Classes',\n      description: '10 points',\n      questions: [\n        { id: 2091, name: 'Class declaration' },\n        { id: 2092, name: 'What does super() do and where we have to use it?' },\n      ],\n    },\n    {\n      id: 2100,\n      name: 'ECMAScript Data Types & Expressions',\n      description: '10 points',\n      questions: [\n        { id: 2101, name: 'Set/Map data types' },\n        { id: 2102, name: 'WeakSet/WeakMap data types' },\n      ],\n    },\n    {\n      id: 2110,\n      name: 'ECMAScript Advanced',\n      description: '10 points',\n      questions: [\n        { id: 2115, name: 'event loop' },\n        { id: 2111, name: 'Promises' },\n        { id: 2112, name: 'Promise Chaining' },\n        { id: 2113, name: 'Promise static methods' },\n        { id: 2114, name: 'Be able to handle errors in promises' },\n        { id: 2116, name: 'async/await' },\n      ],\n    },\n    {\n      id: 2120,\n      name: 'Network requests',\n      description: '5 points',\n      questions: [\n        { id: 2121, name: 'Fetch' },\n        { id: 2122, name: 'XMLHTTPRequest (concept) (optional)' },\n      ],\n    },\n    {\n      id: 2130,\n      name: 'Page Lifecycle',\n      description: '10 points',\n      questions: [\n        { id: 2131, name: 'Parsing' },\n        { id: 2132, name: 'Reflow' },\n        { id: 2133, name: 'Repaint' },\n      ],\n    },\n    {\n      id: 2150,\n      name: 'Typescript',\n      description: '10 points',\n      questions: [\n        { id: 2151, name: 'basic types' },\n        { id: 2152, name: 'enums' },\n        { id: 2153, name: 'type / interface, differences between them' },\n        { id: 2154, name: 'function types' },\n        { id: 2155, name: 'generic types (concept)' },\n        { id: 2156, name: 'utitily types (optional)' },\n        { id: 2157, name: 'typeguards (optional)' },\n      ],\n    },\n    {\n      id: 2160,\n      name: 'Testing (optional)',\n      description: '10 points',\n      questions: [\n        { id: 2161, name: 'Testing Types' },\n        { id: 2162, name: 'Test Pyramid' },\n      ],\n    },\n    {\n      id: 2170,\n      name: 'Software Development Methodologies',\n      description: '10 points',\n      questions: [\n        { id: 2171, name: 'Agile' },\n        { id: 2172, name: 'Scrum / Kanban / Waterfall' },\n      ],\n    },\n    {\n      id: 2180,\n      name: 'Coding Task',\n      description: '20 points',\n      questions: [{ id: 2181, name: 'Coding Task', type: InputType.Input }],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/index.ts",
    "content": "import { angularTemplate } from './angular';\nimport { corejs1Template } from './corejs1';\nimport { corejs2Template } from './corejs2';\nimport { reactTemplate } from './react';\nimport { shortTrackScreeningTemplate } from './shortTrackScreening';\nimport { shortTrackJavaScriptTemplate } from './shortTrackJavaScript';\nimport { shortTrackTypeScriptTemplate } from './shortTrackTypeScript';\nimport { shortTrackPerformanceTemplate } from '@client/data/interviews/shortTrackPerformance';\nimport { validateInterviewTemplate } from './templateValidator';\n\nexport * from './types';\n\nexport const templates = {\n  corejs1: corejs1Template,\n  corejs2: corejs2Template,\n  react: reactTemplate,\n  angular: angularTemplate,\n  shortTrackScreening: shortTrackScreeningTemplate,\n  shortTrackJavaScript: shortTrackJavaScriptTemplate,\n  shortTrackTypeScript: shortTrackTypeScriptTemplate,\n  shortTrackPerformance: shortTrackPerformanceTemplate,\n};\n\nObject.values(templates).forEach(template => {\n  validateInterviewTemplate(template);\n});\n"
  },
  {
    "path": "client/src/data/interviews/react.ts",
    "content": "import { InterviewTemplate } from './types';\n\nexport const reactTemplate: InterviewTemplate = {\n  name: 'React interview',\n  examplesUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/react/questions.md',\n  descriptionHtml: `\n    During the interview, students can score up to 10 points. Each block of questions is rated 0.5 points.<br/>\n    `,\n  categories: [\n    {\n      id: 1000,\n      name: 'Core Concepts & JSX',\n      questions: [\n        { id: 1001, name: 'JSX syntax and how it compiles' },\n        { id: 1002, name: 'React.createElement and virtual DOM basics' },\n        { id: 1003, name: 'Differences between className and class' },\n        { id: 1004, name: 'Embedding expressions' },\n        { id: 1005, name: 'Conditional rendering (&&, ternary)' },\n      ],\n    },\n    {\n      id: 1010,\n      name: 'Components',\n      questions: [\n        { id: 1011, name: 'Functional vs class components' },\n        { id: 1012, name: 'Props and defaultProps' },\n        { id: 1013, name: \"Prop 'children'\" },\n        { id: 1014, name: 'Component composition' },\n        { id: 1015, name: 'Component state' },\n        { id: 1016, name: 'Manipulating DOM directly using Refs' },\n      ],\n    },\n    {\n      id: 1020,\n      name: 'Hooks',\n      questions: [\n        { id: 1021, name: 'useState' },\n        { id: 1022, name: 'useRef' },\n        { id: 1023, name: 'useMemo' },\n        { id: 1024, name: 'useCallback' },\n        { id: 1025, name: 'useEffect basics' },\n        { id: 1026, name: 'Rule of hooks' },\n      ],\n    },\n    {\n      id: 1030,\n      name: 'Lifecycle Methods & Effects',\n      questions: [\n        { id: 1031, name: 'Lifecycle in class vs functional components' },\n        { id: 1032, name: 'Cleanup functions in useEffect' },\n        { id: 1033, name: 'Dependency array pitfalls' },\n      ],\n    },\n    {\n      id: 1040,\n      name: 'Event Handling and Forms',\n      questions: [\n        { id: 1041, name: 'Event binding in class components [legacy]' },\n        { id: 1042, name: 'Controlled vs uncontrolled components' },\n        { id: 1043, name: 'Preventing default and handling submission' },\n        { id: 1044, name: 'useActionState' },\n      ],\n    },\n    {\n      id: 1050,\n      name: 'Lifting State, Data Flow',\n      questions: [\n        { id: 1051, name: 'Lifting state up, Props drilling' },\n        { id: 1052, name: 'Passing values' },\n      ],\n    },\n    {\n      id: 1060,\n      name: 'Lists, Keys, and Reconciliation',\n      questions: [\n        { id: 1061, name: 'Rendering lists with .map, the role and rules of key, avoiding unstable keys' },\n        { id: 1062, name: 'Reconciliation algorithm (VDOM diffing)' },\n      ],\n    },\n    {\n      id: 1070,\n      name: 'Styling in React',\n      questions: [\n        { id: 1071, name: 'Inline styles, CSS modules' },\n        { id: 1072, name: 'Styled Components' },\n      ],\n    },\n    {\n      id: 1080,\n      name: 'Testing in React',\n      questions: [\n        { id: 1081, name: 'Testing component logic with RTL' },\n        { id: 1082, name: 'Queries in RTL' },\n        { id: 1083, name: 'Firing events' },\n        { id: 1084, name: 'Testing Frameworks' },\n      ],\n    },\n    {\n      id: 1090,\n      name: 'Advanced Hooks & Patterns',\n      questions: [\n        { id: 1091, name: 'useReducer for complex state' },\n        { id: 1092, name: 'Lazy initialization in hooks' },\n        { id: 1093, name: 'Writing custom hooks' },\n        { id: 1094, name: 'Dependency memoization (useCallback, useMemo)' },\n        { id: 1095, name: 'useLayoutEffect' },\n        { id: 1096, name: 'use' },\n      ],\n    },\n    {\n      id: 1100,\n      name: 'Advanced Rendering Patterns',\n      questions: [{ id: 1101, name: 'React Portal' }],\n    },\n    {\n      id: 1110,\n      name: 'Context API',\n      questions: [\n        { id: 1111, name: 'Creating and consuming contexts' },\n        { id: 1112, name: 'Providing default values' },\n        { id: 1113, name: 'Avoiding unnecessary re-renders' },\n        { id: 1114, name: 'Usecases' },\n      ],\n    },\n    {\n      id: 1120,\n      name: 'Performance Optimization',\n      questions: [\n        { id: 1121, name: 'State collocation' },\n        { id: 1122, name: 'Memoization (React.memo, useMemo, useCallback)' },\n        { id: 1123, name: 'React.lazy' },\n        { id: 1124, name: 'Avoiding prop drilling' },\n        { id: 1125, name: 'Profiling with React DevTools' },\n        { id: 1126, name: 'Avoiding unnecessary re-renders in large trees' },\n        { id: 1127, name: 'Deoptimization using flushSync' },\n      ],\n    },\n    {\n      id: 1130,\n      name: 'React Router (v7+)',\n      questions: [\n        { id: 1131, name: 'Declarative mode. Routing' },\n        { id: 1132, name: 'Data mode. Routing using createBrowserRouter' },\n        { id: 1133, name: 'Nested routes' },\n        { id: 1134, name: 'Dynamic params' },\n        { id: 1135, name: 'Redirects and navigation' },\n        { id: 1136, name: 'Programmatic navigation (useNavigate)' },\n        { id: 1137, name: 'Outlet' },\n      ],\n    },\n    {\n      id: 1140,\n      name: 'Error Handling',\n      questions: [\n        { id: 1141, name: 'Error boundaries (class component pattern)' },\n        { id: 1142, name: 'Async errors in useEffect' },\n        { id: 1143, name: 'Fallback UI handling with Suspense and boundaries' },\n      ],\n    },\n    {\n      id: 1150,\n      name: 'State Management Libraries',\n      questions: [\n        { id: 1151, name: 'Redux (store, reducers, actions)' },\n        { id: 1152, name: 'Redux Middleware' },\n        { id: 1153, name: 'Redux Toolkit' },\n        { id: 1154, name: 'Zustand / Jotai / MobX' },\n        { id: 1155, name: 'React Query (server-state)' },\n        { id: 1156, name: 'Redux Toolkt Query' },\n      ],\n    },\n    {\n      id: 1160,\n      name: 'SSR & Meta Frameworks',\n      questions: [\n        { id: 1161, name: 'Server-side rendering and Server-side generation' },\n        { id: 1162, name: 'Next.JS. Pages router, getStaticProps, getStaticPaths, getServerSideProps' },\n        { id: 1163, name: 'Next.JS. App router. Route Handlers and Middleware in App router' },\n        { id: 1164, name: 'Next.JS. Fetching data' },\n        { id: 1165, name: 'Next.JS. Client/server separation in React Server Components' },\n        { id: 1166, name: 'React Router Framework. Routing' },\n        { id: 1167, name: 'React Router Framework. Client, Server and Static Data Loading' },\n        { id: 1168, name: 'TanStack Start' },\n        { id: 1169, name: 'Waku' },\n      ],\n    },\n    {\n      id: 1170,\n      name: 'Concurrent Features & Suspense',\n      questions: [\n        { id: 1171, name: 'Suspense for lazy-loading' },\n        { id: 1172, name: 'Concurrent rendering in React 18' },\n        { id: 1173, name: 'useTransition, startTransition' },\n        { id: 1174, name: 'useDeferredValue' },\n      ],\n    },\n    {\n      id: 1180,\n      name: 'Build Process / CI/CD / Tooling',\n      questions: [\n        { id: 1181, name: 'CRA vs Vite vs custom Webpack' },\n        { id: 1182, name: 'Linting, formatting, pre-commit hooks' },\n        { id: 1183, name: 'Dockerizing React apps, CI/CD pipelines' },\n      ],\n    },\n    {\n      id: 1190,\n      name: 'Coding task',\n      questions: [{ id: 1191, name: 'Small react app: form, button, results list' }],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/shortTrackJavaScript.ts",
    "content": "import { InputType, InterviewTemplate } from './types';\n\nexport const shortTrackJavaScriptTemplate: InterviewTemplate = {\n  name: 'EPAM ShortTrack Core JS mentors interview #1',\n  examplesUrl: 'https://rolling-scopes-school.github.io/epam-short-track/core-js-ts/interviews/mentors-checkpoint-2',\n  descriptionHtml: `\n    During the interview, students can score up to 10 points. Each question is rated 0.5 points.<br/>\n    `,\n  categories: [\n    {\n      id: 1100,\n      name: 'Core JS mentors interview #1',\n      description: `10 points`,\n      questions: [\n        { id: 1101, name: 'What is the difference between Git and GitHub?' },\n        { id: 1102, name: 'Describe the Git flow branching model.' },\n        {\n          id: 1103,\n          name: 'What is the difference between git pull and git fetch, git reset and git revert, git merge and git rebase?',\n        },\n        {\n          id: 1104,\n          name: 'What are the key differences between primitive data types and Object data types in JavaScript?',\n        },\n        {\n          id: 1105,\n          name: \"How does the 'typeof' operator work in JavaScript, and when would you use it?\",\n          type: InputType.Input,\n        },\n        {\n          id: 1106,\n          name: 'What are the differences between var, let, and const in JavaScript? Can you explain what hoisting and TDZ are?',\n        },\n        { id: 1107, name: 'How do you use conditional statements in JavaScript to make decisions in your code?' },\n        {\n          id: 1108,\n          name: 'How does destructuring work in JavaScript, and give examples of its use with arrays and objects?',\n          type: InputType.Input,\n        },\n        {\n          id: 1109,\n          name: 'What are some ways to create arrays in JavaScript? What array methods that modify the original array in JavaScript?',\n        },\n        {\n          id: 1110,\n          name: 'Explain how you would use array operations such as sort, filter, find, map, and reduce in JavaScript.',\n        },\n        { id: 1111, name: 'What are static methods in JavaScript? Provide an example.' },\n        {\n          id: 1112,\n          name: 'Can you explain automatic data type conversion in JavaScript?',\n          type: InputType.Input,\n        },\n        { id: 1113, name: \"What is the difference between 'Object.create()' and new keyword for creating objects?\" },\n        {\n          id: 1114,\n          name: 'What are property descriptors in JavaScript objects and how can you manipulate them? How do getter and setter methods work in JavaScript objects?',\n        },\n        {\n          id: 1115,\n          name: 'How can you clone an object in JavaScript? How can you prevent modifications to an object in JavaScript?',\n        },\n        {\n          id: 1116,\n          name: 'Explain the difference between function declarations, function expressions, and arrow functions.',\n        },\n        {\n          id: 1117,\n          name: \"How do default parameters work in JavaScript functions and why are they useful? What is a rest operator and how can it be used in functions? Rest operator vs 'arguments'.\",\n        },\n        {\n          id: 1118,\n          name: \"How does the 'this' keyword work in different types of functions? What are the 'call', 'apply', and 'bind' methods? Provide examples of how and when you would use each.\",\n        },\n        { id: 1119, name: \"How does 'use strict' mode affect 'this' behavior in functions?\" },\n        {\n          id: 1120,\n          name: 'Explain the concept of closure in JavaScript. Explain functional patterns in JavaScript: IIFE (Immediately Invoked Function Expressions), callback, memoization, currying, chaining, higher-order function, recursion.',\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/shortTrackPerformance.ts",
    "content": "import { InterviewTemplate } from './types';\n\nexport const shortTrackPerformanceTemplate: InterviewTemplate = {\n  name: 'EPAM ShortTrack TS mentors interview #2',\n  examplesUrl: 'https://rolling-scopes-school.github.io/epam-short-track/core-js-ts/interviews/mentors-checkpoint-2',\n  descriptionHtml: `\n    During the interview, students can score up to 10 points. Each question is rated 0.5 points.<br/>\n    `,\n  categories: [\n    {\n      id: 1100,\n      name: 'Node.js Basics',\n      description: `2.5 points`,\n      questions: [\n        {\n          id: 1101,\n          name: 'What is Node.js, and how does it differ from browser-based JavaScript?',\n        },\n        {\n          id: 1102,\n          name: 'What is the purpose of the package.json file, and how does it differ from package-lock.json?',\n        },\n        {\n          id: 1103,\n          name: 'How do CommonJS (require) and ES Modules (import) differ in Node.js?',\n        },\n        {\n          id: 1104,\n          name: 'Briefly explain what streams are in Node.js and describe their types.',\n        },\n        {\n          id: 1105,\n          name: 'How do environment variables work in Node.js, and how are they used in applications?',\n        },\n      ],\n    },\n    {\n      id: 1200,\n      name: 'Networking',\n      description: '2 points',\n      questions: [\n        {\n          id: 1201,\n          name: 'What is HTTP, and how does the HTTP request-response cycle work?',\n        },\n        {\n          id: 1202,\n          name: 'Compare and contrast HTTP/1, HTTP/2, and HTTP/3.',\n        },\n        {\n          id: 1203,\n          name: 'What is REST, and what are its key principles?',\n        },\n        {\n          id: 1204,\n          name: 'What are HTTP status codes? Provide examples of success, client error, and server error status codes.',\n        },\n      ],\n    },\n    {\n      id: 1300,\n      name: 'Security',\n      description: '2 points',\n      questions: [\n        {\n          id: 1301,\n          name: 'What is XSS (Cross-Site Scripting), and how can it be prevented?',\n        },\n        {\n          id: 1302,\n          name: 'What is CSRF (Cross-Site Request Forgery), and how does it differ from XSS?',\n        },\n        {\n          id: 1303,\n          name: 'Explain how you would secure sensitive environment variables in a Node.js application.',\n        },\n        {\n          id: 1304,\n          name: 'What is CORS, and how does it relate to the Same-Origin Policy?',\n        },\n      ],\n    },\n    {\n      id: 1400,\n      name: 'Testing',\n      description: '2 points',\n      questions: [\n        {\n          id: 1401,\n          name: 'What is the Arrange-Act-Assert (AAA) pattern, and why is it important in writing unit tests?',\n        },\n        {\n          id: 1402,\n          name: 'What is mocking, and how does it help isolate dependencies in tests?',\n        },\n        {\n          id: 1403,\n          name: 'Explain the principles of the FIRST (Fast, Independent, Repeatable, Self-validating, Timely) acronym in testing.',\n        },\n        {\n          id: 1404,\n          name: 'What are flaky tests, and how can they be addressed to improve CI/CD pipelines?',\n        },\n      ],\n    },\n    {\n      id: 1500,\n      name: 'Critical Rendering Path (CRP)',\n      description: '1 point',\n      questions: [\n        {\n          id: 1501,\n          name: 'What are the main stages of the Critical Rendering Path (CRP)?',\n        },\n        {\n          id: 1502,\n          name: 'For moving an element during animation, is it better to use transition: translate or modify properties like left, top, etc.? Why?',\n        },\n      ],\n    },\n    {\n      id: 1600,\n      name: 'Debugging Tools',\n      description: '0.5 point',\n      questions: [\n        {\n          id: 1601,\n          name: 'What debugging tools do you use for Node.js or web development, such as Chrome DevTools? Share an example of how you’ve identified or resolved an issue using these tools.',\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/shortTrackScreening.ts",
    "content": "import { InputType, InterviewTemplate } from './types';\n\nexport const shortTrackScreeningTemplate: InterviewTemplate = {\n  name: 'EPAM ShortTrack Technical Screening',\n  examplesUrl: 'https://github.com/rolling-scopes-school/epam-short-track/tree/main/screening',\n  descriptionHtml: `\n    During the interview, students can score up to 100 points. Each topic has its value in points.<br/>\n    Please see the details below.\n    <ul>\n    <li>Basics of JS - up to 10 points</li>\n    <li>Arrays - up to 10 points</li>\n    <li>Objects - up to 10 points</li>\n    <li>Functions - up to 10 points</li>\n    <li>Classes - up to 10 points</li>\n    <li>Async - up to 10 points</li>\n    <li>Basics of HTML/CSS - up to 10 points</li>\n    <li>Client Side - up to 10 points</li>\n    <li>DOM manipulation - up to 10 points</li>\n    <li>Event Handling - up to 10 points</li>\n    </ul>\n    <br/>\n    `,\n  categories: [\n    {\n      id: 1010,\n      name: 'Basics of JS',\n      description: `10 points`,\n      questions: [\n        { id: 1011, name: 'Data types: Primitives vs Objects' },\n        { id: 1012, name: 'Variables: var vs let vs const (Hoisting, Temporal Dead Zone)' },\n        {\n          id: 1013,\n          name: 'Ternary, Nullish Coalescing, Optional Chaining, and Logical Operators (few practical tasks)',\n          type: InputType.Input,\n        },\n        { id: 1014, name: 'Loops - for, while, do while, for of, for in' },\n        { id: 1015, name: 'Type conversions, == / === (few practical tasks)', type: InputType.Input },\n      ],\n    },\n    {\n      id: 1020,\n      name: 'Arrays',\n      description: `10 points`,\n      questions: [\n        {\n          id: 1021,\n          name: 'Most popular methods: map vs forEach, filter vs find, sort, reduce, pop/push, shift/unshift, toSorted (node 20)',\n          type: InputType.Input,\n        },\n        { id: 1022, name: 'Modification: mutating vs non mutating methods' },\n        { id: 1023, name: 'Array vs Set' },\n      ],\n    },\n    {\n      id: 1030,\n      name: 'Objects',\n      description: '10 points',\n      questions: [\n        { id: 1031, name: 'How to get keys/values (Object.keys, values, entries)' },\n        {\n          id: 1032,\n          name: 'How to copy object ({...obj}, Object.assign, JSON.parse/stringify, using loop, Object.create): shallow copy vs deep copy',\n          type: InputType.Input,\n        },\n        {\n          id: 1033,\n          name: 'Destructuring (few practical tasks)',\n          type: InputType.Input,\n        },\n        { id: 1034, name: 'Getter/setter (optional)' },\n        { id: 1035, name: 'Object vs Map' },\n      ],\n    },\n    {\n      id: 1040,\n      name: 'Functions',\n      description: '10 points',\n      questions: [\n        { id: 1041, name: 'Declaration vs expression vs arrow functions' },\n        { id: 1042, name: 'Default params' },\n        { id: 1043, name: 'Rest operator' },\n        { id: 1044, name: 'this' },\n        { id: 1045, name: 'Call vs apply vs bind' },\n      ],\n    },\n    {\n      id: 1050,\n      name: 'Classes',\n      description: '10 points',\n      questions: [\n        { id: 1051, name: 'Constructor' },\n        { id: 1052, name: 'Public vs Private methods' },\n        { id: 1053, name: 'Static methods' },\n        { id: 1054, name: 'Inheritance' },\n      ],\n    },\n    {\n      id: 1060,\n      name: 'Async',\n      description: '10 points',\n      questions: [\n        { id: 1061, name: 'Promise and its methods' },\n        { id: 1062, name: 'Promises vs async/await' },\n        { id: 1063, name: 'Error handling (try / catch / finally)' },\n        { id: 1064, name: 'EventLoop (high lvl!)' },\n      ],\n    },\n    {\n      id: 1070,\n      name: 'Basics of HTML/CSS',\n      description: '10 points',\n      questions: [\n        { id: 1071, name: 'Selector weights' },\n        { id: 1072, name: 'Pseudo-classes and pseudo-elements (optional)' },\n        { id: 1073, name: 'em vs rem, relative and absolute values (optional)' },\n        { id: 1074, name: 'FlexBox vs Grid' },\n      ],\n    },\n    {\n      id: 1080,\n      name: 'Client Side',\n      description: '10 points',\n      questions: [\n        { id: 1081, name: 'Global object window (document, location, history, cookies)' },\n        { id: 1082, name: 'Web Storage (sessionStorage vs localStorage)' },\n      ],\n    },\n    {\n      id: 1090,\n      name: 'DOM manipulation',\n      description: '10 points',\n      questions: [\n        { id: 1091, name: 'Selection (getElementBy vs querySelector)' },\n        { id: 1092, name: 'HTML attributes' },\n        { id: 1093, name: 'Traversing (...child, ...sibling, element vs node) (optional)' },\n      ],\n    },\n    {\n      id: 1100,\n      name: 'Event Handling',\n      description: '10 points',\n      questions: [\n        { id: 1101, name: 'AddEventListener vs on[Event]' },\n        { id: 1102, name: 'PreventDefault vs stopPropagation vs stopImmediatePropagation' },\n        { id: 1103, name: 'Event delegation (optional)' },\n        { id: 1104, name: 'target vs currentTarget' },\n      ],\n    },\n    {\n      id: 1110,\n      name: 'English Level',\n      description: 'Checking by Mentor',\n      questions: [\n        { id: 1111, name: 'less then B1' },\n        { id: 1112, name: 'B1' },\n        { id: 1113, name: 'B1+' },\n        { id: 1114, name: 'B2' },\n        { id: 1115, name: 'B2+' },\n        { id: 1116, name: 'C1' },\n      ],\n    },\n    {\n      id: 1120,\n      name: 'Practical task',\n      description: 'additional 20 points',\n      questions: [\n        {\n          id: 1121,\n          name: 'Task solved',\n          type: InputType.Input,\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/shortTrackTypeScript.ts",
    "content": "import { InputType, InterviewTemplate } from './types';\n\nexport const shortTrackTypeScriptTemplate: InterviewTemplate = {\n  name: 'EPAM ShortTrack TS mentors interview #2',\n  examplesUrl: 'https://rolling-scopes-school.github.io/epam-short-track/core-js-ts/interviews/mentors-checkpoint-2',\n  descriptionHtml: `\n    During the interview, students can score up to 10 points. Each question is rated 0.5 points.<br/>\n    `,\n  categories: [\n    {\n      id: 1100,\n      name: 'TypeScript',\n      description: `5 points`,\n      questions: [\n        {\n          id: 1101,\n          name: 'What is the difference between TypeScript and JavaScript, and why would you choose TypeScript for a project?',\n        },\n        { id: 1102, name: 'Explain what structural typing is in TypeScript and how it differs from nominal typing.' },\n        {\n          id: 1103,\n          name: 'What are the primitive types in TypeScript, and how are they different from JavaScript’s types?',\n        },\n        {\n          id: 1104,\n          name: 'How do Partial&lt;T&gt; and Required&lt;T&gt; utility types differ, and why would you use them in your code?',\n        },\n        {\n          id: 1105,\n          name: 'What is the difference between never and void in TypeScript, and when would you use each?',\n        },\n        {\n          id: 1106,\n          name: 'What are generics in TypeScript, and how can they make functions or classes more reusable? Provide examples.',\n        },\n        {\n          id: 1107,\n          name: 'How does the Pick&lt;T, K&gt; utility type differ from Omit&lt;T, K&gt;, and when would you use each of them?',\n        },\n        {\n          id: 1108,\n          name: 'Describe the difference between any and unknown in TypeScript and discuss scenarios where one should be preferred over the other.',\n        },\n        {\n          id: 1109,\n          name: 'How does the use of interface differ from type in TypeScript, and when would you choose one over the other?',\n        },\n        {\n          id: 1110,\n          name: 'What is the purpose of the readonly modifier in TypeScript, and how does it differ from a const variable?',\n        },\n      ],\n    },\n    {\n      id: 1200,\n      name: 'Object-Oriented Programming (OOP)',\n      description: '2.5 points',\n      questions: [\n        {\n          id: 1201,\n          name: 'What is the difference between public, private, and static members in a TypeScript/JavaScript class, and what are their use cases?',\n        },\n        {\n          id: 1202,\n          name: 'Discuss SOLID principles and how they apply to Object-Oriented Programming in JavaScript or TypeScript.',\n        },\n        {\n          id: 1203,\n          name: 'What is the instanceof operator in JavaScript, and how does it work when checking if an object belongs to a specific class?',\n        },\n        {\n          id: 1204,\n          name: 'How do ES2015 classes simplify creating objects compared to traditional functions and prototypal inheritance in JavaScript?',\n        },\n        {\n          id: 1205,\n          name: 'Explain how you can achieve multiple inheritance in TypeScript and how it differs from extending classes in traditional OOP.',\n        },\n      ],\n    },\n    {\n      id: 1300,\n      name: 'Async JavaScript',\n      description: '1.5 points',\n      questions: [\n        {\n          id: 1301,\n          name: 'What is the difference between setTimeout and setInterval in JavaScript, and how can you stop them from running?',\n        },\n        {\n          id: 1302,\n          name: 'What are Promise.all() and Promise.race(), and how do they differ in their behavior when resolving multiple promises?',\n        },\n        {\n          id: 1303,\n          name: 'How does async/await syntax enhances the readability and maintainability of asynchronous JavaScript code compared to traditional promise chains?',\n          type: InputType.Input,\n        },\n      ],\n    },\n    {\n      id: 1400,\n      name: 'Errors and Debugging',\n      description: '1 point',\n      questions: [\n        {\n          id: 1401,\n          name: 'What is the role of the finally block in error handling, and how does it interact with try and catch blocks in JavaScript?',\n        },\n        {\n          id: 1402,\n          name: 'How can you create and throw a custom error in JavaScript, and why would you do so? Provide a practical example.',\n          type: InputType.Input,\n        },\n      ],\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/data/interviews/technical-screening.tsx",
    "content": "import { ReactNode } from 'react';\nimport ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';\nimport { InputType } from './types';\nimport { InterviewFeedbackStepData, InterviewQuestion } from '@common/models';\n\nfunction TimeForStep({ minutes }: { minutes: string }) {\n  return (\n    <div>\n      <ClockCircleOutlined />\n      &nbsp; {minutes} min\n    </div>\n  );\n}\n\n/**\n * define steps for interview feedback, required to be filled\n */\nexport enum FeedbackStepId {\n  Introduction = 'intro',\n  Theory = 'theory',\n  Practice = 'practice',\n  English = 'english',\n  Decision = 'decision',\n}\n\nexport type Step = {\n  id: FeedbackStepId;\n  title: string;\n  /** description of the step shown as a content */\n  description: ReactNode;\n  /** description of the step shown on the stepper description section */\n  stepperDescription: string;\n  items: StepFormItem[];\n};\n\nexport type FeedbackStep = Step & InterviewFeedbackStepData;\n\nexport type Feedback = {\n  /**\n   * version of the feedback template. Currently supported only 1.\n   */\n  version: number;\n\n  /** the steps of the feedback template */\n  steps: FeedbackStep[];\n  /**\n   * defines whether interview is completed\n   */\n  isCompleted: boolean;\n};\n\n/**\n * The mentor decision about the student\n */\nexport enum Decision {\n  Yes = 'yes',\n  No = 'no',\n  Draft = 'draft',\n  SeparateStudy = 'separateStudy',\n  MissedWithReason = 'missedWithReason',\n  MissedIgnoresMentor = 'missedIgnoresMentor',\n}\n\nexport type StepFormItem = RadioItem | TextItem | InputItem | CheckboxItem | RadioButtonItem | QuestionItem;\n\ninterface Field {\n  id: string;\n  title: string;\n  required?: boolean;\n}\n\nexport interface RadioItem extends Field {\n  type: InputType.Radio;\n  options: RadioOption[];\n}\n\nexport interface RadioButtonItem extends Field {\n  type: InputType.RadioButton;\n  options: RadioOption[];\n  description?: string;\n}\n\ninterface InputItem extends Field {\n  type: InputType.Input;\n  placeholder?: string;\n  description?: string;\n  inputType: 'number' | 'text';\n  defaultValue?: string | number;\n  min?: number;\n  max?: number;\n}\n\ninterface TextItem extends Field {\n  type: InputType.TextArea;\n  description?: string;\n  placeholder: string;\n}\n\ninterface CheckboxItem extends Field {\n  type: InputType.Checkbox;\n  options: CheckboxOption[];\n}\n\nexport interface QuestionItem {\n  id: string;\n  title: string;\n  required?: boolean;\n  type: InputType.Rating;\n  /**\n   * The default list of questions for the interview\n   */\n  questions: InterviewQuestion[];\n  /**\n   * The pool of the questions available for consideration. Once the some of the `questions` are removed they are automatically added to the pool on the client\n   */\n  examples?: InterviewQuestion[];\n  /**\n   * The explanation of the rating values\n   */\n  tooltips?: string[];\n}\n\nexport const SKILLS_LEVELS = [\n  `Doesn't know`,\n  `Poor knowledge (almost doesn't know)`,\n  'Knows something (with tips)',\n  'Good knowledge (makes not critical mistakes)',\n  'Great knowledge',\n];\n\nexport const CODING_LEVELS = [\n  `Isn't able to code`,\n  `Poor coding ability (almost isn't able to)`,\n  'Can code with tips',\n  'Good coding ability (makes not critical mistakes)',\n  'Great coding ability',\n];\n\ntype CheckboxOption = {\n  id: string;\n  title: string;\n};\n\nexport type RadioOption = {\n  id: string;\n  title: string;\n  options?: RadioOption[];\n};\n\n//#region Steps definition\nexport const introduction: Step = {\n  id: FeedbackStepId.Introduction,\n  title: 'Introduction',\n  stepperDescription: 'Interview confirmation',\n  description: (\n    <>\n      <div>The interviewer checks a student's camera, sound and video.</div>\n      <div>\n        Then the mentor tells about himself in some words and becomes ready to listen to student's brief intro. Face to\n        face interviewing helps both parties to interact and form a connection.\n      </div>\n      <div>Make a mark, if the interview can't be managed.</div>\n      <TimeForStep minutes=\"3\" />\n    </>\n  ),\n  items: [\n    {\n      id: 'interviewResult',\n      type: InputType.Radio,\n      title: 'Did the student show up for the interview?',\n      required: true,\n      options: [\n        { id: 'completed', title: \"Yes, it's ok.\" },\n        {\n          id: 'missed',\n          title: 'No, interview is failed.',\n          options: [\n            { id: Decision.MissedWithReason, title: 'Student has a significant reason.' },\n            { id: Decision.MissedIgnoresMentor, title: 'Student ignores mentor.' },\n            { id: Decision.SeparateStudy, title: 'Student continues separate studying.' },\n          ],\n        },\n      ],\n    },\n    {\n      id: 'comment',\n      type: InputType.TextArea,\n      title: 'Your comment',\n      placeholder: \"Comment about student's skills\",\n    },\n  ],\n};\n\nconst theoryQuestions = [\n  {\n    id: 'html',\n    title:\n      'Position and display attribute values, tags, weight of selectors, pseudo-classes and elements, box model, relative and absolute values, em vs rem, semantic, semantic tags, etc.',\n    topic: 'HTML/CSS',\n  },\n  {\n    id: 'oop',\n    title: 'OOP (Encapsulation, Polymorphism, and Inheritance)',\n    topic: 'Computer Science',\n  },\n  {\n    id: 'algorithms',\n    title: 'Sorting and search algorithms (Binary search, Bubble sort, Quick sort, etc.)',\n    topic: 'Computer Science',\n  },\n  {\n    id: 'bigO',\n    title: 'Big O notation',\n    topic: 'Computer Science',\n  },\n  {\n    id: 'binaryNumbers',\n    title: 'Binary number',\n    topic: 'Computer Science',\n  },\n  {\n    id: 'array',\n    title: 'Array. Operations complexity.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'list',\n    title: 'List. Operations complexity.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'stack',\n    title: 'Stack. Operations complexity.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'queue',\n    title: 'Queue. Operations complexity.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'tree',\n    title: 'Tree. Operations complexity.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'hashTable',\n    title: 'Hash table. Operations complexity.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'heap',\n    title: 'Heap. Operations complexity.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'structuresOverview',\n    title: 'Difference between list and array, or between stack and queue.',\n    topic: 'Data structures',\n  },\n  {\n    id: 'dataTypes',\n    title: 'Understanding data types - from primitives to objects',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'variables',\n    title: 'Variables',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'numbers',\n    title: 'Number & Math methods',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'strings',\n    title: 'String methods & String templates',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'operators',\n    title: 'Ternary, Nullish Coalescing, Optional Chaining, and Logical Operators – Syntax and Use Cases',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'switchCase',\n    title: 'Switch case - examples where it can be useful',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'loops',\n    title: 'Loops - for, while, do while',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'typesConversion',\n    title: 'Be able to discover cases of implicit data types conversion into boolean, string, number',\n    topic: 'JavaScript Fundamentals',\n  },\n  {\n    id: 'strictComparison',\n    title: 'Strict comparison',\n    topic: 'JavaScript Fundamentals',\n  },\n];\n\nconst theory: Step = {\n  id: FeedbackStepId.Theory,\n  title: 'Theory',\n  stepperDescription: 'Talk about theory, how things work',\n  description: (\n    <>\n      <div>\n        Ask student some questions from the self-study course. You can use the list of recommended questions or add your\n        own.\n      </div>\n      <TimeForStep minutes=\"30-45\" />\n    </>\n  ),\n  items: [\n    {\n      type: InputType.Rating,\n      id: 'questions',\n      title: 'What questions did the student have to answer?',\n      required: true,\n      tooltips: SKILLS_LEVELS,\n      questions: theoryQuestions,\n      examples: theoryQuestions,\n    },\n    {\n      id: 'comment',\n      type: InputType.TextArea,\n      title: 'Your comment',\n      placeholder: \"Comment about student's skills\",\n    },\n  ],\n};\nconst practiceQuestions = [\n  {\n    id: '1',\n    title: `Given an integer array arr and a filtering function fn, return a new array with a fewer or equal number of elements.\n  The returned array should only contain elements where fn(arr[i], i) evaluated to a truthy value.`,\n  },\n  {\n    id: '2',\n    title: `Implement a function that takes two arrays of numbers and returns an array of numbers that are common between the two input arrays.`,\n  },\n];\nconst practice: Step = {\n  id: FeedbackStepId.Practice,\n  title: 'Practice',\n  stepperDescription: 'Propose technical tasks to solve',\n  description: (\n    <>\n      Ask the student to solve the coding problem. See the list of examples of coding tasks or suggest another problem\n      of the same level.\n      <TimeForStep minutes=\"10-30\" />\n    </>\n  ),\n  items: [\n    {\n      id: 'questions',\n      type: InputType.Rating,\n      title: 'What task did the student have to solve?',\n      required: true,\n      tooltips: CODING_LEVELS,\n      questions: practiceQuestions,\n      examples: practiceQuestions,\n    },\n    {\n      id: 'comment',\n      type: InputType.TextArea,\n      title: 'Your comment',\n      required: true,\n      placeholder: \"Comment about student's skills\",\n    },\n  ],\n};\n\nconst english: Step = {\n  id: FeedbackStepId.English,\n  title: 'English-language proficiency',\n  stepperDescription: 'Check English level',\n  description: (\n    <>\n      Ask the student to tell about themselves (2—3 min), hobby, favorite book, film etc.\n      <TimeForStep minutes=\"3-5\" />\n    </>\n  ),\n  items: [\n    {\n      id: 'englishCertificate',\n      type: InputType.RadioButton,\n      required: true,\n      title: 'Certified level of English',\n      description: 'Make a mark, if the student has a certificate, proving his English level',\n      options: [\n        { id: 'none', title: 'No certificate' },\n        { id: 'A1', title: 'A1' },\n        { id: 'A2', title: 'A2' },\n        { id: 'B1', title: 'B1' },\n        { id: 'B2', title: 'B2' },\n        { id: 'C1', title: 'C1' },\n        { id: 'C2', title: 'C2' },\n      ],\n    },\n    {\n      id: 'selfAssessment',\n      type: InputType.RadioButton,\n      title: 'English level by interviewers opinion',\n      required: true,\n      description:\n        \"Make a mark showing your own opinion of student's English level. It doesn't influence on the final score of the interview.\",\n      options: [\n        { id: 'A1', title: 'A1' },\n        { id: 'A2', title: 'A2' },\n        { id: 'B1', title: 'B1' },\n        { id: 'B2', title: 'B2' },\n        { id: 'C1', title: 'C1' },\n        { id: 'C2', title: 'C2' },\n      ],\n    },\n    {\n      id: 'comment',\n      type: InputType.TextArea,\n      title: 'Where did the student learn English? Your comment ',\n      placeholder: \"Comment about student's skills\",\n    },\n  ],\n};\n\nconst mentorDecision: Step = {\n  id: FeedbackStepId.Decision,\n  title: 'Mentor decision',\n  stepperDescription: 'Student admission to the mentoring program',\n  description: (\n    <>\n      Make a decision to accept a student into a mentoring program.\n      <TimeForStep minutes=\"5\" />\n    </>\n  ),\n  items: [\n    {\n      id: 'finalScore',\n      type: InputType.Input,\n      title: 'Final Score',\n      description: 'We calculated average score based on your marks, but you can adjust the final score',\n      inputType: 'number',\n      required: true,\n      min: 0,\n    },\n    {\n      id: 'isGoodCandidate',\n      type: InputType.Checkbox,\n      title:\n        'In your opinion, is this student a good candidate for mentoring with active interest and motivation? Make a mark',\n      options: [{ id: 'true', title: 'The student is a good candidate for mentoring.' }],\n    },\n    {\n      id: 'decision',\n      type: InputType.Radio,\n      title: 'Do you want to mentor this student and take them in your group?',\n      required: true,\n      options: [\n        { id: Decision.Yes, title: 'Yes, I will mentor this student.' },\n        { id: Decision.No, title: \"No, I won't.\" },\n        { id: Decision.Draft, title: \"I haven't decided yet. I'll submit the feedback later.\" },\n        { id: Decision.SeparateStudy, title: \"No, I won't. Student continues separate studying.\" },\n      ],\n    },\n    {\n      id: 'redFlags',\n      type: InputType.TextArea,\n      title: 'Red flags',\n      placeholder: \"Specify any red flags you've noticed during the interview\",\n    },\n    {\n      id: 'comment',\n      type: InputType.TextArea,\n      title: 'You can say something to the student (optional)',\n      description: 'The student will see this comment in interview results',\n      placeholder: \"Comment about student's skills\",\n    },\n  ],\n};\n\n//#endregion\n\n/**\n * define the order of the steps in the interview feedback template\n */\nconst steps: Step[] = [introduction, theory, practice, english, mentorDecision];\n\nexport const feedbackTemplate = {\n  version: 1,\n  steps,\n};\n"
  },
  {
    "path": "client/src/data/interviews/templateValidator.ts",
    "content": "import { InterviewTemplate, QuestionCategory } from '@client/data/interviews/types';\n\nexport class TemplateValidationError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = 'TemplateValidationError';\n  }\n}\n\nexport function validateInterviewTemplate(template: InterviewTemplate) {\n  const { name, categories } = template;\n\n  const uniqueCatIds = new Set<number>(categories.map(c => c.id));\n\n  if (uniqueCatIds.size !== categories.length) {\n    throw new TemplateValidationError(`Categories must have unique ids. Template name: ${name}`);\n  }\n\n  try {\n    categories.forEach(category => {\n      validateQuestionCategory(category);\n    });\n  } catch (error: unknown) {\n    if (error instanceof TemplateValidationError) {\n      throw new TemplateValidationError(`Template name ${name}. ${error.message}`);\n    }\n    throw error;\n  }\n}\n\nexport function validateQuestionCategory(category: QuestionCategory) {\n  const { id, questions } = category;\n\n  const uniqueQuestionIds = new Set<number>(questions.map(q => q.id) ?? []);\n\n  if (uniqueQuestionIds.size !== questions.length) {\n    throw new TemplateValidationError(`Questions must have unique ids. Category ID: ${id}`);\n  }\n}\n"
  },
  {
    "path": "client/src/data/interviews/types.ts",
    "content": "export enum InputType {\n  Input = 'input',\n  TextArea = 'textarea',\n  Checkbox = 'checkbox',\n  Radio = 'radio',\n  RadioButton = 'radioButton',\n  Rating = 'rating',\n}\n\nexport type Question = {\n  id: number;\n  name: string;\n  type?: InputType;\n};\n\nexport type QuestionCategory = {\n  id: number;\n  name: string;\n  description?: string;\n  questions: Question[];\n};\n\nexport type InterviewTemplate = {\n  name: string;\n  categories: QuestionCategory[];\n  examplesUrl: string;\n  descriptionHtml?: string;\n};\n"
  },
  {
    "path": "client/src/data/skills.ts",
    "content": "export const SKILLS = [\n  'react',\n  'angular',\n  'unit-tests',\n  'jest',\n  'javascript-core',\n  'redux',\n  'html',\n  'css',\n  'scss',\n  'less',\n  'nestjs',\n];\n"
  },
  {
    "path": "client/src/data/taskTypes.ts",
    "content": "import { TaskDtoTypeEnum } from '../api';\n\nexport const TASK_TYPES: { id: TaskDtoTypeEnum; name: string }[] = [\n  { id: TaskDtoTypeEnum.Jstask, name: 'JS task' },\n  { id: TaskDtoTypeEnum.Kotlintask, name: 'Kotlin task' },\n  { id: TaskDtoTypeEnum.Objctask, name: 'Objective-C task' },\n  { id: TaskDtoTypeEnum.Htmltask, name: 'HTML task' },\n  { id: TaskDtoTypeEnum.Ipynb, name: 'Jupyter Notebook' },\n  { id: TaskDtoTypeEnum.Cvmarkdown, name: 'CV (Markdown)' },\n  { id: TaskDtoTypeEnum.Cvhtml, name: 'CV (HTML)' },\n  { id: TaskDtoTypeEnum.Selfeducation, name: 'RS School App Test' },\n  { id: TaskDtoTypeEnum.Codewars, name: 'Codewars' },\n  { id: TaskDtoTypeEnum.Test, name: 'Google Form Test' },\n  { id: TaskDtoTypeEnum.Codejam, name: 'Codejam' },\n  { id: TaskDtoTypeEnum.Interview, name: 'Interview' },\n  { id: TaskDtoTypeEnum.StageInterview, name: 'Technical Screening' },\n];\n\nexport const TASK_TYPES_MAP = TASK_TYPES.reduce(\n  (acc, { id, name }) => ({ ...acc, [id]: name }),\n  {} as Record<string, string>,\n);\n"
  },
  {
    "path": "client/src/data/tshirts.ts",
    "content": "export const TSHIRT_SIZES = [\n  {\n    id: 'xs',\n    name: 'XS',\n  },\n  {\n    id: 's',\n    name: 'S',\n  },\n  {\n    id: 'm',\n    name: 'M',\n  },\n  {\n    id: 'l',\n    name: 'L',\n  },\n  {\n    id: 'xl',\n    name: 'XL',\n  },\n  {\n    id: 'xxl',\n    name: 'XXL',\n  },\n  {\n    id: 'xxxl',\n    name: 'XXXL',\n  },\n];\n"
  },
  {
    "path": "client/src/domain/course.test.ts",
    "content": "import { CourseTaskDto } from '@client/api';\nimport { getTasksTotalScore } from './course';\n\ndescribe('getTasksTotalScore', () => {\n  test('should calculate total score', () => {\n    expect(\n      getTasksTotalScore([\n        {\n          maxScore: 100,\n          scoreWeight: 1,\n        },\n        {\n          maxScore: 100,\n          scoreWeight: 0.5,\n        },\n      ] as CourseTaskDto[]),\n    ).toBe(100 + 50);\n  });\n});\n"
  },
  {
    "path": "client/src/domain/course.ts",
    "content": "import { CourseTaskDto } from '@client/api';\n\nexport function getTasksTotalScore(courseTasks: CourseTaskDto[]) {\n  return courseTasks.reduce((score, task) => score + (task.maxScore ?? 0) * task.scoreWeight, 0);\n}\n"
  },
  {
    "path": "client/src/domain/interview.test.ts",
    "content": "import { getRating, isInterviewRegistrationInProgress, isInterviewStarted } from './interview';\n\ndescribe('interview', () => {\n  beforeAll(() => vi.useFakeTimers().setSystemTime(new Date('2023-01-01')));\n\n  afterAll(() => vi.useRealTimers());\n\n  describe('isInterviewRegistrationInProgess', () => {\n    test('should return false if interview period starts more than in 2 weeks from now', () => {\n      const isInProgress = isInterviewRegistrationInProgress('2023-02-01');\n\n      expect(isInProgress).toBe(false);\n    });\n\n    test('should return false if interview period already started', () => {\n      const isInProgress = isInterviewRegistrationInProgress('2022-11-25');\n\n      expect(isInProgress).toBe(false);\n    });\n\n    test('should return true if interview period starts less than in 2 weeks', () => {\n      const isInProgress = isInterviewRegistrationInProgress('2023-01-10');\n\n      expect(isInProgress).toBe(true);\n    });\n  });\n\n  describe('isInterviewStarted', () => {\n    test('should return false if interview starts in future', () => {\n      const isStarted = isInterviewStarted('2023-02-01');\n\n      expect(isStarted).toBe(false);\n    });\n\n    test('should return true if current date is after interview start date', () => {\n      const isStarted = isInterviewStarted('2022-12-01');\n\n      expect(isStarted).toBe(true);\n    });\n  });\n\n  describe('getRating', () => {\n    test.each([\n      [5, 0.5],\n      [30, 3],\n      [50, 5],\n      [100, 5],\n    ])(`should calculate %s rating based on score %s for legacy feedback`, (score, expected) => {\n      const rating = getRating(score, 100, 0);\n\n      expect(rating).toBe(expected);\n    });\n\n    test.each([\n      [5, 0.25],\n      [30, 1.5],\n      [50, 2.5],\n      [90, 4.5],\n      [100, 5],\n    ])(`should calculate %s rating based on score %s`, (score, expected) => {\n      const rating = getRating(score, 100, 1);\n\n      expect(rating).toBe(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/domain/interview.tsx",
    "content": "import { Tag, Typography } from 'antd';\nimport { TaskDtoTypeEnum } from '@client/api';\nimport { StageInterviewFeedbackVerdict, InterviewDetails as CommonInterviewDetails } from '@common/models';\nimport { Decision } from '@client/data/interviews/technical-screening';\nimport dayjs from 'dayjs';\nimport between from 'dayjs/plugin/isBetween';\nimport { featureToggles } from '@client/services/features';\nimport CalendarOutlined from '@ant-design/icons/CalendarOutlined';\nimport { formatDate, formatShortDate } from '@client/services/formatter';\n\ndayjs.extend(between);\n\nexport function friendlyStageInterviewVerdict(value: StageInterviewFeedbackVerdict) {\n  switch (value) {\n    case 'didNotDecideYet':\n      return 'No decision yet';\n    case 'no':\n      return 'No';\n    case 'yes':\n      return 'Yes';\n    case 'noButGoodCandidate':\n      return 'No, but good student';\n    default:\n      return value;\n  }\n}\n\nexport function getInterviewResult(decision: Decision) {\n  switch (decision) {\n    case Decision.Yes:\n      return 'Mentor accepted';\n    case Decision.No:\n      return 'Mentor declined';\n    case Decision.Draft:\n      return 'No decision yet';\n    case Decision.SeparateStudy:\n      return 'Separate study';\n    case Decision.MissedIgnoresMentor:\n      return 'Ignored mentor';\n    case Decision.MissedWithReason:\n      return 'Missed with a reason';\n    default:\n      return friendlyStageInterviewVerdict(decision as StageInterviewFeedbackVerdict);\n  }\n}\n\nexport type InterviewDetails = CommonInterviewDetails;\n\nexport enum InterviewStatus {\n  NotCompleted,\n  Completed,\n  Canceled,\n}\n\nexport enum InterviewResult {\n  Yes = 'yes',\n  No = 'no',\n  Draft = 'draft',\n}\n\nexport function getInterviewCardResult(decision: Decision | StageInterviewFeedbackVerdict) {\n  switch (decision) {\n    case Decision.Yes:\n      return InterviewResult.Yes;\n    case Decision.Draft:\n      return InterviewResult.Draft;\n    case 'didNotDecideYet':\n      return InterviewResult.Draft;\n    case Decision.No:\n      return InterviewResult.No;\n    case Decision.SeparateStudy:\n      return InterviewResult.No;\n    case Decision.MissedIgnoresMentor:\n      return InterviewResult.No;\n    case Decision.MissedWithReason:\n      return InterviewResult.No;\n    case 'noButGoodCandidate':\n      return InterviewResult.No;\n    default:\n      return InterviewResult.Yes;\n  }\n}\n\nexport function isInterviewRegistrationInProgress(interviewStartDate: string) {\n  const startDate = dayjs(interviewStartDate).subtract(2, 'weeks');\n\n  return dayjs().isBetween(startDate, interviewStartDate);\n}\n\nexport function isInterviewStarted(interviewStartDate: string) {\n  return dayjs().isAfter(dayjs(interviewStartDate));\n}\n\nexport function getInterviewFeedbackUrl({\n  courseAlias,\n  interviewName,\n  interviewId,\n  studentGithubId,\n  template,\n  studentId,\n}: {\n  courseAlias: string;\n  studentGithubId: string;\n  studentId: number;\n  template?: string | null;\n  interviewName: string;\n  interviewId: number;\n}) {\n  const isScreening = isTechnicalScreening(interviewName);\n  if (!featureToggles.feedback && isScreening) {\n    return `/course/mentor/interview-technical-screening?course=${courseAlias}&githubId=${studentGithubId}`;\n  }\n  const type = isScreening ? TaskDtoTypeEnum.StageInterview : template;\n  return `/course/interview/${type}/feedback?course=${courseAlias}&githubId=${studentGithubId}&studentId=${studentId}&interviewId=${interviewId}`;\n}\n\nexport function isTechnicalScreening(name: string) {\n  return name.includes('Technical Screening');\n}\n\nexport const getInterviewWaitList = (courseAlias: string, interviewId: number) =>\n  `/course/mentor/interview-wait-list?course=${courseAlias}&interviewId=${interviewId}`;\n\n/** calculates the rating based on the interview score. rating scales from [0,5] */\nexport function getRating(score: number, maxScore: number, feedbackVersion: number) {\n  if (!feedbackVersion) {\n    // In the legacy feedback, the score is a number with limit 50\n    const maxScore = 50;\n    return (score > maxScore ? maxScore : score) / 10;\n  }\n  // calculate rating on the scale from 0 to 5\n  const rating = (score / maxScore) * 5;\n  return rating;\n}\n\nexport function DecisionTag({ decision, status }: { decision?: Decision; status?: InterviewStatus }) {\n  if (!decision) {\n    return (\n      <Tag color={status === InterviewStatus.Completed ? 'green' : undefined}>\n        {status === InterviewStatus.Completed ? 'Completed' : 'Uncompleted'}\n      </Tag>\n    );\n  }\n\n  switch (decision) {\n    case Decision.Yes:\n    case Decision.No:\n      return <Tag color=\"green\">Completed</Tag>;\n    case Decision.Draft:\n      return <Tag color=\"orange\">Unfilled form</Tag>;\n    case Decision.SeparateStudy:\n      return <Tag color=\"blue\">Separate study</Tag>;\n    case Decision.MissedIgnoresMentor:\n      return <Tag color=\"red\">Ignored mentor</Tag>;\n    case Decision.MissedWithReason:\n      return <Tag color=\"cyan\">Missed with a reason</Tag>;\n    default: {\n      // fallback to the old feedback values\n      if (decision === 'noButGoodCandidate') {\n        return <Tag color=\"green\">Completed</Tag>;\n      }\n      if (decision === 'didNotDecideYet') {\n        return <Tag color=\"orange\">Unfilled form</Tag>;\n      }\n      return <Tag>Uncompleted</Tag>;\n    }\n  }\n}\n\nexport function InterviewPeriod({\n  startDate,\n  endDate,\n  shortDate,\n}: {\n  startDate: string;\n  endDate: string;\n  shortDate?: boolean;\n}) {\n  const format = shortDate ? formatShortDate : formatDate;\n  return (\n    <div className=\"interview-period\">\n      <Typography.Text type=\"secondary\">\n        <CalendarOutlined style={{ marginRight: 8 }} />\n        {`${format(startDate)} - ${format(endDate)}`}\n      </Typography.Text>\n    </div>\n  );\n}\n\nexport const isRegistrationNotStarted = (studentRegistrationStartDate: string): boolean => {\n  return !!studentRegistrationStartDate && new Date() < new Date(studentRegistrationStartDate);\n};\n"
  },
  {
    "path": "client/src/domain/user.test.tsx",
    "content": "import { Session } from '@client/components/withSession';\nimport { CourseRole } from '@client/services/models';\nimport * as user from './user';\n\nconst mockSession: Session = {\n  courses: {},\n  githubId: 'test',\n  id: 1,\n  isAdmin: false,\n  isHirer: false,\n};\n\ndescribe('User', () => {\n  it('isAdmin', () => {\n    expect(user.isAdmin({} as Session)).toBe(false);\n    expect(user.isAdmin({ isAdmin: true } as Session)).toBe(true);\n  });\n\n  it('isAnyCourseManager', () => {\n    expect(user.isAnyCourseManager({ ...mockSession, courses: { 1: { roles: [CourseRole.Manager] } } })).toBe(true);\n    expect(user.isAnyCourseManager({ ...mockSession, courses: { 1: { roles: [CourseRole.Mentor] } } })).toBe(false);\n  });\n\n  it('isCourseManager', () => {\n    expect(user.isCourseManager({ ...mockSession, courses: { 1: { roles: [CourseRole.Manager] } } }, 1)).toBe(true);\n    expect(user.isCourseManager({ ...mockSession, courses: { 2: { roles: [CourseRole.Manager] } } }, 1)).toBe(false);\n  });\n\n  it('isPowerUser', () => {\n    expect(user.isPowerUser({ ...mockSession, courses: { 1: { roles: [CourseRole.Manager] } } }, 1)).toBe(true);\n    expect(user.isPowerUser({ ...mockSession, courses: { 1: { roles: [CourseRole.Supervisor] } } }, 1)).toBe(true);\n    expect(user.isPowerUser({ ...mockSession, isAdmin: true } as Session, 0)).toBe(true);\n    expect(user.isPowerUser({ ...mockSession, courses: { 1: { roles: [CourseRole.Supervisor] } } }, 2)).toBe(false);\n  });\n\n  it('isAnyMentor', () => {\n    expect(user.isAnyMentor({ ...mockSession, courses: { 1: { roles: [CourseRole.Mentor] } } })).toBe(true);\n    expect(user.isAnyMentor({ ...mockSession, courses: { 1: { roles: [] } } })).toBe(false);\n  });\n});\n"
  },
  {
    "path": "client/src/domain/user.ts",
    "content": "import { Session } from '@client/components/withSession';\nimport keys from 'lodash/keys';\nimport { CourseRole } from '@client/services/models';\n\nfunction hasRole(session: Session, courseId: number, role: CourseRole) {\n  return session.courses[courseId]?.roles.includes(role) ?? false;\n}\n\nexport function isExpelledStudent(session: Session, courseId: number) {\n  return session.courses[courseId]?.isExpelled === true;\n}\n\nexport function hasRoleInAnyCourse(session: Session, role: CourseRole) {\n  return keys(session.courses).some(courseId => hasRole(session, Number(courseId), role));\n}\n\nexport function isAdmin(session: Session) {\n  return Boolean(session.isAdmin);\n}\n\nexport function isMentor(session: Session, courseId: number) {\n  return !!courseId && hasRole(session, courseId, CourseRole.Mentor);\n}\n\nexport function getMentorId(session: Session, courseId: number) {\n  return session.courses[courseId]?.mentorId ?? null;\n}\n\nexport function isAnyMentor(session: Session) {\n  return hasRoleInAnyCourse(session, CourseRole.Mentor);\n}\n\nexport function isStudent(session: Session, courseId: number) {\n  return !!courseId && hasRole(session, courseId, CourseRole.Student);\n}\n\nexport function isActiveStudent(session: Session, courseId: number) {\n  return isStudent(session, courseId) && !isExpelledStudent(session, courseId);\n}\n\nexport function isCourseManager(session: Session, courseId: number) {\n  return isAdmin(session) || hasRole(session, courseId, CourseRole.Manager);\n}\n\nexport function isDementor(session: Session, courseId: number) {\n  return isAdmin(session) || hasRole(session, courseId, CourseRole.Dementor);\n}\n\nexport function isPowerUser(session: Session, courseId: number) {\n  return isAdmin(session) || isCourseManager(session, courseId) || isCourseSupervisor(session, courseId);\n}\n\nexport function isAnyCourseManager(session: Session) {\n  return hasRoleInAnyCourse(session, CourseRole.Manager);\n}\n\nexport function isCourseSupervisor(session: Session, courseId: number) {\n  return isAdmin(session) || hasRole(session, courseId, CourseRole.Supervisor);\n}\n\nexport function isAnyCourseSupervisor(session: Session) {\n  return isAdmin(session) || hasRoleInAnyCourse(session, CourseRole.Supervisor);\n}\n\nexport function isAnyCoursePowerUser(session: Session) {\n  return (\n    isAdmin(session) ||\n    hasRoleInAnyCourse(session, CourseRole.Manager) ||\n    hasRoleInAnyCourse(session, CourseRole.Supervisor)\n  );\n}\n\nexport function isAnyCourseDementor(session: Session) {\n  return isAdmin(session) || hasRoleInAnyCourse(session, CourseRole.Dementor);\n}\n\nexport function isTaskOwner(session: Session, courseId: number) {\n  return hasRole(session, courseId, CourseRole.TaskOwner);\n}\n\nexport function isHirer(session: Session) {\n  return Boolean(session.isHirer);\n}\n\nexport function getFullName(user: { firstName: string | null; lastName: string | null; githubId: string }) {\n  return user.firstName && user.lastName ? `${user.firstName} ${user.lastName}` : `${user.githubId}`;\n}\n"
  },
  {
    "path": "client/src/hooks/index.ts",
    "content": "export { useModalForm } from '@client/shared/hooks/useModal/useModalForm';\nexport type { ModalFormMode } from '@client/shared/hooks/useModal/useModalForm';\nexport { useMessage } from '@client/shared/hooks/useMessage';\nexport { useTheme } from '@client/shared/hooks/useTheme';\n"
  },
  {
    "path": "client/src/index.d.ts",
    "content": "declare module 'mq-polyfill';\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/AttemptsAnswers/AttemptsAnswers.tsx",
    "content": "import { Button, Col, Form, Row, Space, theme, Typography } from 'antd';\nimport { TaskVerificationAttemptDto } from '@client/api';\nimport { Question } from '@client/modules/AutoTest/components';\nimport dayjs from 'dayjs';\nimport { Fragment } from 'react';\nimport CalendarOutlined from '@ant-design/icons/CalendarOutlined';\n\ntype Props = {\n  attempts: TaskVerificationAttemptDto[];\n  hideAnswers: () => void;\n};\n\nconst { Title, Text } = Typography;\n\nfunction AttemptsAnswers({ attempts, hideAnswers }: Props) {\n  const { token } = theme.useToken();\n\n  return (\n    <Row style={{ background: token.colorBgContainer, padding: 24 }} gutter={[0, 24]} justify=\"center\">\n      <Col xs={24} lg={18} xl={12}>\n        <Row style={{ marginBottom: 16 }} wrap={false}>\n          <Col flex=\"auto\">Check your incorrect answers per attempt.</Col>\n          <Col flex=\"none\">\n            <Button onClick={hideAnswers}>Back to table</Button>\n          </Col>\n        </Row>\n        <Row>\n          <Col span={24}>\n            <Form layout=\"vertical\" requiredMark={false} disabled={true}>\n              {attempts.map((attempt, idx) => (\n                <Fragment key={idx}>\n                  <Row style={{ marginBottom: '0.5em' }}>\n                    <Col flex=\"auto\">\n                      <Title level={4} style={{ margin: 0 }}>\n                        Attempt #{attempts.length - idx}\n                      </Title>\n                    </Col>\n                    <Col flex=\"none\">\n                      <Text type=\"secondary\">\n                        <Space>\n                          <CalendarOutlined />\n                          {dayjs(attempt.createdDate).format('YYYY-MM-DD HH:mm')}\n                        </Space>\n                      </Text>\n                    </Col>\n                    <Col span={24}>\n                      <Text type=\"secondary\">\n                        Score: {attempt.score} / {attempt.maxScore}\n                      </Text>\n                    </Col>\n                  </Row>\n                  {attempt.questions?.map((question, questionIdx) => (\n                    <Question key={questionIdx} question={question} />\n                  ))}\n                </Fragment>\n              ))}\n            </Form>\n          </Col>\n        </Row>\n      </Col>\n    </Row>\n  );\n}\n\nexport default AttemptsAnswers;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/AutoTestTaskCard/AutoTestTaskCard.tsx",
    "content": "import { Button, Card, Col, Divider, Row, Typography, Switch } from 'antd';\nimport { TaskCardColumn } from '..';\nimport { BasicAutoTestTaskDto } from '@client/api';\nimport Link from 'next/link';\n\nconst { Paragraph } = Typography;\n\nexport interface AutoTestTaskCardProps {\n  courseTask: BasicAutoTestTaskDto;\n}\n\nfunction AutoTestTaskCard({ courseTask }: AutoTestTaskCardProps) {\n  const columns = [\n    {\n      label: 'Max attempts number',\n      value: courseTask.maxAttemptsNumber ? <>{courseTask.maxAttemptsNumber}</> : <>&ndash;</>,\n    },\n    {\n      label: 'Number of Questions',\n      value: courseTask.numberOfQuestions ? <>{courseTask.numberOfQuestions}</> : <>&ndash;</>,\n    },\n    {\n      label: 'Strict attempts mode',\n      value: <Switch checked={!!courseTask.strictAttemptsMode} />,\n    },\n    {\n      label: 'Threshold percentage',\n      value: courseTask.thresholdPercentage ? <>{courseTask.thresholdPercentage}</> : <>&ndash;</>,\n    },\n  ];\n\n  return (\n    <Card>\n      <Row gutter={[24, 4]}>\n        <Col span={24}>\n          <Paragraph\n            ellipsis={{\n              expandable: false,\n              rows: 1,\n            }}\n          >\n            <Typography.Text strong>{' ' + courseTask.name} </Typography.Text>\n          </Paragraph>\n        </Col>\n        <Col span={24}>\n          <Link href={`/admin/auto-test-task/${courseTask.id}`}>\n            <Button type=\"primary\">Preview Task</Button>\n          </Link>\n        </Col>\n      </Row>\n      <Divider />\n      <Row gutter={[8, 8]}>\n        {columns.map(item => (\n          <Col span={12} key={item.label}>\n            <TaskCardColumn {...item} />\n          </Col>\n        ))}\n      </Row>\n    </Card>\n  );\n}\n\nexport default AutoTestTaskCard;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/Coding/Coding.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { CourseTaskDetailedDtoTypeEnum, CheckerEnum } from '@client/api';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport Coding, { CodingProps } from './Coding';\n\nfunction renderCoding(type: CourseTaskDetailedDtoTypeEnum) {\n  const courseTask = {\n    name: 'Course Task',\n    studentStartDate: '2022-09-10 12:00',\n    studentEndDate: '2022-10-10 12:00',\n    checker: CheckerEnum.AutoTest,\n    id: 10,\n    descriptionUrl: 'description-url',\n    githubRepoName: 'github-repo-name',\n    type,\n    publicAttributes: {\n      maxAttemptsNumber: 2,\n    },\n  } as CourseTaskVerifications;\n\n  const props: CodingProps = {\n    courseTask,\n    githubId: 'github-id',\n  };\n  return render(<Coding {...props} />);\n}\n\ndescribe('Coding', () => {\n  it.each`\n    type                                        | text\n    ${CourseTaskDetailedDtoTypeEnum.Codewars}   | ${/Please use the next username in your/i}\n    ${CourseTaskDetailedDtoTypeEnum.Codewars}   | ${/codewars profile/i}\n    ${CourseTaskDetailedDtoTypeEnum.Jstask}     | ${/Tests run on Node.js version 22. Please make sure your solution works on Node.js version 22./i}\n    ${CourseTaskDetailedDtoTypeEnum.Jstask}     | ${/The system will run tests in the following repository and will update the score based on the result:/i}\n    ${CourseTaskDetailedDtoTypeEnum.Jstask}     | ${/https:\\/\\/github.com\\/github-id\\/github-repo-name/i}\n    ${CourseTaskDetailedDtoTypeEnum.Kotlintask} | ${/The system will run tests in the following repository and will update the score based on the result:/i}\n    ${CourseTaskDetailedDtoTypeEnum.Kotlintask} | ${/https:\\/\\/github.com\\/github-id\\/github-repo-name/i}\n  `(\n    'should render $type task with $text',\n    async ({ type, text }: { type: CourseTaskDetailedDtoTypeEnum; text: RegExp | string }) => {\n      renderCoding(type);\n\n      expect(await screen.findByText(text)).toBeInTheDocument();\n    },\n  );\n});\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/Coding/Coding.tsx",
    "content": "import { Space, Typography } from 'antd';\nimport { CourseTaskDetailedDtoTypeEnum } from '@client/api';\nimport CopyToClipboardButton from '@client/shared/components/CopyToClipboardButton';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport { useAsync } from 'react-use';\nimport { useState } from 'react';\n\nexport type CodingProps = {\n  courseTask: CourseTaskVerifications;\n  githubId: string;\n};\n\nconst { Paragraph, Text, Link } = Typography;\n\nasync function getCodewarsUsername(githubId: string) {\n  const digest = await window.crypto.subtle.digest('sha-1', new TextEncoder().encode(githubId));\n  const bytes = [...new Uint8Array(digest)];\n  const hash = bytes.map(x => x.toString(16).padStart(2, '0')).join('');\n  return `rsschool_${hash.slice(0, 16)}`;\n}\n\nfunction Coding({ courseTask, githubId }: CodingProps) {\n  const repoUrl = `https://github.com/${githubId}/${courseTask?.githubRepoName}`;\n  const codewarsLink = 'https://www.codewars.com/users/edit';\n  const [codewarsUsername, setCodewarsUsername] = useState<string | null>(null);\n\n  useAsync(async () => {\n    setCodewarsUsername(await getCodewarsUsername(githubId));\n  }, []);\n\n  if (courseTask.type === CourseTaskDetailedDtoTypeEnum.Codewars) {\n    return (\n      <>\n        <Paragraph>\n          Please use the next username in your{' '}\n          <Link href={codewarsLink} target=\"_blank\">\n            codewars profile\n          </Link>\n          :\n        </Paragraph>\n        <Paragraph>\n          {codewarsUsername ? (\n            <Space>\n              <Text strong>{codewarsUsername}</Text>\n              <CopyToClipboardButton value={codewarsUsername} />\n            </Space>\n          ) : null}\n        </Paragraph>\n      </>\n    );\n  }\n\n  return (\n    <>\n      {courseTask.type === CourseTaskDetailedDtoTypeEnum.Jstask && (\n        <Paragraph type=\"warning\" strong>\n          Tests run on Node.js version 22. Please make sure your solution works on Node.js version 22.\n        </Paragraph>\n      )}\n      <Paragraph>\n        The system will run tests in the following repository and will update the score based on the result:\n      </Paragraph>\n      <Paragraph>\n        <a href={repoUrl} target=\"_blank\">\n          {repoUrl}\n        </a>\n      </Paragraph>\n    </>\n  );\n}\n\nexport default Coding;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/Exercise/Exercise.tsx",
    "content": "import { Coding, JupyterNotebook, SelfEducation } from '@client/modules/AutoTest/components';\nimport { CourseTaskDetailedDtoTypeEnum } from '@client/api';\nimport { Col, ColProps, Form, Row, theme } from 'antd';\nimport { useCourseTaskSubmit } from '@client/modules/AutoTest/hooks';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport { useEffect, useState } from 'react';\nimport { TooltipedButton } from '@client/shared/components/TooltipedButton';\n\ntype ExerciseProps = {\n  githubId: string;\n  courseId: number;\n  courseTask: CourseTaskVerifications;\n  finishTask: () => void;\n};\n\nfunction responsiveColumns(type: CourseTaskDetailedDtoTypeEnum): ColProps | undefined {\n  if (type !== CourseTaskDetailedDtoTypeEnum.Selfeducation) {\n    return;\n  }\n\n  return {\n    xs: 24,\n    lg: 18,\n    xl: 12,\n  };\n}\n\nfunction Exercise({ githubId, courseId, courseTask, finishTask }: ExerciseProps) {\n  const { form, loading, submit, change } = useCourseTaskSubmit(courseId, courseTask, finishTask);\n  const [validationError, setValidationError] = useState(false);\n\n  const values = Form.useWatch([], form);\n\n  useEffect(() => {\n    if (!values || !Object.values(values).every(Boolean)) {\n      return;\n    }\n\n    form.validateFields({ validateOnly: true }).then(\n      () => {\n        setValidationError(false);\n      },\n      () => {\n        setValidationError(true);\n      },\n    );\n  }, [values]);\n\n  const getExercise = () => {\n    switch (courseTask?.type) {\n      case CourseTaskDetailedDtoTypeEnum.Jstask:\n      case CourseTaskDetailedDtoTypeEnum.Codewars:\n      case CourseTaskDetailedDtoTypeEnum.Kotlintask:\n      case CourseTaskDetailedDtoTypeEnum.Objctask:\n        return <Coding courseTask={courseTask} githubId={githubId} />;\n      case CourseTaskDetailedDtoTypeEnum.Ipynb:\n        return <JupyterNotebook />;\n      case CourseTaskDetailedDtoTypeEnum.Selfeducation:\n        return <SelfEducation courseTask={courseTask} />;\n      default:\n        return null;\n    }\n  };\n\n  const { token } = theme.useToken();\n\n  return (\n    <Row style={{ background: token.colorBgContainer, padding: '0 24px 24px' }} gutter={[0, 24]} justify=\"center\">\n      <Col {...responsiveColumns(courseTask.type)}>\n        <Form\n          form={form}\n          layout=\"vertical\"\n          requiredMark={false}\n          onFinish={submit}\n          onFinishFailed={() => setValidationError(true)}\n          onChange={change}\n        >\n          {getExercise()}\n          <Row justify=\"center\">\n            <TooltipedButton\n              tooltipTitle=\"Form has validation errors! Check that all required fields are filled!\"\n              open={validationError}\n              buttonText=\"Submit\"\n              loading={loading}\n              disabled={loading}\n            ></TooltipedButton>\n          </Row>\n        </Form>\n      </Col>\n    </Row>\n  );\n}\n\nexport default Exercise;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/JupyterNotebook/JupyterNotebook.tsx",
    "content": "import { UploadFile, Form, Upload, Button, Row } from 'antd';\nimport { useState } from 'react';\nimport { UploadOutlined } from '@ant-design/icons';\n\nfunction JupyterNotebook() {\n  const [uploadFile, setUploadFile] = useState<UploadFile | null>(null);\n  const handleFileChose = async (info: any) => setUploadFile(info.file);\n\n  return (\n    <Row>\n      <Form.Item name=\"upload\" rules={[{ required: true, message: 'Please upload the file' }]}>\n        <Upload fileList={uploadFile ? [uploadFile] : []} onChange={handleFileChose} multiple={false}>\n          <Button>\n            <UploadOutlined /> Select Jupyter Notebook\n          </Button>\n        </Upload>\n      </Form.Item>\n    </Row>\n  );\n}\n\nexport default JupyterNotebook;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/Question/Question.module.css",
    "content": ".question :global(.ant-radio) {\n  align-self: flex-start !important;\n  margin-top: 3px !important;\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/Question/Question.tsx",
    "content": "import { Typography, Form, Row, Checkbox, Radio, Col, Space } from 'antd';\nimport { SelfEducationQuestionSelectedAnswersDto } from '@client/api';\nimport styles from './Question.module.css';\n\nconst { Title } = Typography;\n\ntype QuestionProps = {\n  question: SelfEducationQuestionSelectedAnswersDto;\n};\n\nfunction Question({ question: selfEducationQuestion }: QuestionProps): JSX.Element {\n  const { question, questionImage, answers, answersType, multiple, selectedAnswers } = selfEducationQuestion;\n  const Element = multiple ? Checkbox : Radio;\n\n  return (\n    <div className={styles.question}>\n      <Form.Item\n        label={\n          <Row>\n            <Col>\n              <Title level={5}>{question}</Title>\n            </Col>\n            <Col span={24}>\n              {questionImage && (\n                <img\n                  src={questionImage}\n                  style={{\n                    width: '100%',\n                    maxWidth: '700px',\n                    marginBottom: '10px',\n                  }}\n                />\n              )}\n            </Col>\n          </Row>\n        }\n      >\n        <Space direction=\"vertical\" size=\"small\">\n          {answers?.map((answer, answerIndex) => {\n            const checked = selectedAnswers?.includes(answerIndex);\n\n            return (\n              <Element key={answerIndex} value={answerIndex} checked={checked}>\n                {answersType === 'image' ? (\n                  <>\n                    ({answerIndex + 1}){' '}\n                    <img\n                      src={answer}\n                      style={{\n                        width: '100%',\n                        maxWidth: '400px',\n                        marginBottom: '10px',\n                      }}\n                    />\n                  </>\n                ) : (\n                  answer\n                )}\n              </Element>\n            );\n          })}\n        </Space>\n      </Form.Item>\n    </div>\n  );\n}\n\nexport default Question;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/SelfEducation/SelfEducation.module.css",
    "content": ".selfEducation :global(.ant-radio) {\n  align-self: flex-start !important;\n  margin-top: 3px !important;\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/SelfEducation/SelfEducation.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { SelfEducationPublicAttributes, SelfEducationQuestion } from '@client/services/course';\nimport { CourseTaskVerifications } from '../../types';\nimport SelfEducation from './SelfEducation';\n\ndescribe('SelfEducation', () => {\n  it('should show 10 questions when total question count is 15', () => {\n    const NUMBER_OF_QUESTIONS = 10;\n    renderSelfEducation({ numberOfQuestions: NUMBER_OF_QUESTIONS });\n\n    const questions = screen.getAllByRole('heading', { name: /question-/i });\n    expect(questions).toHaveLength(NUMBER_OF_QUESTIONS);\n  });\n\n  it.each`\n    type          | multiple\n    ${'radio'}    | ${false}\n    ${'checkbox'} | ${true}\n  `(\n    \"should render $type when question's multiple option is $multiple\",\n    ({ type, multiple }: { type: string; multiple: boolean }) => {\n      renderSelfEducation({ questions: [generateQuestion({ multiple })] });\n\n      const question = screen.getByLabelText(/answer-0/i);\n      expect(question).toHaveClass(`ant-${type}-input`);\n    },\n  );\n\n  it.each`\n    condition                     | question\n    ${'image in question'}        | ${{ questionImage: 'some-image-url' }}\n    ${'image in radio answer'}    | ${{ answersType: 'image', answers: ['some-image-url'] }}\n    ${'image in checkbox answer'} | ${{ answersType: 'image', answers: ['some-image-url'], multiple: true }}\n  `('should render image when question has $condition', ({ question }: { question: SelfEducationQuestion }) => {\n    renderSelfEducation({ questions: [generateQuestion(question)] });\n\n    const image = screen.getByRole('img');\n    expect(image).toBeInTheDocument();\n  });\n});\n\nfunction renderSelfEducation({ numberOfQuestions = 5, questions = generateQuestions() }) {\n  const courseTask = {\n    publicAttributes: {\n      questions,\n      numberOfQuestions,\n      maxAttemptsNumber: 2,\n      tresholdPercentage: 90,\n    } as SelfEducationPublicAttributes,\n  } as CourseTaskVerifications;\n\n  return render(\n    <Form>\n      <SelfEducation courseTask={courseTask} />\n    </Form>,\n  );\n}\n\nfunction generateQuestion({\n  question = 'question-0',\n  answers = ['answer-0', 'answer-1'],\n  multiple = false,\n  answersType,\n  questionImage,\n}: Partial<SelfEducationQuestion>): SelfEducationQuestion {\n  return {\n    question,\n    answers,\n    multiple,\n    answersType,\n    questionImage,\n  };\n}\n\nfunction generateQuestions(count = 15): SelfEducationQuestion[] {\n  return new Array(count).fill({}).map((_, idx) => {\n    const question = `Question-${idx}`;\n    const answers = new Array(2).fill('').map((_, index) => `${question} Answer-${index}`);\n\n    return generateQuestion({\n      question,\n      answers,\n      multiple: false,\n    });\n  });\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/SelfEducation/SelfEducation.tsx",
    "content": "import { Typography, Form, Row, Checkbox, Radio, Col, Space } from 'antd';\nimport { useMemo } from 'react';\nimport { SelfEducationQuestionWithIndex, SelfEducationQuestion } from '@client/services/course';\nimport shuffle from 'lodash/shuffle';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\n\nimport styles from './SelfEducation.module.css';\n\ntype SelfEducationProps = {\n  courseTask: CourseTaskVerifications;\n};\n\nconst { Paragraph, Title } = Typography;\n\nfunction getRandomQuestions(questions: SelfEducationQuestion[]): SelfEducationQuestionWithIndex[] {\n  const questionsWithIndex = questions?.map((question, index) => ({ ...question, index }));\n  return shuffle(questionsWithIndex);\n}\n\nfunction SelfEducation({ courseTask }: SelfEducationProps) {\n  const { questions, numberOfQuestions } = courseTask.publicAttributes || {};\n\n  const randomQuestions = useMemo(\n    () => (getRandomQuestions(questions) || []).slice(0, numberOfQuestions),\n    [questions?.length, numberOfQuestions],\n  );\n\n  return (\n    <div className={styles.selfEducation}>\n      <Paragraph>To submit the task answer the questions.</Paragraph>\n      {randomQuestions?.map(\n        ({ question, answers, multiple, questionImage, answersType, index: questionIndex }, idx) => {\n          const questionNumber = idx + 1;\n          return (\n            <Form.Item\n              key={questionIndex}\n              label={\n                <Row>\n                  <Col>\n                    <Title level={5}>\n                      {questionNumber}. {question}\n                    </Title>\n                  </Col>\n                  <Col span={24}>\n                    {questionImage && (\n                      <img\n                        src={questionImage}\n                        style={{\n                          width: '100%',\n                          maxWidth: '700px',\n                          marginBottom: '10px',\n                        }}\n                      />\n                    )}\n                  </Col>\n                </Row>\n              }\n              name={`answer-${questionIndex}`}\n              rules={[{ required: true, message: 'Please answer the question' }]}\n            >\n              {multiple ? (\n                <Checkbox.Group>\n                  <Space direction=\"vertical\" size=\"small\">\n                    {answers?.map((answer, answerIndex) => (\n                      <Checkbox key={answerIndex} value={answerIndex}>\n                        {answersType === 'image' ? (\n                          <>\n                            ({answerIndex + 1}){' '}\n                            <img\n                              src={answer}\n                              style={{\n                                width: '100%',\n                                maxWidth: '400px',\n                                marginBottom: '10px',\n                              }}\n                            />\n                          </>\n                        ) : (\n                          answer\n                        )}\n                      </Checkbox>\n                    ))}\n                  </Space>\n                </Checkbox.Group>\n              ) : (\n                <Radio.Group>\n                  <Space direction=\"vertical\" size=\"small\">\n                    {answers?.map((answer, index) => (\n                      <Radio key={index} value={index}>\n                        {answersType === 'image' ? (\n                          <>\n                            ({index + 1}){' '}\n                            <img\n                              src={answer}\n                              style={{\n                                width: '100%',\n                                maxWidth: '400px',\n                                marginBottom: '10px',\n                              }}\n                            />\n                          </>\n                        ) : (\n                          answer\n                        )}\n                      </Radio>\n                    ))}\n                  </Space>\n                </Radio.Group>\n              )}\n            </Form.Item>\n          );\n        },\n      )}\n    </div>\n  );\n}\n\nexport default SelfEducation;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/StatusTabs/StatusTabs.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport StatusTabs from './StatusTabs';\nimport { CourseTaskStatus } from '@client/modules/AutoTest/types';\n\ndescribe('StatusTabs', () => {\n  const onTabChangeMock = vi.fn();\n\n  it('should render status tabs', () => {\n    const statuses = generateStatuses();\n\n    render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n    expect(screen.getAllByRole('tab')).toHaveLength(3);\n  });\n\n  it('should render status tabs when statuses were not provided', () => {\n    render(<StatusTabs statuses={[]} onTabChange={onTabChangeMock} />);\n\n    expect(screen.getAllByRole('tab')).toHaveLength(3);\n  });\n\n  it.each`\n    status                        | count\n    ${CourseTaskStatus.Available} | ${2}\n    ${CourseTaskStatus.Missed}    | ${3}\n    ${CourseTaskStatus.Done}      | ${4}\n  `(\n    'should render badge with count of $count for \"$status\" tab',\n    ({ status, count }: { status: string; count: number }) => {\n      const statuses = generateStatuses(undefined, { [status]: count });\n\n      render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n      expect(screen.getByText(count)).toBeInTheDocument();\n    },\n  );\n\n  describe('when active tab was changed', () => {\n    it.each`\n      tabName\n      ${CourseTaskStatus.Missed}\n      ${CourseTaskStatus.Done}\n    `('should call onTabChange with tab name \"$tabName\"', ({ tabName }: { tabName: string }) => {\n      const statuses = generateStatuses(undefined, { [tabName]: 2 });\n      render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n      const selectedTab = screen.getByText(new RegExp(tabName, 'i'));\n      fireEvent.click(selectedTab);\n\n      expect(onTabChangeMock).toHaveBeenCalledWith(tabName);\n    });\n  });\n});\n\nfunction generateStatuses(count = 3, statusTypeAndCount: Record<string, number> | null = null): CourseTaskStatus[] {\n  if (statusTypeAndCount) {\n    const statuses: CourseTaskStatus[] = [];\n\n    for (const statusType in statusTypeAndCount) {\n      if (Object.prototype.hasOwnProperty.call(statusTypeAndCount, statusType)) {\n        const statusCount = statusTypeAndCount[statusType];\n        statuses.push(...new Array(statusCount).fill(statusType));\n      }\n    }\n\n    return statuses;\n  }\n\n  return new Array(count).fill('').map(() => CourseTaskStatus.Missed);\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/StatusTabs/StatusTabs.tsx",
    "content": "import { Tabs } from 'antd';\nimport { useMemo } from 'react';\nimport { CourseTaskStatus } from '@client/modules/AutoTest/types';\nimport { tabsRenderer } from './renderers';\n\nexport interface StatusTabsProps {\n  statuses: CourseTaskStatus[];\n  activeTab?: CourseTaskStatus;\n  onTabChange: (tab: CourseTaskStatus) => void;\n}\n\nconst StatusTabs = ({ statuses, activeTab, onTabChange }: StatusTabsProps) => {\n  const tabs = useMemo(() => tabsRenderer(statuses, activeTab), [statuses, activeTab]);\n\n  const handleTabChange = (selectedTab: string) => {\n    onTabChange(selectedTab as CourseTaskStatus);\n  };\n\n  return <Tabs tabBarStyle={{ marginBottom: 0 }} activeKey={activeTab} items={tabs} onChange={handleTabChange} />;\n};\n\nexport default StatusTabs;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/StatusTabs/renderers.tsx",
    "content": "import { CourseTaskStatus, COURSE_TASK_STATUSES } from '@client/modules/AutoTest/types';\nimport { LabelItem, tabRenderer } from '@client/components/TabsWithCounter/renderers';\n\nexport const tabsRenderer = (statuses: CourseTaskStatus[], activeTab?: string) => {\n  return COURSE_TASK_STATUSES.reduce<LabelItem[]>((acc, current) => {\n    const { key, value } = current;\n\n    const newItem = {\n      label: value,\n      key,\n      count: statuses.filter(el => el === key).length || 0,\n    };\n    return [...acc, newItem];\n  }, []).map(item => tabRenderer(item, activeTab));\n};\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/TaskCard/TaskCard.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { TaskCard } from '..';\nimport { CheckerEnum } from '@client/api';\nimport { Course } from '@client/services/models';\nimport { CourseTaskState, CourseTaskVerifications } from '@client/modules/AutoTest/types';\n\nconst COURSE_MOCK = { alias: 'course-alias', id: 100 } as Course;\n\ndescribe('TaskCard', () => {\n  it.each`\n    prop                | value\n    ${'task name'}      | ${'Course Task'}\n    ${'start date'}     | ${'Sep 10'}\n    ${'end date'}       | ${'Oct 10'}\n    ${'state'}          | ${'Missed'}\n    ${'attempts count'} | ${'2 left'}\n    ${'score'}          | ${'–'}\n  `('should render $prop', ({ value }: { value: string }) => {\n    const courseTask = generateCourseTask(2);\n    render(<TaskCard course={COURSE_MOCK} courseTask={courseTask} />);\n\n    const element = screen.getByText(new RegExp(value, 'i'));\n    expect(element).toBeInTheDocument();\n  });\n\n  it('should render attempts count as \"No limits\" when max attempts was not provided', () => {\n    const courseTask = generateCourseTask();\n    render(<TaskCard course={COURSE_MOCK} courseTask={courseTask} />);\n\n    const element = screen.getByText(/No limits/i);\n    expect(element).toBeInTheDocument();\n  });\n});\n\nfunction generateCourseTask(maxAttemptsNumber?: number): CourseTaskVerifications {\n  return {\n    name: 'Course Task',\n    studentStartDate: '2022-09-10T12:00:00.000Z',\n    studentEndDate: '2022-10-10T12:00:00.000Z',\n    checker: CheckerEnum.AutoTest,\n    id: 10,\n    descriptionUrl: 'description-url',\n    state: CourseTaskState.Missed,\n    publicAttributes: {\n      maxAttemptsNumber,\n    },\n  } as CourseTaskVerifications;\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/TaskCard/TaskCard.tsx",
    "content": "import { Button, Card, Col, Divider, Row, Tag, Typography } from 'antd';\nimport Link from 'next/link';\nimport { getAutoTestTaskRoute } from '@client/services/routes';\nimport { TaskCardColumn, TaskDeadlineDate } from '..';\nimport { Course } from '@client/services/models';\nimport { useAttemptsMessage } from '@client/modules/AutoTest/hooks';\nimport { CourseTaskState, CourseTaskVerifications } from '@client/modules/AutoTest/types';\n\nconst { Title, Paragraph } = Typography;\n\nexport interface TaskCardProps {\n  courseTask: CourseTaskVerifications;\n  course: Course;\n}\n\nfunction getStatusTag(state: CourseTaskState) {\n  switch (state) {\n    case CourseTaskState.Completed:\n      return <Tag color=\"success\">{state}</Tag>;\n    case CourseTaskState.Missed:\n      return <Tag color=\"error\">{state}</Tag>;\n    default:\n      return <Tag color=\"default\">{CourseTaskState.Uncompleted}</Tag>;\n  }\n}\n\nfunction TaskCard({ courseTask, course }: TaskCardProps) {\n  const { id, name, studentStartDate, studentEndDate, verifications, state, descriptionUrl } = courseTask;\n  const { attemptsCount, explanation } = useAttemptsMessage(courseTask);\n\n  const score = verifications?.[0]?.score ?? null;\n\n  const columns = [\n    {\n      label: 'Status',\n      value: getStatusTag(state),\n    },\n    {\n      label: 'Attempts',\n      value: isFinite(attemptsCount) ? `${attemptsCount} left` : 'No limits',\n    },\n    {\n      label: 'Score',\n      value: score !== null ? score : <>&ndash;</>,\n    },\n  ];\n\n  return (\n    <Card\n      title={\n        <Title level={5} ellipsis={true}>\n          {name}\n        </Title>\n      }\n      extra={<TaskDeadlineDate startDate={studentStartDate} endDate={studentEndDate} state={state} />}\n    >\n      <Row gutter={[24, 24]}>\n        <Col span={24}>\n          <Paragraph\n            ellipsis={{\n              expandable: false,\n              rows: 1,\n            }}\n          >\n            Task:{' '}\n            <Link href={descriptionUrl} target=\"_blank\">\n              {descriptionUrl}\n            </Link>\n          </Paragraph>\n          <Paragraph\n            ellipsis={{\n              expandable: true,\n              rows: 3,\n              symbol: 'Read more',\n            }}\n          >\n            {explanation}\n          </Paragraph>\n        </Col>\n        <Col span={24}>\n          <Link href={getAutoTestTaskRoute(course.alias, id)} legacyBehavior>\n            <Button type=\"primary\">Open Task</Button>\n          </Link>\n        </Col>\n      </Row>\n      <Divider />\n      <Row gutter={8}>\n        {columns.map(item => (\n          <Col flex=\"auto\" key={item.label}>\n            <TaskCardColumn {...item} />\n          </Col>\n        ))}\n      </Row>\n    </Card>\n  );\n}\n\nexport default TaskCard;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/TaskCardColumn/TaskCardColumn.tsx",
    "content": "import { Space, Typography } from 'antd';\nimport { ReactNode } from 'react';\n\ntype TaskCardColumnProps = {\n  label: string;\n  value: ReactNode;\n};\n\nconst { Text } = Typography;\n\nfunction TaskCardColumn({ label, value }: TaskCardColumnProps) {\n  return (\n    <Space direction=\"vertical\">\n      <Text type=\"secondary\">{label}</Text>\n      {value}\n    </Space>\n  );\n}\n\nexport default TaskCardColumn;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/TaskDeadlineDate/TaskDeadlineDate.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport dayjs from 'dayjs';\nimport { TaskDeadlineDate, TaskDeadlineDateProps } from '..';\nimport { CourseTaskState } from '@client/modules/AutoTest/types';\n\ndescribe('TaskDeadlineDate', () => {\n  it.each`\n    type           | when                        | state                          | daysCount\n    ${'secondary'} | ${'end date is not passed'} | ${CourseTaskState.Uncompleted} | ${9}\n    ${'secondary'} | ${'end date is not passed'} | ${CourseTaskState.Completed}   | ${9}\n    ${'danger'}    | ${'end date passed'}        | ${CourseTaskState.Missed}      | ${1}\n  `(\n    'should render date as \"$type\" when $when',\n    ({ type, state, daysCount }: { type: string; state: CourseTaskState; daysCount: number }) => {\n      const date = dayjs();\n      const startDate = date.subtract(2, 'd');\n      const endDate = date.add(daysCount, 'd');\n      const props: TaskDeadlineDateProps = {\n        startDate: startDate.format(),\n        endDate: endDate.format(),\n        state,\n      };\n      render(<TaskDeadlineDate {...props} />);\n\n      expect(screen.getByText(new RegExp(endDate.format('MMM DD'), 'i'))).toHaveClass(`ant-typography-${type}`);\n    },\n  );\n});\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/TaskDeadlineDate/TaskDeadlineDate.tsx",
    "content": "import { Space, Typography } from 'antd';\nimport { CalendarOutlined, SwapRightOutlined } from '@ant-design/icons';\nimport { memo, useMemo } from 'react';\nimport { dateWithTimeZoneRenderer } from '@client/shared/components/Table';\nimport { BaseType } from 'antd/lib/typography/Base';\nimport { CourseTaskState } from '@client/modules/AutoTest/types';\n\nconst { Text } = Typography;\n\nexport type TaskDeadlineDateProps = {\n  startDate: string;\n  endDate: string;\n  state: CourseTaskState;\n  format?: string;\n};\n\nfunction getTextType(state: CourseTaskState): BaseType {\n  switch (state) {\n    case CourseTaskState.Missed:\n      return 'danger';\n    default:\n      return 'secondary';\n  }\n}\n\nfunction TaskDeadlineDate({ startDate, endDate, state, format = 'MMM DD' }: TaskDeadlineDateProps): JSX.Element {\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const type = useMemo(() => getTextType(state), [state]);\n\n  const start = dateWithTimeZoneRenderer(timezone, format)(startDate);\n  const end = dateWithTimeZoneRenderer(timezone, format)(endDate);\n\n  return (\n    <Space wrap style={{ justifyContent: 'end' }}>\n      <Text type={type}>\n        <CalendarOutlined />\n      </Text>\n      <Text type={type}>\n        {start} <SwapRightOutlined /> {end}\n      </Text>\n    </Space>\n  );\n}\n\nexport default memo(TaskDeadlineDate);\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/TaskDescription/TaskDescription.tsx",
    "content": "import { Row, Col, Space, Typography, theme } from 'antd';\nimport { ArrowLeftOutlined } from '@ant-design/icons';\nimport { getAutoTestRoute } from '@client/services/routes';\nimport { TaskDeadlineDate } from '..';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\n\nconst { Title, Text, Link } = Typography;\n\ntype TaskDescriptionProps = {\n  courseTask: CourseTaskVerifications;\n  courseAlias: string;\n};\n\nfunction TaskDescription({ courseAlias, courseTask }: TaskDescriptionProps) {\n  const { descriptionUrl, name, studentStartDate, studentEndDate, state } = courseTask;\n\n  const { token } = theme.useToken();\n\n  return (\n    <Row style={{ background: token.colorBgContainer, padding: '16px 24px' }}>\n      <Col flex=\"auto\">\n        <Title level={3}>\n          <Space size={24}>\n            <Link href={getAutoTestRoute(courseAlias)}>\n              <ArrowLeftOutlined />\n            </Link>\n            {name}\n          </Space>\n        </Title>\n      </Col>\n      <Col flex=\"none\">\n        <TaskDeadlineDate\n          startDate={studentStartDate}\n          endDate={studentEndDate}\n          state={state}\n          format=\"YYYY-MM-DD HH:mm\"\n        />\n      </Col>\n      {descriptionUrl ? (\n        <Col span={24}>\n          <Space align=\"start\">\n            <Text type=\"secondary\">Description: </Text>\n            <Link href={descriptionUrl} target=\"_blank\" style={{ wordBreak: 'break-word' }}>\n              {descriptionUrl}\n            </Link>\n          </Space>\n        </Col>\n      ) : null}\n    </Row>\n  );\n}\n\nexport default TaskDescription;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/VerificationInformation/VerificationInformation.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { CourseTaskDetailedDtoTypeEnum, CheckerEnum } from '@client/api';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport VerificationInformation, { VerificationInformationProps } from './VerificationInformation';\n\nfunction renderVerificationInformation({\n  type,\n  studentEndDate = '2022-10-10 12:00',\n  isTableVisible = true,\n}: {\n  type: CourseTaskDetailedDtoTypeEnum;\n  studentEndDate?: string;\n  isTableVisible?: boolean;\n}) {\n  const courseTask = {\n    name: 'Course Task',\n    studentStartDate: '2022-09-10 12:00',\n    studentEndDate,\n    checker: CheckerEnum.AutoTest,\n    id: 10,\n    descriptionUrl: 'description-url',\n    githubRepoName: 'github-repo-name',\n    type,\n    publicAttributes: {\n      maxAttemptsNumber: 2,\n    },\n  } as CourseTaskVerifications;\n\n  const props: VerificationInformationProps = {\n    courseTask,\n    isTableVisible,\n    loading: false,\n    reload: vi.fn(),\n    startTask: vi.fn(),\n    showAnswers: vi.fn(),\n  };\n\n  return render(<VerificationInformation {...props} />);\n}\n\ndescribe('VerificationInformation', () => {\n  it.each`\n    type\n    ${CourseTaskDetailedDtoTypeEnum.Codewars}\n    ${CourseTaskDetailedDtoTypeEnum.Codejam}\n    ${CourseTaskDetailedDtoTypeEnum.Cvhtml}\n    ${CourseTaskDetailedDtoTypeEnum.Cvmarkdown}\n    ${CourseTaskDetailedDtoTypeEnum.Htmltask}\n    ${CourseTaskDetailedDtoTypeEnum.Ipynb}\n    ${CourseTaskDetailedDtoTypeEnum.Jstask}\n    ${CourseTaskDetailedDtoTypeEnum.Kotlintask}\n    ${CourseTaskDetailedDtoTypeEnum.Objctask}\n  `(\n    'should not render \"Show answers\" button when task is $type',\n    ({ type }: { type: CourseTaskDetailedDtoTypeEnum }) => {\n      renderVerificationInformation({ type });\n\n      const answersButton = screen.queryByRole('button', { name: /show answers/i });\n      expect(answersButton).not.toBeInTheDocument();\n    },\n  );\n\n  it('should render \"Show answers\" button when task is SelfEducation', () => {\n    renderVerificationInformation({ type: CourseTaskDetailedDtoTypeEnum.Selfeducation });\n\n    const answersButton = screen.getByRole('button', { name: /show answers/i });\n    expect(answersButton).toBeInTheDocument();\n  });\n\n  it('should render start and refresh buttons if table is visible', () => {\n    renderVerificationInformation({ type: CourseTaskDetailedDtoTypeEnum.Jstask, isTableVisible: true });\n\n    const startTaskButton = screen.getByRole('button', { name: /start task/i });\n    const refreshButton = screen.getByRole('button', { name: /refresh/i });\n    expect(startTaskButton).toBeInTheDocument();\n    expect(refreshButton).toBeInTheDocument();\n  });\n\n  it('should not render start and refresh buttons if table is not visible', () => {\n    renderVerificationInformation({ type: CourseTaskDetailedDtoTypeEnum.Jstask, isTableVisible: false });\n\n    const startTaskButton = screen.queryByRole('button', { name: /start task/i });\n    const refreshButton = screen.queryByRole('button', { name: /refresh/i });\n    expect(startTaskButton).not.toBeInTheDocument();\n    expect(refreshButton).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/VerificationInformation/VerificationInformation.tsx",
    "content": "import { Alert, Button, Col, Row, Space, theme, Tooltip, Typography } from 'antd';\nimport { VerificationsTable } from '@client/modules/AutoTest/components';\nimport { useAttemptsMessage } from '@client/modules/AutoTest/hooks';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport ReloadOutlined from '@ant-design/icons/ReloadOutlined';\nimport { CourseTaskDetailedDtoTypeEnum } from '@client/api';\n\nexport type VerificationInformationProps = {\n  courseTask: CourseTaskVerifications;\n  loading: boolean;\n  isTableVisible: boolean;\n  startTask: () => void;\n  reload: () => void;\n  showAnswers: () => void;\n};\n\nconst { Text } = Typography;\n\nfunction VerificationInformation({\n  courseTask,\n  loading,\n  isTableVisible,\n  startTask,\n  reload,\n  showAnswers,\n}: VerificationInformationProps): any {\n  const { maxScore, verifications } = courseTask;\n  const { explanation, attemptsLeftMessage, allowStartTask, allowCheckAnswers } = useAttemptsMessage(courseTask);\n\n  const isSelfEducationTask = courseTask.type === CourseTaskDetailedDtoTypeEnum.Selfeducation;\n\n  const { token } = theme.useToken();\n\n  return (\n    <>\n      <Row\n        style={{\n          background: token.colorBgContainer,\n          padding: '24px 24px 12px',\n        }}\n        gutter={[0, 24]}\n        justify=\"end\"\n      >\n        <Col span={24}>\n          <Alert showIcon type=\"info\" message={explanation} />\n        </Col>\n        {attemptsLeftMessage && (\n          <Col flex=\"auto\">\n            <Space>\n              <Text type=\"secondary\">Attempts:</Text>\n              <Text>{attemptsLeftMessage}</Text>\n            </Space>\n          </Col>\n        )}\n        {isTableVisible && (\n          <Col flex=\"none\">\n            <Space wrap style={{ justifyContent: 'end' }}>\n              <Button type=\"primary\" onClick={startTask} disabled={!allowStartTask}>\n                Start task\n              </Button>\n              <Button icon={<ReloadOutlined />} onClick={reload}>\n                Refresh\n              </Button>\n              {isSelfEducationTask && (\n                <Tooltip\n                  placement=\"topRight\"\n                  title={allowCheckAnswers ? '' : 'Will be available after the deadline and at least 1 attempt'}\n                >\n                  <Button disabled={!allowCheckAnswers} onClick={showAnswers}>\n                    Show answers\n                  </Button>\n                </Tooltip>\n              )}\n            </Space>\n          </Col>\n        )}\n      </Row>\n      {isTableVisible && (\n        <Row\n          style={{\n            background: token.colorBgContainer,\n            padding: '0 24px 24px',\n          }}\n          gutter={[0, 24]}\n          justify=\"center\"\n        >\n          <Col span={24}>\n            <VerificationsTable maxScore={maxScore} verifications={verifications} loading={loading} />\n          </Col>\n        </Row>\n      )}\n    </>\n  );\n}\n\nexport default VerificationInformation;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/VerificationsTable/VerificationsTable.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { VerificationsTable, VerificationsTableProps } from '..';\nimport { Verification } from '@client/services/course';\n\nconst PROPS_MOCK: VerificationsTableProps = {\n  maxScore: 100,\n  loading: false,\n  verifications: [\n    {\n      id: 1,\n      score: 20,\n      createdDate: '2022-12-10T00:00:00.000Z',\n      details: 'Your accuracy: 40%.',\n    },\n  ] as Verification[],\n};\n\ndescribe('VerificationsTable', () => {\n  it.each`\n    item\n    ${'Date / Time'}\n    ${'Score / Max'}\n    ${'Accuracy'}\n    ${'Details'}\n    ${'20 / 100'}\n    ${'40%'}\n    ${'Your accuracy: 40%.'}\n  `('should render $item', ({ item }: { item: string }) => {\n    render(<VerificationsTable {...PROPS_MOCK} />);\n\n    const element = screen.getByText(item);\n    expect(element).toBeInTheDocument();\n  });\n\n  it('should render metadata when it was provided', () => {\n    const verificationWithMetadata = {\n      id: 1,\n      score: 20,\n      details: 'Accuracy: 30%.',\n      metadata: [\n        {\n          id: '1',\n          name: 'Metadata name',\n        },\n      ] as any,\n      courseTask: {\n        type: 'codewars',\n      },\n    } as Verification;\n    render(<VerificationsTable {...PROPS_MOCK} verifications={[verificationWithMetadata]} />);\n\n    const metadata = screen.getByText(/Metadata name/i);\n    expect(metadata).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/VerificationsTable/VerificationsTable.tsx",
    "content": "import { Table } from 'antd';\nimport { useMemo } from 'react';\nimport { Verification } from '@client/services/course';\nimport { getColumns } from './renderers';\n\nexport type VerificationsTableProps = {\n  maxScore: number;\n  verifications: Verification[];\n  loading: boolean;\n};\n\nfunction VerificationsTable({ maxScore, verifications, loading }: VerificationsTableProps) {\n  const columns = useMemo(() => getColumns(maxScore), [maxScore]);\n\n  return (\n    <Table\n      locale={{\n        // disable default tooltips on sortable columns\n        triggerDesc: undefined,\n        triggerAsc: undefined,\n        cancelSort: undefined,\n      }}\n      pagination={false}\n      columns={columns}\n      dataSource={verifications}\n      size=\"middle\"\n      rowKey=\"id\"\n      loading={loading}\n    />\n  );\n}\n\nexport default VerificationsTable;\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/VerificationsTable/renderers.tsx",
    "content": "import { Space, Typography } from 'antd';\nimport { ColumnType } from 'antd/lib/table';\nimport { dateWithTimeZoneRenderer } from '@client/shared/components/Table';\nimport { CourseTaskDetailedDtoTypeEnum } from '@client/api';\nimport { CheckSquareTwoTone, CloseSquareTwoTone } from '@ant-design/icons';\nimport { Verification } from '@client/services/course';\n\nconst { Text, Link, Title } = Typography;\n\ntype Breakpoint = ColumnType<Verification>['responsive'];\n\nconst DISPLAY_ALL: Breakpoint = ['sm'];\nconst DISPLAY_ACCURACY: Breakpoint = ['md'];\nconst DISPLAY_MOBILE: Breakpoint = ['xs'];\n\ntype Metadata = {\n  id: string;\n  url: string;\n  name: string;\n  completed: boolean;\n};\n\nexport function getColumns(maxScore: number): ColumnType<Verification>[] {\n  return [\n    {\n      key: 'date-time',\n      title: 'Date / Time',\n      dataIndex: 'createdDate',\n      responsive: DISPLAY_ALL,\n      render: renderDate,\n    },\n    {\n      key: 'score',\n      title: 'Score / Max',\n      dataIndex: 'score',\n      responsive: DISPLAY_ALL,\n      render: renderScore(maxScore),\n    },\n    {\n      key: 'accuracy',\n      title: 'Accuracy',\n      responsive: DISPLAY_ACCURACY,\n      render: (_, row: Verification) => {\n        const accuracyWordWithNumber = /accuracy:\\s+(\\d+%)/gi;\n        const [, accuracyNumber] = accuracyWordWithNumber.exec(row.details) ?? [];\n        return accuracyNumber ?? '–';\n      },\n    },\n    {\n      key: 'details',\n      title: 'Details',\n      dataIndex: 'details',\n      responsive: DISPLAY_ALL,\n      render: renderDetails,\n    },\n    {\n      key: 'details',\n      title: 'Details',\n      render: renderMobileRow(maxScore),\n      responsive: DISPLAY_MOBILE,\n    },\n  ];\n}\n\nfunction renderDetails(value: string, row: Verification) {\n  if (row?.courseTask?.type === CourseTaskDetailedDtoTypeEnum.Codewars) {\n    return (\n      <>\n        <Title level={5}>{value}</Title>\n        <Space direction=\"vertical\" align=\"start\">\n          {(row?.metadata as Metadata[])?.map(({ id, url, name, completed }, index: number) => (\n            <Link key={id} href={url} target=\"_blank\">\n              {completed ? (\n                <CheckSquareTwoTone twoToneColor=\"#52c41a\" />\n              ) : (\n                <CloseSquareTwoTone twoToneColor=\"#ff4d4f\" />\n              )}\n              <>\n                {' '}\n                {index}.{name}\n              </>\n            </Link>\n          ))}\n        </Space>\n      </>\n    );\n  }\n\n  return typeof value === 'string' ? value.split('\\\\n').map(str => <div key={str}>{str}</div>) : value;\n}\n\nfunction renderScore(maxScore: number) {\n  return (score: number) => (\n    <Text>\n      {score ?? 0} / {maxScore}\n    </Text>\n  );\n}\n\nfunction renderDate(createdDate: string) {\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n\n  return dateWithTimeZoneRenderer(timezone, 'YYYY-MM-DD HH:mm')(createdDate);\n}\n\nfunction renderMobileRow(maxScore: number) {\n  return (_: string, row: Verification) => (\n    <Space direction=\"vertical\">\n      {renderDate(row.createdDate)}\n      {renderScore(maxScore)(row.score)}\n      {renderDetails(row.details, row)}\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/components/index.tsx",
    "content": "export { default as TaskCard, type TaskCardProps } from './TaskCard/TaskCard';\nexport { default as TaskDescription } from './TaskDescription/TaskDescription';\nexport { default as TaskCardColumn } from './TaskCardColumn/TaskCardColumn';\nexport { default as TaskDeadlineDate, type TaskDeadlineDateProps } from './TaskDeadlineDate/TaskDeadlineDate';\nexport { default as VerificationsTable, type VerificationsTableProps } from './VerificationsTable/VerificationsTable';\nexport { default as Exercise } from './Exercise/Exercise';\nexport { default as Coding } from './Coding/Coding';\nexport { default as JupyterNotebook } from './JupyterNotebook/JupyterNotebook';\nexport { default as SelfEducation } from './SelfEducation/SelfEducation';\nexport { default as VerificationInformation } from './VerificationInformation/VerificationInformation';\nexport { default as StatusTabs } from './StatusTabs/StatusTabs';\nexport { default as AttemptsAnswers } from './AttemptsAnswers/AttemptsAnswers';\nexport { default as Question } from './Question/Question';\n"
  },
  {
    "path": "client/src/modules/AutoTest/hooks/index.ts",
    "content": "export { useAttemptsMessage } from './useAttemptsMessage/useAttemptsMessage';\nexport { useCourseTaskSubmit } from './useCourseTaskSubmit/useCourseTaskSubmit';\nexport { useCourseTaskVerifications } from './useCourseTaskVerifications/useCourseTaskVerifications';\nexport { useVerificationsAnswers } from './useVerificationsAnswers/useVerificationsAnswers';\n"
  },
  {
    "path": "client/src/modules/AutoTest/hooks/useAttemptsMessage/useAttemptsMessage.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { SelfEducationPublicAttributes, Verification } from '@client/services/course';\nimport { CourseTaskDetailedDtoTypeEnum } from '@client/api';\nimport { useAttemptsMessage } from './useAttemptsMessage';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport dayjs from 'dayjs';\n\nconst MAX_ATTEMPTS = 4;\nfunction renderUseAttemptsMessage({\n  verificationsCount = 0,\n  task,\n  verificationCreatedDate,\n}: {\n  verificationsCount?: number;\n  task?: CourseTaskVerifications;\n  verificationCreatedDate?: string;\n}) {\n  const verifications = new Array(verificationsCount).fill({\n    createdDate: dayjs(verificationCreatedDate).add(2, 'h').format(),\n  }) as Verification[];\n\n  let courseTask = {\n    publicAttributes: {\n      maxAttemptsNumber: MAX_ATTEMPTS,\n    },\n    verifications,\n  } as CourseTaskVerifications;\n\n  if (task) {\n    courseTask = { ...task, verifications };\n  }\n\n  const {\n    result: { current },\n  } = renderHook(() => useAttemptsMessage(courseTask));\n\n  return { ...current };\n}\n\ndescribe('useAttemptsMessage', () => {\n  it.each`\n    verificationsCount | attemptsLeft\n    ${0}               | ${4}\n    ${1}               | ${3}\n    ${4}               | ${0}\n    ${10}              | ${0}\n  `(\n    `should return left attempts count equal to $attemptsLeft when verifications count is $verificationsCount and max attempts is ${MAX_ATTEMPTS}`,\n    ({ verificationsCount, attemptsLeft }: { verificationsCount: number; attemptsLeft: number }) => {\n      const { attemptsCount } = renderUseAttemptsMessage({ verificationsCount });\n\n      expect(attemptsCount).toBe(attemptsLeft);\n    },\n  );\n\n  it('should return left attempts count equal to Infinity when max attempts was not provided', () => {\n    const task = {\n      type: CourseTaskDetailedDtoTypeEnum.Jstask,\n    } as CourseTaskVerifications;\n    const { attemptsCount } = renderUseAttemptsMessage({ task });\n\n    expect(attemptsCount).toBe(Infinity);\n  });\n\n  it.each`\n    publicAttributes                                                                              | verificationsCount | expected\n    ${{ strictAttemptsMode: true, maxAttemptsNumber: MAX_ATTEMPTS, tresholdPercentage: 90 }}      | ${0}               | ${'You must score at least 90% of points to pass. You have only 4 attempts.'}\n    ${{ strictAttemptsMode: false, maxAttemptsNumber: MAX_ATTEMPTS, tresholdPercentage: 90 }}     | ${0}               | ${'You must score at least 90% of points to pass. You have only 4 attempts. After limit attempts is over you can get only half of a score.'}\n    ${{ maxAttemptsNumber: MAX_ATTEMPTS, tresholdPercentage: 90, oneAttemptPerNumberOfHours: 1 }} | ${0}               | ${'You must score at least 90% of points to pass. You have only 4 attempts. After limit attempts is over you can get only half of a score. You have only one attempt per 1 hours.'}\n    ${{ maxAttemptsNumber: MAX_ATTEMPTS, tresholdPercentage: 90, oneAttemptPerNumberOfHours: 2 }} | ${1}               | ${'You must score at least 90% of points to pass. You have only 4 attempts. After limit attempts is over you can get only half of a score. You have only one attempt per 2 hours. Next submit is possible in'}\n    ${{ maxAttemptsNumber: undefined, tresholdPercentage: undefined }}                            | ${0}               | ${'You can submit your solution as many times as you need before the deadline. Without fines. After the deadline, the submission will be closed.'}\n  `(\n    `should return explanation when ${JSON.stringify(`$publicAttributes`)}`,\n    ({\n      publicAttributes,\n      verificationsCount,\n      expected,\n    }: {\n      publicAttributes: SelfEducationPublicAttributes;\n      verificationsCount: number;\n      expected: string;\n    }) => {\n      const task = {\n        publicAttributes,\n      } as CourseTaskVerifications;\n      const { explanation } = renderUseAttemptsMessage({ task, verificationsCount });\n\n      expect(explanation).toMatch(new RegExp(expected, 'i'));\n    },\n  );\n\n  it.each`\n    type                                           | strictAttemptsMode | verificationsCount | expected\n    ${CourseTaskDetailedDtoTypeEnum.Jstask}        | ${undefined}       | ${3}               | ${undefined}\n    ${CourseTaskDetailedDtoTypeEnum.Selfeducation} | ${undefined}       | ${3}               | ${\"Only 1 attempt left. Be careful, It's your last attempt!\"}\n    ${CourseTaskDetailedDtoTypeEnum.Selfeducation} | ${undefined}       | ${2}               | ${'2 attempts left.'}\n    ${CourseTaskDetailedDtoTypeEnum.Selfeducation} | ${true}            | ${4}               | ${'You have no more attempts.'}\n    ${CourseTaskDetailedDtoTypeEnum.Selfeducation} | ${undefined}       | ${4}               | ${'Limit of \"free\" attempts is over. Now you can get only half of a score.'}\n  `(\n    `should return left attempts count message when verifications count is $verificationsCount and max attempts is ${MAX_ATTEMPTS}`,\n    ({\n      type,\n      verificationsCount,\n      strictAttemptsMode,\n      expected,\n    }: {\n      type: CourseTaskDetailedDtoTypeEnum;\n      verificationsCount: number;\n      strictAttemptsMode: boolean;\n      expected: string;\n    }) => {\n      const task = {\n        type,\n        publicAttributes: {\n          maxAttemptsNumber: MAX_ATTEMPTS,\n          strictAttemptsMode,\n        },\n      } as CourseTaskVerifications;\n      const { attemptsLeftMessage } = renderUseAttemptsMessage({ task, verificationsCount });\n\n      expect(attemptsLeftMessage).toBe(expected);\n    },\n  );\n\n  describe('should allow to start task', () => {\n    it('when strict mode is false and attempts count is 0', () => {\n      const task = {\n        publicAttributes: {\n          maxAttemptsNumber: MAX_ATTEMPTS,\n          strictAttemptsMode: false,\n        },\n      } as CourseTaskVerifications;\n      const { allowStartTask } = renderUseAttemptsMessage({ task, verificationsCount: MAX_ATTEMPTS });\n\n      expect(allowStartTask).toBeTruthy();\n    });\n\n    it('when deadline has not passed', () => {\n      const task = {\n        studentEndDate: dayjs().add(7, 'd').format(),\n      } as CourseTaskVerifications;\n      const { allowStartTask } = renderUseAttemptsMessage({ task, verificationsCount: MAX_ATTEMPTS });\n\n      expect(allowStartTask).toBeTruthy();\n    });\n\n    it('when attempts per hours are not over', () => {\n      const task = {\n        studentEndDate: dayjs().add(7, 'd').format(),\n        publicAttributes: {\n          oneAttemptPerNumberOfHours: 1,\n        },\n      } as CourseTaskVerifications;\n      const { allowStartTask } = renderUseAttemptsMessage({\n        task,\n        verificationsCount: 1,\n        verificationCreatedDate: dayjs().subtract(4, 'h').format(),\n      });\n\n      expect(allowStartTask).toBeTruthy();\n    });\n  });\n\n  describe('should not allow to start task', () => {\n    it('when strict mode is true and attempts count is 0', () => {\n      const task = {\n        publicAttributes: {\n          maxAttemptsNumber: MAX_ATTEMPTS,\n          strictAttemptsMode: true,\n        },\n      } as CourseTaskVerifications;\n      const { allowStartTask } = renderUseAttemptsMessage({ task, verificationsCount: MAX_ATTEMPTS });\n\n      expect(allowStartTask).toBeFalsy();\n    });\n\n    it('when deadline has passed', () => {\n      const task = {\n        studentEndDate: '1970-01-01T00:00:00.000Z',\n      } as CourseTaskVerifications;\n      const { allowStartTask } = renderUseAttemptsMessage({ task });\n\n      expect(allowStartTask).toBeFalsy();\n    });\n\n    it('when attempts per hours are over', () => {\n      const task = {\n        studentEndDate: dayjs().add(7, 'd').format(),\n        publicAttributes: {\n          oneAttemptPerNumberOfHours: 3,\n        },\n      } as CourseTaskVerifications;\n      const { allowStartTask } = renderUseAttemptsMessage({ task, verificationsCount: 1 });\n\n      expect(allowStartTask).toBeFalsy();\n    });\n  });\n\n  describe('should not allow to check answers', () => {\n    it('when deadline is not passed', () => {\n      const task = {\n        studentEndDate: dayjs().add(7, 'd').format(),\n      } as CourseTaskVerifications;\n      const { allowCheckAnswers } = renderUseAttemptsMessage({ task });\n\n      expect(allowCheckAnswers).toBeFalsy();\n    });\n\n    it('when deadline is passed and attempts were not taken', () => {\n      const task = {\n        studentEndDate: dayjs().subtract(7, 'd').format(),\n        publicAttributes: {\n          maxAttemptsNumber: 5,\n        },\n      } as CourseTaskVerifications;\n      const { allowCheckAnswers } = renderUseAttemptsMessage({ task, verificationsCount: 0 });\n\n      expect(allowCheckAnswers).toBeFalsy();\n    });\n  });\n\n  describe('should allow to check answers', () => {\n    it('when deadline is passed and attempts were taken', () => {\n      const task = {\n        studentEndDate: dayjs().subtract(7, 'd').format(),\n        publicAttributes: {\n          maxAttemptsNumber: 5,\n        },\n      } as CourseTaskVerifications;\n      const { allowCheckAnswers } = renderUseAttemptsMessage({ task, verificationsCount: 3 });\n\n      expect(allowCheckAnswers).toBeTruthy();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/AutoTest/hooks/useAttemptsMessage/useAttemptsMessage.ts",
    "content": "import { useMemo } from 'react';\nimport { CourseTaskDetailedDtoTypeEnum } from '@client/api';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\n\ndayjs.extend(utc);\n\nexport function useAttemptsMessage(courseTask: CourseTaskVerifications) {\n  const { publicAttributes, type, verifications, studentEndDate } = courseTask;\n  const { maxAttemptsNumber, tresholdPercentage, strictAttemptsMode, oneAttemptPerNumberOfHours } =\n    publicAttributes || {};\n\n  const attemptsCount = useMemo((): number => {\n    if (maxAttemptsNumber) {\n      const leftCount = maxAttemptsNumber - (verifications?.length || 0);\n      return leftCount > 0 ? leftCount : 0;\n    }\n\n    return Infinity;\n  }, [maxAttemptsNumber, verifications?.length]);\n\n  const isDeadlinePassed = useMemo(() => {\n    const now = dayjs();\n    const endDate = dayjs(studentEndDate);\n\n    return now.isAfter(endDate);\n  }, [studentEndDate]);\n\n  const timeToNextSubmit = useMemo((): number => {\n    const [lastAttempt] = verifications || [];\n    const lastAttemptTime = lastAttempt?.createdDate;\n\n    if (oneAttemptPerNumberOfHours && lastAttemptTime) {\n      const diff = dayjs(lastAttemptTime).diff(dayjs().subtract(oneAttemptPerNumberOfHours, 'hour'));\n\n      if (diff < 0) {\n        return 0;\n      }\n\n      return diff;\n    }\n\n    return 0;\n  }, [oneAttemptPerNumberOfHours, verifications]);\n\n  const explanation = useMemo(() => {\n    if (tresholdPercentage && maxAttemptsNumber) {\n      let str = `You must score at least ${tresholdPercentage}% of points to pass. You have only ${maxAttemptsNumber} attempts.`;\n\n      if (!strictAttemptsMode) {\n        str += ' After limit attempts is over you can get only half of a score.';\n      }\n\n      if (oneAttemptPerNumberOfHours) {\n        str += ` You have only one attempt per ${oneAttemptPerNumberOfHours} hours.`;\n      }\n\n      if (timeToNextSubmit !== 0 && attemptsCount > 0) {\n        str += ` Next submit is possible in ${dayjs.utc(timeToNextSubmit).format('HH:mm:ss')}`;\n      }\n\n      return str;\n    }\n\n    return 'You can submit your solution as many times as you need before the deadline. Without fines. After the deadline, the submission will be closed.';\n  }, [maxAttemptsNumber, tresholdPercentage, strictAttemptsMode, timeToNextSubmit]);\n\n  const attemptsLeftMessage = useMemo((): string | undefined => {\n    if (type !== CourseTaskDetailedDtoTypeEnum.Selfeducation || isDeadlinePassed) {\n      return;\n    }\n\n    if (attemptsCount === 1) {\n      return `Only 1 attempt left. Be careful, It's your last attempt!`;\n    }\n\n    if (attemptsCount > 1) {\n      return `${attemptsCount} attempts left.`;\n    }\n\n    if (strictAttemptsMode) {\n      return 'You have no more attempts.';\n    }\n\n    return 'Limit of \"free\" attempts is over. Now you can get only half of a score.';\n  }, [attemptsCount, strictAttemptsMode]);\n\n  const allowStartTask = useMemo(() => {\n    if (isDeadlinePassed || !!timeToNextSubmit || (strictAttemptsMode && !attemptsCount)) {\n      return false;\n    }\n\n    return true;\n  }, [strictAttemptsMode, attemptsCount, isDeadlinePassed]);\n\n  const allowCheckAnswers = useMemo(\n    () => isDeadlinePassed && verifications?.length > 0,\n    [isDeadlinePassed, verifications?.length],\n  );\n\n  return {\n    attemptsCount,\n    explanation,\n    attemptsLeftMessage,\n    allowStartTask,\n    allowCheckAnswers,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/hooks/useCourseTaskSubmit/useCourseTaskSubmit.test.ts",
    "content": "import { renderHook } from '@testing-library/react';\nimport { CourseTaskDetailedDtoTypeEnum, CourseTaskVerificationsApi } from '@client/api';\nimport { IpynbFile, useCourseTaskSubmit } from './useCourseTaskSubmit';\nimport { FilesService } from '@client/services/files';\nimport { act } from 'react-dom/test-utils';\nimport { AxiosError } from 'axios';\nimport * as UserUtils from '@client/domain/user';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\n\nvi.mock('@client/services/files', () => ({\n  FilesService: vi.fn(),\n}));\nvi.mock('@client/domain/user');\nvi.mock('@client/api', async importOriginal => {\n  const actual = await importOriginal<typeof import('@client/api')>();\n  function MockCourseTaskVerificationsApi() {}\n  MockCourseTaskVerificationsApi.prototype.createTaskVerification = vi.fn();\n  return {\n    ...actual,\n    CourseTaskVerificationsApi: MockCourseTaskVerificationsApi,\n  };\n});\n\nconst mockErrorNotification = vi.fn();\nconst mockSuccessNotification = vi.fn();\n\nvi.mock('@client/hooks/useMessage', () => ({\n  useMessage: () => ({\n    notification: {\n      error: mockErrorNotification,\n      success: mockSuccessNotification,\n    },\n  }),\n}));\n\nconst uploadFileMock = vi.fn().mockImplementation(() => ({ s3Key: 'some-string' }));\nvi.mocked(FilesService).mockImplementation(function () {\n  return { uploadFile: uploadFileMock };\n});\n\nconst FILE_VALUE_MOCK = {\n  upload: {\n    file: {\n      originFileObj: new File([new Blob(['blob-blob'])], 'file-name'),\n    },\n  },\n} as IpynbFile;\nconst SELF_EDUCATION_MOCK = { ['answer-0']: 1, ['answer-1']: 2 };\n\nconst CODING_RESULT = {\n  githubRepoName: 'github-repo-name',\n  sourceGithubRepoUrl: 'source-github-repo-url',\n};\nconst CODEWARS_RESULT = { deadline: '2022-10-10T00:00.000Z' };\nconst SELF_EDUCATION_RESULT = [\n  { index: 0, value: 1 },\n  { index: 1, value: 2 },\n];\nconst IPYNB_RESULT = { s3Key: expect.any(String), taskName: 'course_task_ipynb' };\n\ndescribe('useCourseTaskSubmit', () => {\n  it.each`\n    type                                           | values                 | result\n    ${CourseTaskDetailedDtoTypeEnum.Ipynb}         | ${FILE_VALUE_MOCK}     | ${IPYNB_RESULT}\n    ${CourseTaskDetailedDtoTypeEnum.Selfeducation} | ${SELF_EDUCATION_MOCK} | ${SELF_EDUCATION_RESULT}\n    ${CourseTaskDetailedDtoTypeEnum.Codewars}      | ${{}}                  | ${CODEWARS_RESULT}\n    ${CourseTaskDetailedDtoTypeEnum.Jstask}        | ${{}}                  | ${CODING_RESULT}\n    ${CourseTaskDetailedDtoTypeEnum.Kotlintask}    | ${{}}                  | ${CODING_RESULT}\n    ${CourseTaskDetailedDtoTypeEnum.Objctask}      | ${{}}                  | ${CODING_RESULT}\n    ${CourseTaskDetailedDtoTypeEnum.Cvmarkdown}    | ${{}}                  | ${{}}\n    ${CourseTaskDetailedDtoTypeEnum.Cvhtml}        | ${{}}                  | ${{}}\n  `(\n    'should post task verification for $type',\n    async ({ type, values, result }: { type: CourseTaskDetailedDtoTypeEnum; values: any; result: any }) => {\n      const createTaskVerificationMock = vi\n        .spyOn(CourseTaskVerificationsApi.prototype, 'createTaskVerification')\n        .mockResolvedValueOnce({ data: { courseTask: { type } } });\n\n      const courseTask = generateCourseTask(type);\n      const { submit } = renderUseCourseTaskSubmit(courseTask);\n\n      await act(async () => {\n        await submit(values);\n      });\n\n      expect(createTaskVerificationMock).toHaveBeenCalledWith(100, courseTask.id, result);\n    },\n  );\n\n  it(`should trigger file upload when task is ${CourseTaskDetailedDtoTypeEnum.Ipynb}`, async () => {\n    const courseTask = generateCourseTask(CourseTaskDetailedDtoTypeEnum.Ipynb);\n    const { submit } = renderUseCourseTaskSubmit(courseTask);\n\n    await act(async () => {\n      await submit(FILE_VALUE_MOCK);\n    });\n\n    expect(uploadFileMock).toHaveBeenCalled();\n  });\n\n  describe('when request failed', () => {\n    beforeEach(() => {\n      vi.clearAllMocks();\n    });\n\n    it.each`\n      statusCode | _message\n      ${401}     | ${'Your authorization token has expired. You need to re-login in the application.'}\n      ${429}     | ${'Please wait. You will be able to submit your task again when the current verification is completed.'}\n      ${423}     | ${'Please reload page. This task was expired for submit.'}\n      ${500}     | ${'An error occurred. Please try later.'}\n    `(\n      'and status code is $statusCode should trigger error notification',\n      async ({ statusCode }: { statusCode: number }) => {\n        const error = generateAxiosError(statusCode);\n        const createTaskVerificationSpy = vi\n          .spyOn(CourseTaskVerificationsApi.prototype, 'createTaskVerification')\n          .mockRejectedValueOnce(error);\n\n        const courseTask = generateCourseTask();\n        const { submit, finishTask, result } = renderUseCourseTaskSubmit(courseTask);\n\n        await act(async () => {\n          await submit({});\n        });\n\n        expect(createTaskVerificationSpy).toHaveBeenCalledTimes(1);\n        expect(finishTask).not.toHaveBeenCalled();\n        expect(result.current.loading).toBe(false);\n      },\n    );\n\n    it.each`\n      isExpelled | perHour      | _message\n      ${true}    | ${undefined} | ${'This task can only be submitted by active students.'}\n      ${false}   | ${undefined} | ${'You can submit this task only 4 times.  For now your attempts limit is over!'}\n      ${false}   | ${1}         | ${'You can submit this task only 4 times. You can submit this task not more than one time per 1 hours. For now your attempts limit is over!'}\n    `(\n      `and status code is 403 should trigger error notification`,\n      async ({ isExpelled, perHour }: { isExpelled: boolean; perHour: number }) => {\n        const error = generateAxiosError(403);\n        vi.spyOn(UserUtils, 'isExpelledStudent').mockImplementationOnce(() => isExpelled);\n        const createTaskVerificationSpy = vi\n          .spyOn(CourseTaskVerificationsApi.prototype, 'createTaskVerification')\n          .mockRejectedValueOnce(error);\n\n        const courseTask = generateCourseTask(CourseTaskDetailedDtoTypeEnum.Jstask, perHour);\n        const { submit, finishTask, result } = renderUseCourseTaskSubmit(courseTask);\n\n        await act(async () => {\n          await submit({});\n        });\n\n        expect(createTaskVerificationSpy).toHaveBeenCalledTimes(1);\n        expect(finishTask).not.toHaveBeenCalled();\n        expect(result.current.loading).toBe(false);\n      },\n    );\n  });\n});\n\nfunction renderUseCourseTaskSubmit(courseTask: CourseTaskVerifications) {\n  const finishTask = vi.fn();\n  const { result: view } = renderHook(() => useCourseTaskSubmit(100, courseTask, finishTask));\n\n  return { ...view.current, result: view, finishTask };\n}\n\nfunction generateCourseTask(\n  type: CourseTaskDetailedDtoTypeEnum = CourseTaskDetailedDtoTypeEnum.Jstask,\n  oneAttemptPerNumberOfHours?: number,\n): CourseTaskVerifications {\n  return {\n    id: 10,\n    name: `Course task ${type}`,\n    type,\n    checker: 'auto-test',\n    studentEndDate: '2022-10-10T00:00.000Z',\n    githubRepoName: 'github-repo-name',\n    sourceGithubRepoUrl: 'source-github-repo-url',\n    publicAttributes: {\n      maxAttemptsNumber: 4,\n      oneAttemptPerNumberOfHours,\n    },\n  } as CourseTaskVerifications;\n}\n\nfunction generateAxiosError(code: number): AxiosError {\n  return {\n    isAxiosError: true,\n    response: {\n      status: code,\n    },\n  } as AxiosError;\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/hooks/useCourseTaskSubmit/useCourseTaskSubmit.ts",
    "content": "import { Form } from 'antd';\nimport { CourseTaskDetailedDtoTypeEnum, CourseTaskVerificationsApi } from '@client/api';\nimport snakeCase from 'lodash/snakeCase';\nimport { useContext, useMemo, useState } from 'react';\nimport { FilesService } from '@client/services/files';\nimport { SelfEducationPublicAttributes } from '@client/services/course';\nimport { AxiosError } from 'axios';\nimport { isExpelledStudent } from '@client/domain/user';\nimport { SessionContext } from '@client/modules/Course/contexts';\nimport { InternalUploadFile } from 'antd/lib/upload/interface';\nimport { useBeforeUnload } from 'react-use';\nimport { CourseTaskVerifications } from '@client/modules/AutoTest/types';\nimport { useMessage } from '@client/hooks';\n\ntype SelfEducationValues = Record<string, number>;\nexport type IpynbFile = { upload: { file: InternalUploadFile } };\ntype FormValues = SelfEducationValues | IpynbFile;\n\nfunction isIpynbFile(item: unknown): item is IpynbFile {\n  return !!item && typeof item === 'object' && 'upload' in item;\n}\n\nexport function useCourseTaskSubmit(courseId: number, courseTask: CourseTaskVerifications, finishTask: () => void) {\n  const { notification } = useMessage();\n  const session = useContext(SessionContext);\n  const taskVerificationsApi = useMemo(() => new CourseTaskVerificationsApi(), []);\n  const [loading, setLoading] = useState(false);\n  const [isModified, setIsModified] = useState(false);\n  const [form] = Form.useForm<FormValues>();\n\n  useBeforeUnload(isModified, 'You have changes in test. Do you really want to close this page?');\n\n  const uploadIpynbFile = async (values: FormValues): Promise<string | undefined> => {\n    if (isIpynbFile(values)) {\n      const filesService = new FilesService();\n      const fileData = await readFile(values.upload.file);\n      const { s3Key } = await filesService.uploadFile('', fileData);\n      return s3Key;\n    }\n  };\n\n  const getSubmitData = async (values: FormValues) => {\n    switch (courseTask.type) {\n      case CourseTaskDetailedDtoTypeEnum.Ipynb: {\n        const s3Key = await uploadIpynbFile(values);\n        return {\n          s3Key,\n          taskName: snakeCase(courseTask.name),\n        };\n      }\n\n      case CourseTaskDetailedDtoTypeEnum.Selfeducation: {\n        return Object.entries(values)\n          .filter(([key]) => /answer/.test(key))\n          .map(([key, value]) => {\n            const [, index = 0] = key.match(/answer-(.*)$/) || [];\n            return { index: Number(index), value };\n          });\n      }\n\n      case CourseTaskDetailedDtoTypeEnum.Codewars: {\n        return {\n          deadline: courseTask.studentEndDate,\n        };\n      }\n\n      case CourseTaskDetailedDtoTypeEnum.Jstask:\n      case CourseTaskDetailedDtoTypeEnum.Kotlintask:\n      case CourseTaskDetailedDtoTypeEnum.Objctask:\n        return {\n          githubRepoName: courseTask.githubRepoName,\n          sourceGithubRepoUrl: courseTask.sourceGithubRepoUrl,\n        };\n\n      case CourseTaskDetailedDtoTypeEnum.Cvmarkdown:\n      case CourseTaskDetailedDtoTypeEnum.Cvhtml:\n        return {};\n\n      default:\n        return null;\n    }\n  };\n\n  const getError = (error: AxiosError<Error>): string => {\n    switch (error.response?.status) {\n      case 401:\n        return 'Your authorization token has expired. You need to re-login in the application.';\n\n      case 429:\n        return 'Please wait. You will be able to submit your task again when the current verification is completed.';\n\n      case 423:\n        return 'Please reload page. This task was expired for submit.';\n\n      case 403: {\n        if (isExpelledStudent(session, courseId)) {\n          return 'This task can only be submitted by active students.';\n        }\n        const { oneAttemptPerNumberOfHours, maxAttemptsNumber = 0 } = (courseTask?.publicAttributes ??\n          {}) as SelfEducationPublicAttributes;\n        const timeLimitedAttempts = oneAttemptPerNumberOfHours\n          ? `You can submit this task not more than one time per ${oneAttemptPerNumberOfHours} hours.`\n          : '';\n        return `You can submit this task only ${maxAttemptsNumber} times. ${timeLimitedAttempts} For now your attempts limit is over!`;\n      }\n\n      default:\n        return 'An error occurred. Please try later.';\n    }\n  };\n\n  const submit = async (values: FormValues) => {\n    if (loading) {\n      return;\n    }\n\n    setLoading(true);\n\n    const data = await getSubmitData(values);\n\n    if (!data) {\n      return;\n    }\n\n    try {\n      const response = await taskVerificationsApi.createTaskVerification(courseId, courseTask.id, data);\n\n      if (!response.data.id) {\n        notification.success({ message: 'The task has been submitted.' });\n      } else {\n        notification.success({ message: 'The task has been submitted for verification and it will be checked soon.' });\n      }\n\n      finishTask();\n      setIsModified(false);\n    } catch (e) {\n      const error = e as AxiosError<Error>;\n      const message = getError(error);\n\n      notification.error({\n        message,\n        // notification will never be closed automatically when status is 401\n        duration: error.response?.status === 401 ? false : undefined,\n      });\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const change = () => {\n    setIsModified(true);\n  };\n\n  return { form, loading, submit, change };\n}\n\nfunction readFile(file: any) {\n  return new Promise<string>((resolve, reject) => {\n    const reader = new FileReader();\n    reader.readAsText(file?.originFileObj, 'utf-8');\n    reader.onload = ({ target }) => resolve(target ? (target.result as string) : '');\n    reader.onerror = e => reject(e);\n  });\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/hooks/useCourseTaskVerifications/useCourseTaskVerifications.ts",
    "content": "import { useRequest } from 'ahooks';\nimport { message } from 'antd';\nimport { CheckerEnum, CoursesTasksApi, CourseTaskDtoTypeEnum } from '@client/api';\nimport dayjs from 'dayjs';\nimport isSameOrAfter from 'dayjs/plugin/isSameOrAfter';\nimport { mapTo } from '@client/modules/AutoTest/utils/map';\nimport { useEffect, useMemo, useState } from 'react';\nimport { CourseService } from '@client/services/course';\n\ndayjs.extend(isSameOrAfter);\n\nexport function useCourseTaskVerifications(courseId: number) {\n  const [isExerciseVisible, setIsExerciseVisible] = useState(false);\n\n  const { data } = useRequest(async () => {\n    const { data } = await new CoursesTasksApi().getCourseTasksDetailed(courseId);\n    const now = dayjs();\n    return data.filter(\n      item =>\n        item.checker === CheckerEnum.AutoTest &&\n        item.type !== CourseTaskDtoTypeEnum.Test &&\n        now.isSameOrAfter(item.studentStartDate),\n    );\n  });\n\n  const courseTasks = data;\n  const courseService = useMemo(() => new CourseService(courseId), []);\n\n  const {\n    loading,\n    data: allVerifications = [],\n    error,\n    run: reload,\n  } = useRequest(async () => await courseService.getTaskVerifications());\n\n  const tasks = useMemo(() => courseTasks?.map(ct => mapTo(ct, allVerifications)), [courseTasks, allVerifications]);\n\n  function startTask() {\n    setIsExerciseVisible(true);\n  }\n\n  function finishTask() {\n    reload();\n    setIsExerciseVisible(false);\n  }\n\n  useEffect(() => {\n    if (error?.message) {\n      message.error(error.message);\n    }\n  }, [error?.message]);\n\n  return {\n    tasks,\n    loading,\n    isExerciseVisible,\n    startTask,\n    finishTask,\n    reload,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/hooks/useVerificationsAnswers/useVerificationsAnswers.ts",
    "content": "import { message } from 'antd';\nimport { useState } from 'react';\nimport { CourseTaskVerificationsApi, TaskVerificationAttemptDto } from '@client/api';\nimport { AxiosError } from 'axios';\nimport { useLoading } from '@client/components/useLoading';\n\nexport function useVerificationsAnswers(courseId: number, courseTaskId: number) {\n  const [answers, setAnswers] = useState<TaskVerificationAttemptDto[] | null>(null);\n  const [loading, withLoading] = useLoading(false, e => {\n    const error = e as AxiosError<Error>;\n    message.error(error.response?.data?.message || error?.message);\n  });\n\n  const showAnswers = withLoading(async () => {\n    const result = await new CourseTaskVerificationsApi().getAnswers(courseId, courseTaskId);\n    setAnswers(result.data);\n  });\n\n  const hideAnswers = () => {\n    setAnswers(null);\n  };\n\n  return { loading, answers, showAnswers, hideAnswers };\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/pages/AutoTests/AutoTests.tsx",
    "content": "import { Col, Row } from 'antd';\nimport { ColProps } from 'antd/lib/grid';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { StatusTabs, TaskCard } from '@client/modules/AutoTest/components';\nimport { useCourseTaskVerifications } from '@client/modules/AutoTest/hooks';\nimport { CourseTaskStatus } from '@client/modules/AutoTest/types';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useMemo, useState } from 'react';\n\nconst RESPONSIVE_COLUMNS: ColProps = {\n  sm: 24,\n  md: 12,\n  lg: 8,\n  xl: 8,\n  xxl: 6,\n};\n\nfunction AutoTests() {\n  const { course } = useActiveCourseContext();\n  const { tasks } = useCourseTaskVerifications(course.id);\n  const [activeTab, setActiveTab] = useState(CourseTaskStatus.Available);\n  const statuses = useMemo(() => tasks?.map(t => t.status) || [], [tasks]);\n  const filteredTasks = useMemo(() => tasks?.filter(t => t.status === activeTab) || [], [tasks, activeTab]);\n\n  return (\n    <PageLayout loading={false} title=\"Auto-tests\" withMargin={false} showCourseName>\n      <Row gutter={24} style={{ marginRight: 0, marginBottom: 24, padding: '0 16px' }}>\n        <Col span={24}>\n          <StatusTabs statuses={statuses} activeTab={activeTab} onTabChange={setActiveTab} />\n        </Col>\n      </Row>\n      <Row gutter={[24, 24]} style={{ padding: '0 16px', marginRight: 0 }}>\n        {filteredTasks.map(courseTask => (\n          <Col {...RESPONSIVE_COLUMNS} key={courseTask.id}>\n            <TaskCard courseTask={courseTask} course={course} />\n          </Col>\n        ))}\n      </Row>\n    </PageLayout>\n  );\n}\n\nexport default AutoTests;\n"
  },
  {
    "path": "client/src/modules/AutoTest/pages/Task/Task.tsx",
    "content": "import { useContext } from 'react';\nimport { CoursePageProps } from '@client/services/models';\nimport { CourseTaskDetailedDto } from '@client/api';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport {\n  AttemptsAnswers,\n  Exercise,\n  TaskDescription,\n  VerificationInformation,\n} from '@client/modules/AutoTest/components';\nimport { useCourseTaskVerifications, useVerificationsAnswers } from '@client/modules/AutoTest/hooks';\nimport { useRouter } from 'next/router';\nimport { theme } from 'antd';\n\nexport interface AutoTestTaskProps extends CoursePageProps {\n  task: CourseTaskDetailedDto;\n}\n\nfunction Task() {\n  const { course } = useActiveCourseContext();\n  const { githubId } = useContext(SessionContext);\n  const { query } = useRouter();\n\n  const courseTaskId = Number(query.courseTaskId);\n\n  const { loading, tasks, isExerciseVisible, startTask, finishTask, reload } = useCourseTaskVerifications(course.id);\n  const { answers, showAnswers, hideAnswers } = useVerificationsAnswers(course.id, courseTaskId);\n  const courseTask = tasks?.find(t => t.id === courseTaskId);\n\n  const { token } = theme.useToken();\n\n  if (!courseTask) {\n    return null;\n  }\n\n  return (\n    <PageLayout loading={false} title=\"Auto-tests\" withMargin={false} showCourseName background={token.colorBgLayout}>\n      <TaskDescription courseAlias={course.alias} courseTask={courseTask} />\n      <div style={{ margin: 16 }}>\n        {!answers ? (\n          <VerificationInformation\n            courseTask={courseTask}\n            loading={loading}\n            isTableVisible={!isExerciseVisible}\n            startTask={startTask}\n            reload={reload}\n            showAnswers={showAnswers}\n          />\n        ) : (\n          <AttemptsAnswers attempts={answers} hideAnswers={hideAnswers} />\n        )}\n        {isExerciseVisible && (\n          <Exercise courseId={course.id} courseTask={courseTask} githubId={githubId} finishTask={finishTask} />\n        )}\n      </div>\n    </PageLayout>\n  );\n}\n\nexport default Task;\n"
  },
  {
    "path": "client/src/modules/AutoTest/pages/index.tsx",
    "content": "export { default as AutoTests } from './AutoTests/AutoTests';\nexport { default as Task, type AutoTestTaskProps } from './Task/Task';\n"
  },
  {
    "path": "client/src/modules/AutoTest/types.ts",
    "content": "import { CourseTaskDetailedDto } from '@client/api';\nimport { SelfEducationPublicAttributes, Verification } from '@client/services/course';\n\nexport enum CourseTaskStatus {\n  Available = 'Available',\n  Missed = 'Missed',\n  Done = 'Done',\n}\n\nexport const COURSE_TASK_STATUSES = Object.entries(CourseTaskStatus).map(([key, value]) => ({ key, value }));\n\nexport enum CourseTaskState {\n  Uncompleted = 'Uncompleted',\n  Missed = 'Missed',\n  Completed = 'Completed',\n}\n\nexport interface CourseTaskVerifications extends CourseTaskDetailedDto {\n  publicAttributes: SelfEducationPublicAttributes;\n  status: CourseTaskStatus;\n  state: CourseTaskState;\n  verifications: Verification[];\n}\n"
  },
  {
    "path": "client/src/modules/AutoTest/utils/map.ts",
    "content": "import { CourseTaskDetailedDto } from '@client/api';\nimport dayjs from 'dayjs';\nimport { SelfEducationPublicAttributes, Verification } from '@client/services/course';\nimport { CourseTaskState, CourseTaskStatus, CourseTaskVerifications } from '../types';\n\nfunction getState(courseTask: CourseTaskDetailedDto, verifications: Verification[]): CourseTaskState {\n  const now = dayjs();\n  const end = dayjs(courseTask.studentEndDate);\n  const attemptsCount = verifications?.length || 0;\n\n  const publicAttributes = courseTask.publicAttributes as SelfEducationPublicAttributes;\n  const isScorePassed = verifications?.some(v => v.score >= publicAttributes.tresholdPercentage) ?? false;\n\n  if (isScorePassed) {\n    return CourseTaskState.Completed;\n  }\n\n  if (now.isAfter(end) && !attemptsCount) {\n    return CourseTaskState.Missed;\n  }\n\n  return CourseTaskState.Uncompleted;\n}\n\nfunction getStatus(\n  { studentEndDate, maxScore, publicAttributes }: CourseTaskDetailedDto,\n  verifications: Verification[],\n): CourseTaskStatus {\n  const attemptsCount = verifications?.length || 0;\n  const now = dayjs();\n  const end = dayjs(studentEndDate);\n\n  const publicAttr = publicAttributes as SelfEducationPublicAttributes;\n  const isMaxAttemptsCount = attemptsCount >= publicAttr.maxAttemptsNumber;\n\n  if (now.isAfter(end) && !attemptsCount) {\n    return CourseTaskStatus.Missed;\n  }\n\n  if (publicAttr.strictAttemptsMode && isMaxAttemptsCount) {\n    return CourseTaskStatus.Done;\n  }\n\n  if (maxScore === verifications[0]?.score || (now.isAfter(end) && attemptsCount)) {\n    return CourseTaskStatus.Done;\n  }\n\n  if (isMaxAttemptsCount && maxScore / 2 === verifications[0]?.score) {\n    return CourseTaskStatus.Done;\n  }\n\n  return CourseTaskStatus.Available;\n}\n\n// TODO: refactor nestjs models to return CourseTaskVerifications from server\nexport function mapTo(courseTask: CourseTaskDetailedDto, verifications: Verification[]): CourseTaskVerifications {\n  const taskVerifications = verifications.filter(v => v.courseTaskId === courseTask.id);\n\n  return {\n    ...courseTask,\n    state: getState(courseTask, taskVerifications),\n    status: getStatus(courseTask, taskVerifications),\n    publicAttributes: courseTask.publicAttributes as SelfEducationPublicAttributes,\n    verifications: taskVerifications,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/Contributor/components/ContributorModal.tsx",
    "content": "import { Form, Input, message, Modal, Spin } from 'antd';\nimport { ContributorDto, ContributorsApi, UsersApi } from '@client/api';\nimport useRequest from 'ahooks/lib/useRequest';\nimport { UserSearch } from '@client/shared/components/UserSearch';\n\ntype Props = {\n  contributorId: number | null;\n  onClose: () => void;\n};\nconst api = new ContributorsApi();\nconst usersApi = new UsersApi();\n\ntype FormData = {\n  user: { id: number };\n  description: string;\n};\n\nexport function ContributorModal(props: Props) {\n  const [form] = Form.useForm<FormData>();\n\n  const initial = useRequest<Partial<ContributorDto>, []>(async () => {\n    if (!props.contributorId) {\n      return {};\n    }\n    const response = await api.getContributor(props.contributorId);\n    return response.data;\n  });\n\n  const submitForm = async () => {\n    try {\n      const value = await form.validateFields();\n      const record = {\n        description: value.description,\n        userId: value.user.id,\n      };\n      if (props.contributorId) {\n        await api.updateContributor(props.contributorId, record);\n      } else {\n        await api.createContributor(record);\n      }\n      props.onClose();\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    }\n  };\n\n  const loadUsers = async (searchText: string) => {\n    const { data } = await usersApi.searchUsers(searchText);\n    return data;\n  };\n\n  const user = initial.data?.user\n    ? { ...initial.data?.user, name: `${initial.data.user.firstName} ${initial.data.user.lastName}` }\n    : null;\n\n  return (\n    <Modal\n      title={props.contributorId ? 'Edit Contributor' : 'Add Contributor'}\n      open={true}\n      onCancel={props.onClose}\n      onOk={submitForm}\n      okButtonProps={{ loading: initial.loading, disabled: initial.loading }}\n      okText=\"Save\"\n    >\n      {initial.loading ? <Spin spinning /> : null}\n      {initial.data ? (\n        <Form layout=\"vertical\" form={form} initialValues={initial.data}>\n          <Form.Item name={['user', 'id']} label=\"User\" rules={[{ required: true }]}>\n            <UserSearch defaultValues={user ? [user] : []} searchFn={loadUsers} />\n          </Form.Item>\n          <Form.Item name=\"description\" label=\"Description\" rules={[{ required: true }]}>\n            <Input.TextArea />\n          </Form.Item>\n        </Form>\n      ) : null}\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Contributor/components/ContributorsTable.tsx",
    "content": "import { Button, Space, Table } from 'antd';\nimport { ContributorDto } from '@client/api';\nimport { DeleteOutlined, EditOutlined } from '@ant-design/icons';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\n\nconst { Column } = Table;\n\ntype Props = {\n  data: ContributorDto[];\n  handleUpdate: (record: ContributorDto) => void;\n  handleDelete: (record: ContributorDto) => Promise<void>;\n};\n\nexport const ContributorsTable = ({ data: disciplines, handleDelete, handleUpdate }: Props) => {\n  return (\n    <Table dataSource={disciplines} rowKey=\"id\">\n      <Column\n        title=\"User\"\n        dataIndex={['user', 'githubId']}\n        render={(value: string) => {\n          return (\n            <>\n              <GithubAvatar size={24} githubId={value} /> {value}\n            </>\n          );\n        }}\n      />\n      <Column title=\"Description\" dataIndex=\"description\" />\n      <Column\n        title=\"Actions\"\n        key=\"action\"\n        width={100}\n        render={record => (\n          <Space size=\"middle\">\n            <Button key={'edit'} onClick={() => handleUpdate(record)} size=\"small\">\n              <EditOutlined size={8} />\n            </Button>\n            <Button key={'delete'} onClick={() => handleDelete(record)} size=\"small\" danger>\n              <DeleteOutlined size={8} />\n            </Button>\n          </Space>\n        )}\n      />\n    </Table>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Contributor/pages/ContributorPage.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { Button, Layout } from 'antd';\nimport { ContributorDto, ContributorsApi } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { ContributorModal } from '../components/ContributorModal';\nimport { ContributorsTable } from '../components/ContributorsTable';\nimport { useState } from 'react';\n\nconst api = new ContributorsApi();\n\nexport const ContributorPage = () => {\n  const { courses } = useActiveCourseContext();\n  const [modalId, setModalId] = useState<number | null | undefined>();\n\n  const response = useRequest(async () => {\n    const { data } = await api.getContributors();\n    return data;\n  });\n\n  const handleDelete = async (record: ContributorDto) => {\n    await api.deleteContributor(record.id);\n    response.run();\n  };\n\n  const handleUpdate = async (record: ContributorDto) => {\n    setModalId(record.id);\n  };\n\n  const handleClose = () => {\n    setModalId(undefined);\n    response.run();\n  };\n\n  return (\n    <AdminPageLayout title=\"Manage Contributors\" loading={response.loading} courses={courses}>\n      <Layout.Content style={{ margin: 8 }}>\n        <Button type=\"primary\" style={{ marginBottom: '25px' }} onClick={() => setModalId(null)}>\n          Add Contributor\n        </Button>\n        <ContributorsTable data={response.data ?? []} handleUpdate={handleUpdate} handleDelete={handleDelete} />\n      </Layout.Content>\n      {modalId !== undefined ? <ContributorModal onClose={handleClose} contributorId={modalId} /> : null}\n    </AdminPageLayout>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Course/components/CourseNoAccess.tsx",
    "content": "import { Button, Col, Result, Row } from 'antd';\n\nexport function CourseNoAccess() {\n  return (\n    <Row justify=\"center\">\n      <Col md={12} xs={18} style={{ marginTop: '60px' }}>\n        <Result\n          status=\"403\"\n          title=\"You Have No Access to Course Page\"\n          subTitle=\"Probably you do not participate in the course. Please register or choose another course.\"\n          extra={\n            <Button type=\"primary\" href=\"/\">\n              Go Home\n            </Button>\n          }\n        />\n      </Col>\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Course/components/NoSubmissionAvailable/index.tsx",
    "content": "import { Typography } from 'antd';\nimport Link from 'next/link';\n\nexport function NoSubmissionAvailable({ courseAlias }: { courseAlias: string }) {\n  return (\n    <>\n      <Typography.Title level={4}>No tasks available for submission now</Typography.Title>\n      Check start dates in <Link href={`/course/schedule?course=${courseAlias}`}>Schedule</Link>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Course/contexts/ActiveCourseContext.tsx",
    "content": "import { ProfileCourseDto } from '@client/api';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport { useRouter } from 'next/router';\nimport React, { useCallback, useContext, useMemo, useState } from 'react';\nimport { useLocalStorage } from 'react-use';\nimport { UserService } from '@client/services/user';\nimport { WelcomeCard } from '@client/components/WelcomeCard';\nimport { Alert, Col, notification, Row } from 'antd';\nimport useRequest from 'ahooks/lib/useRequest';\nimport { AxiosError } from 'axios';\n\ntype ActiveCourseContextType = {\n  course: ProfileCourseDto;\n  courses: ProfileCourseDto[];\n  setCourse: (course: ProfileCourseDto) => void;\n  refresh: () => void;\n};\n\nconst ActiveCourseContext = React.createContext<ActiveCourseContextType>({\n  course: {} as ProfileCourseDto,\n  courses: [],\n  setCourse: () => {},\n  refresh: () => {},\n});\n\nexport const useActiveCourseContext = () => {\n  return useContext(ActiveCourseContext);\n};\n\ntype Props = React.PropsWithChildren<{\n  publicRoutes: string[];\n}>;\n\nexport const ActiveCourseProvider = ({ children, publicRoutes }: Props) => {\n  const router = useRouter();\n\n  // course alias\n  const alias = router.query.course;\n  const isPublicRoute = publicRoutes?.includes(router.pathname);\n\n  const [storageCourseId, setStorageCourseId] = useLocalStorage<string>('activeCourseId');\n  const [activeCourse, setActiveCourse] = useState<ProfileCourseDto>();\n\n  const { data, loading, refresh } = useRequest(() => resolveCourse(alias, storageCourseId), {\n    ready: router.isReady && !isPublicRoute,\n    onSuccess: ([course]) => setCourse(course),\n    onError: error => {\n      const { pathname, search } = document.location;\n      const redirectUrl = encodeURIComponent(`${pathname}${search}`);\n      router.push('/login', { pathname: '/login', query: { url: redirectUrl } });\n\n      if ((error as AxiosError).status !== 401) {\n        notification.error({\n          message: 'Error occurred during login',\n          description: 'Please try again later or contact course manager',\n        });\n      }\n    },\n  });\n\n  const setCourse = useCallback((course: ProfileCourseDto | null) => {\n    if (course) {\n      setActiveCourse(course);\n      setStorageCourseId(course.id.toString());\n    }\n  }, []);\n\n  const value = useMemo(\n    () => (data && activeCourse ? { course: activeCourse, courses: data?.[1] ?? [], setCourse, refresh } : undefined),\n    [activeCourse, data, setCourse, refresh],\n  );\n\n  if (isPublicRoute && router.isReady) {\n    return <>{children}</>;\n  }\n\n  if (alias && activeCourse && activeCourse.alias !== alias) {\n    return (\n      <Row justify=\"center\">\n        <Col md={12} xs={18} style={{ marginTop: '60px' }}>\n          <Alert\n            message=\"No Access\"\n            description=\"Probably you do not participate in the course. Please register or choose another course.\"\n            type=\"error\"\n          />\n        </Col>\n      </Row>\n    );\n  }\n\n  if (data && data[0] === null) {\n    return <WelcomeCard />;\n  }\n\n  if (value) {\n    return <ActiveCourseContext.Provider value={value}>{children}</ActiveCourseContext.Provider>;\n  }\n\n  return <LoadingScreen show={loading} />;\n};\n\nasync function resolveCourse(\n  alias: string | string[] | undefined,\n  storageCourseId?: string,\n): Promise<[ProfileCourseDto | null, ProfileCourseDto[]]> {\n  const courses = await new UserService().getCourses();\n\n  const course =\n    courses.find(course => course.alias === alias) ??\n    courses.find(course => String(course.id) === String(storageCourseId)) ??\n    courses[0] ??\n    null;\n  return [course, courses] as const;\n}\n"
  },
  {
    "path": "client/src/modules/Course/contexts/SessionContext.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { SessionProvider } from './';\nimport Router from 'next/router';\nimport { useActiveCourseContext } from './ActiveCourseContext';\nimport useRequest from 'ahooks/lib/useRequest';\n\nvi.mock('next/router', () => ({ default: { push: vi.fn() }, push: vi.fn() }));\nvi.mock('./ActiveCourseContext', () => ({\n  useActiveCourseContext: vi.fn(),\n}));\n\nvi.mock('ahooks/lib/useRequest');\n\ndescribe('<SessionProvider />', () => {\n  const mockChildren = <div>Child Component</div>;\n\n  const mockSession = { isAdmin: true, courses: { 1: { roles: ['student'] } } };\n  const mockCourse = { id: 1 };\n  const mockActiveCourse = { course: mockCourse };\n\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  beforeEach(() => {\n    vi.mocked(useActiveCourseContext).mockReturnValue(mockActiveCourse);\n  });\n\n  it('should render loading screen', () => {\n    vi.mocked(useRequest).mockReturnValue({ loading: true });\n    render(<SessionProvider>{mockChildren}</SessionProvider>);\n    expect(screen.getByText(/loading/i)).toBeInTheDocument();\n  });\n\n  it('should handle error and redirect to login', () => {\n    vi.mocked(useRequest).mockReturnValue({ error: true });\n    render(<SessionProvider>{mockChildren}</SessionProvider>);\n    expect(Router.push).toHaveBeenCalledWith('/login', expect.anything());\n  });\n\n  it('should render children for admin user for admin-only pages', () => {\n    vi.mocked(useRequest).mockReturnValue({ data: mockSession });\n    render(<SessionProvider adminOnly={true}>{mockChildren}</SessionProvider>);\n    expect(screen.getByText('Child Component')).toBeInTheDocument();\n  });\n\n  it('should render warning for non-admin user for admin-only pages', () => {\n    vi.mocked(useRequest).mockReturnValue({ data: { ...mockSession, isAdmin: false } });\n    render(<SessionProvider adminOnly={true}>{mockChildren}</SessionProvider>);\n    expect(screen.getByText(/You don't have required role to access this page/)).toBeInTheDocument();\n  });\n\n  it('should render children for user with allowed roles', () => {\n    vi.mocked(useRequest).mockReturnValue({ data: mockSession });\n    render(<SessionProvider allowedRoles={['student']}>{mockChildren}</SessionProvider>);\n    expect(screen.getByText('Child Component')).toBeInTheDocument();\n  });\n\n  it('should render warning for user without allowed roles', () => {\n    vi.mocked(useRequest).mockReturnValue({ data: { ...mockSession, isAdmin: false } });\n    render(<SessionProvider allowedRoles={['mentor']}>{mockChildren}</SessionProvider>);\n    expect(screen.getByText(/You don't have required role to access this page/)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Course/contexts/SessionContext.tsx",
    "content": "import useRequest from 'ahooks/lib/useRequest';\nimport { Button, Result } from 'antd';\nimport { type ProfileCourseDto, SessionApi } from '@client/api';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport type { Session } from '@client/components/withSession';\nimport { hasRoleInAnyCourse } from '@client/domain/user';\nimport Router from 'next/router';\nimport React, { useEffect } from 'react';\nimport { CourseRole } from '@client/services/models';\nimport { useActiveCourseContext } from './ActiveCourseContext';\n\nexport const SessionContext = React.createContext<Session>({} as Session);\n\ntype Props = React.PropsWithChildren<{\n  allowedRoles?: CourseRole[];\n  course?: ProfileCourseDto;\n  adminOnly?: boolean;\n  anyCoursePowerUser?: boolean;\n  hirerOnly?: boolean;\n}>;\n\nexport const AccessDeniedWarning = () => (\n  <Result\n    status=\"warning\"\n    title=\"You don't have required role to access this page\"\n    extra={\n      <Button type=\"primary\" key=\"console\" onClick={() => window.history.back()}>\n        Go Back\n      </Button>\n    }\n  />\n);\n\nconst sessionApi = new SessionApi();\n\nexport function SessionProvider(props: Props) {\n  const { allowedRoles, anyCoursePowerUser } = props;\n  const activeCourse = useActiveCourseContext().course;\n  const course = props.course ?? activeCourse;\n\n  const {\n    data: session,\n    loading,\n    error,\n  } = useRequest(\n    async () => {\n      const response = await sessionApi.getSession();\n      return response.data;\n    },\n    {\n      cacheKey: 'session',\n      staleTime: 1000 * 60 * 15,\n    },\n  );\n\n  useEffect(() => {\n    if (!error) {\n      return;\n    }\n    const { pathname, search } = document.location;\n    const redirectUrl = encodeURIComponent(`${pathname}${search}`);\n    Router.push('/login', { pathname: '/login', query: { url: redirectUrl } });\n  }, [error]);\n\n  if (session && props.adminOnly && !session.isAdmin) {\n    return <AccessDeniedWarning />;\n  }\n\n  if (session && props.hirerOnly && !session.isHirer && !session.isAdmin) {\n    return <AccessDeniedWarning />;\n  }\n\n  if (session && allowedRoles && course) {\n    const { courses, isAdmin } = session;\n    const id = course.id;\n\n    if (!isAdmin) {\n      const roles = courses?.[id]?.roles ?? [];\n      const hasRoleInCurrentCourse = allowedRoles.some(role => roles.includes(role));\n      const isAnyCoursePowerUser = anyCoursePowerUser && allowedRoles.some(role => hasRoleInAnyCourse(session, role));\n\n      if (!hasRoleInCurrentCourse && !isAnyCoursePowerUser) {\n        return <AccessDeniedWarning />;\n      }\n    }\n  }\n  if (session) {\n    return <SessionContext.Provider value={session}>{props.children}</SessionContext.Provider>;\n  }\n  return <LoadingScreen show={loading} />;\n}\n"
  },
  {
    "path": "client/src/modules/Course/contexts/index.ts",
    "content": "export * from './SessionContext';\nexport * from './ActiveCourseContext';\n"
  },
  {
    "path": "client/src/modules/Course/pages/CouseNoAccess/index.tsx",
    "content": "import { CourseNoAccess } from '../../components/CourseNoAccess';\n\nexport function CouseNoAccessPage() {\n  return <CourseNoAccess />;\n}\n"
  },
  {
    "path": "client/src/modules/Course/pages/Student/CrossCheckSubmit/index.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { Alert, Button, Checkbox, Col, Form, Input, Modal, Result, Row } from 'antd';\nimport { CheckboxChangeEvent } from 'antd/lib/checkbox';\nimport { Rule } from 'antd/lib/form';\nimport {\n  CoursesTasksApi,\n  CrossCheckFeedbackDto,\n  CrossCheckMessageDtoRoleEnum,\n  CrossCheckStatusEnum,\n} from '@client/api';\nimport { CourseTaskSelect, ScoreInput } from '@client/shared/components/Forms';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { NoSubmissionAvailable } from '@client/modules/Course/components/NoSubmissionAvailable';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CriteriaForm } from '@client/modules/CrossCheck/components/CriteriaForm';\nimport { SolutionReview } from '@client/modules/CrossCheck/components/SolutionReview';\nimport { SolutionReviewSettingsPanel } from '@client/modules/CrossCheck/components/SolutionReviewSettingsPanel';\nimport { SubmittedStatus } from '@client/modules/CrossCheck/components/SubmittedStatus';\nimport { useSolutionReviewSettings } from '@client/modules/CrossCheck/hooks';\nimport { useRouter } from 'next/router';\nimport { useContext, useEffect, useMemo, useState } from 'react';\nimport {\n  CourseService,\n  CrossCheckComment,\n  CrossCheckCriteria,\n  CrossCheckReview,\n  TaskSolution,\n} from '@client/services/course';\nimport { githubPrUrl, privateRsRepoPattern, urlWithIpPattern } from '@client/services/validators';\nimport { getQueryString } from '@client/shared/utils/queryParams-utils';\nimport { useMessage } from '@client/hooks';\n\nconst colSizes = { xs: 24, sm: 18, md: 12, lg: 10 };\n\nconst createGithubInUrlRule = (githubId: string): Rule => {\n  return {\n    message: 'Your GitHub Username should be in the URL',\n    required: true,\n    pattern: new RegExp(`${githubId}`, 'i'),\n  };\n};\n\nconst validUrlRule: Rule = {\n  required: true,\n  pattern: urlWithIpPattern,\n  message: 'Please provide a valid link (must start with `http://` or `https://`)',\n};\n\nconst githubPrInUrlRule: Rule = {\n  required: true,\n  pattern: githubPrUrl,\n  message: 'Link should be a valid GitHub Pull Request URL',\n};\n\nconst notPrivateRsRepoRule: Rule = {\n  validator: (_, value) => {\n    if (privateRsRepoPattern.test(value)) {\n      return Promise.reject(\"Please provide another link. Students can't see Pull Requests of private RS School repos\");\n    }\n    return Promise.resolve();\n  },\n};\n\nexport function CrossCheckSubmit() {\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const [form] = Form.useForm();\n  const courseService = useMemo(() => new CourseService(course.id), [course.id]);\n  const teamDistributionApi = useMemo(() => new CoursesTasksApi(), []);\n  const solutionReviewSettings = useSolutionReviewSettings();\n  const [feedback, setFeedback] = useState<CrossCheckFeedbackDto | null>(null);\n  const [submittedSolution, setSubmittedSolution] = useState(null as TaskSolution | null);\n  const router = useRouter();\n  const queryTaskId = router.query.taskId ? +router.query.taskId : null;\n  const [courseTaskId, setCourseTaskId] = useState(queryTaskId);\n  const [criteria, setCriteria] = useState([] as CrossCheckCriteria[]);\n  const [comments, setComments] = useState([] as CrossCheckComment[]);\n  const [newComments, setNewComments] = useState([] as CrossCheckComment[]);\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const [buttonDisabled, setButtonDisabled] = useState(true);\n  const [submitDeadlinePassed, setSubmitDeadlinePassed] = useState<boolean>(false);\n  const { message } = useMessage();\n\n  const [authorId, setAuthorId] = useState<number | null>(null);\n\n  const { data: courseTasks = [], loading } = useRequest(() => courseService.getCourseCrossCheckTasks('started'), {\n    refreshDeps: [course.id],\n  });\n\n  useEffect(() => {\n    if (loading) return;\n\n    if (queryTaskId) {\n      handleTaskChange(queryTaskId);\n    }\n  }, [loading, queryTaskId]);\n\n  const handleSubmit = async (values: { url: string; review: CrossCheckReview[] }) => {\n    if (!courseTaskId) {\n      return;\n    }\n    try {\n      await courseService.postTaskSolution(session.githubId, courseTaskId, values.url, values.review, newComments);\n      message.success('The task solution has been submitted');\n      form.resetFields();\n      setComments([]);\n      setCourseTaskId(null);\n    } catch {\n      message.error('An error occured. Please try later.');\n    }\n  };\n\n  const handleCancellation = async () => {\n    if (!courseTaskId) {\n      return;\n    }\n\n    try {\n      await courseService.deleteTaskSolution(session.githubId, courseTaskId);\n      message.success('The task submission has been canceled');\n      setIsModalVisible(false);\n      form.resetFields();\n      setSubmittedSolution(null);\n      setCourseTaskId(null);\n    } catch {\n      message.error('An error occurred. Please try later.');\n    }\n\n    setButtonDisabled(true);\n  };\n\n  const showModal = () => {\n    setIsModalVisible(true);\n  };\n\n  const cancelModal = () => {\n    setIsModalVisible(false);\n    setButtonDisabled(true);\n  };\n\n  const cancellationChange = (e: CheckboxChangeEvent) => setButtonDisabled(!e.target.checked);\n\n  function selectTask(value: number) {\n    const query = { ...router.query, taskId: value };\n    const url = `${router.route}${getQueryString(query)}`;\n    router.replace(url);\n  }\n\n  const handleTaskChange = async (value: number) => {\n    setFeedback(null);\n    const courseTaskId = Number(value);\n    const courseTask = courseTasks.find(t => t.id === courseTaskId);\n    if (courseTask == null) {\n      return;\n    }\n\n    const [{ data: feedback }, submittedSolution, taskDetails] = await Promise.all([\n      teamDistributionApi.getMyCrossCheckFeedbacks(course.id, courseTask.id),\n      courseService.getCrossCheckTaskSolution(session.githubId, courseTask.id).catch(() => null),\n      courseService.getCrossCheckTaskDetails(courseTask.id),\n    ]);\n\n    const review = submittedSolution?.review ?? [];\n    const criteria = taskDetails?.criteria ?? [];\n    const endDate = taskDetails?.studentEndDate ? new Date(taskDetails.studentEndDate) : null;\n    const submitDeadlinePassed = Date.now() > (endDate ? endDate.getTime() : 0);\n\n    form.setFieldsValue({ review });\n    form.setFieldsValue({ score: calculateFinalScore(review, criteria) });\n    form.setFieldsValue({ url: submittedSolution?.url });\n\n    setFeedback(feedback);\n    setSubmittedSolution(submittedSolution);\n    setCourseTaskId(courseTask.id);\n    setCriteria(criteria);\n    setSubmitDeadlinePassed(submitDeadlinePassed);\n    setComments(submittedSolution?.comments ?? []);\n    setAuthorId(submittedSolution?.studentId ?? null);\n  };\n\n  const handleReviewChange = (review: CrossCheckReview[], comments: CrossCheckComment[]) => {\n    form.setFieldsValue({ score: calculateFinalScore(review, criteria) });\n    setNewComments(comments);\n  };\n\n  const task = courseTasks.find(task => task.id === courseTaskId);\n  const maxScore = task?.maxScore;\n  const taskExists = !!task;\n  const submitAllowed = taskExists && !submitDeadlinePassed;\n  const newCrossCheck = criteria.length > 0;\n  const isCrossCheckCompleted = task?.crossCheckStatus === CrossCheckStatusEnum.Completed;\n  const isCrossCheckOngoing = task?.crossCheckStatus === CrossCheckStatusEnum.Distributed;\n  const hasReviews = !!feedback?.reviews?.length;\n\n  return (\n    <PageLayout loading={loading} title=\"Cross-Check Submit\" showCourseName>\n      <Row gutter={24}>\n        <Col {...colSizes}>\n          <Form form={form} onFinish={handleSubmit} layout=\"vertical\">\n            {courseTasks.length > 0 && (\n              <CourseTaskSelect\n                data={courseTasks}\n                groupBy=\"deadline\"\n                onChange={selectTask}\n                defaultValue={courseTaskId}\n              />\n            )}\n            {courseTasks.length === 0 && !loading && <NoSubmissionAvailable courseAlias={course.alias} />}\n            <SubmittedStatus\n              taskExists={taskExists}\n              solution={submittedSolution}\n              deadlinePassed={submitDeadlinePassed}\n            />\n            {submitAllowed && (\n              <>\n                <Form.Item\n                  name=\"url\"\n                  label=\"Solution URL\"\n                  rules={[\n                    validUrlRule,\n                    notPrivateRsRepoRule,\n                    ...(task.validations?.githubIdInUrl ? [createGithubInUrlRule(session.githubId)] : []),\n                    ...(task.validations?.githubPrInUrl ? [githubPrInUrlRule] : []),\n                  ]}\n                >\n                  <Input placeholder=\"link in the form of https://www.google.com\" />\n                </Form.Item>\n                {task.submitText ? <Alert showIcon message={task.submitText} /> : null}\n              </>\n            )}\n            {submitAllowed && newCrossCheck && (\n              <Form.Item name=\"review\">\n                <CriteriaForm\n                  authorId={authorId ?? 0}\n                  onChange={handleReviewChange}\n                  criteria={criteria}\n                  comments={comments ?? []}\n                  reviewComments={newComments ?? []}\n                />\n              </Form.Item>\n            )}\n            {submitAllowed && newCrossCheck && <ScoreInput courseTask={task} />}\n            {submitAllowed && (\n              <Row style={{ marginTop: 16 }}>\n                <Col span={12}>\n                  <Button type=\"primary\" htmlType=\"submit\">\n                    Submit\n                  </Button>\n                </Col>\n                {submittedSolution && (\n                  <Col span={12} style={{ display: 'flex', justifyContent: 'flex-end' }}>\n                    <Button danger ghost onClick={showModal}>\n                      Cancel Submit\n                    </Button>\n                    <Modal\n                      title=\"Cancel submission\"\n                      open={isModalVisible}\n                      onOk={handleCancellation}\n                      onCancel={cancelModal}\n                      okButtonProps={{ disabled: buttonDisabled }}\n                    >\n                      <Checkbox checked={!buttonDisabled} onChange={cancellationChange}>\n                        Being of sound mind and body, do hereby declare that I want to cancel my submission\n                      </Checkbox>\n                    </Modal>\n                  </Col>\n                )}\n              </Row>\n            )}\n          </Form>\n        </Col>\n      </Row>\n\n      {submittedSolution && !hasReviews && (isCrossCheckCompleted || isCrossCheckOngoing) && (\n        <Row gutter={24}>\n          <Col {...colSizes}>\n            <Result\n              title={isCrossCheckCompleted ? 'No one has checked your work.' : 'No one has checked your work yet.'}\n              status=\"404\"\n              extra={\n                isCrossCheckCompleted && (\n                  <Button type=\"link\" target=\"_blank\" href=\"https://docs.rs.school/#/en/cross-check-flow?id=appeal\">\n                    Check if you are eligible to appeal here.\n                  </Button>\n                )\n              }\n            />\n          </Col>\n        </Row>\n      )}\n      {hasReviews && (\n        <Row style={{ margin: '8px 0' }}>\n          <Col>\n            <SolutionReviewSettingsPanel settings={solutionReviewSettings} />\n          </Col>\n        </Row>\n      )}\n\n      {feedback?.reviews?.map((review, index) => (\n        <Row key={index}>\n          <Col span={24}>\n            <SolutionReview\n              sessionId={session.id}\n              sessionGithubId={session.githubId}\n              courseId={course.id}\n              reviewNumber={index}\n              settings={solutionReviewSettings}\n              courseTaskId={courseTaskId}\n              review={review}\n              isActiveReview={true}\n              currentRole={CrossCheckMessageDtoRoleEnum.Student}\n              maxScore={maxScore}\n            />\n          </Col>\n        </Row>\n      ))}\n    </PageLayout>\n  );\n}\n\nfunction calculateFinalScore(\n  review: { percentage: number; criteriaId: string }[],\n  criteria: CrossCheckCriteria[] = [],\n) {\n  return review.reduce((acc, r) => {\n    const max = criteria.find(c => c.criteriaId === r.criteriaId)?.max ?? 0;\n    return acc + Math.round(max * r.percentage);\n  }, 0);\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/CertificateCriteriaModal/CertificateCriteriaModal.test.tsx",
    "content": "import { render, screen, fireEvent } from '@testing-library/react';\nimport {\n  CERTIFICATE_ALERT_MESSAGE,\n  CertificateCriteriaModal,\n  FormValues,\n  hasValidCriteria,\n} from './CertificateCriteriaModal';\nimport userEvent from '@testing-library/user-event';\nimport * as ReactUse from 'react-use';\n\nconst props = {\n  courseId: 1,\n  onSubmit: vi.fn(),\n  onClose: vi.fn(),\n  isModalOpen: true,\n};\n\nconst renderCertificateCriteriaModal = () => {\n  render(<CertificateCriteriaModal {...props} />);\n};\n\ndescribe('CertificateCriteriaModal', () => {\n  beforeAll(() => {\n    // mock CoursesTasksApi call\n    vi.spyOn(ReactUse, 'useAsync').mockReturnValue({\n      value: [\n        {\n          name: 'course 1',\n          id: 1,\n        },\n      ],\n      loading: false,\n    });\n  });\n\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  const user = userEvent.setup();\n\n  test('should render modal title', async () => {\n    renderCertificateCriteriaModal();\n\n    const title = await screen.findByText('Certificate Criteria');\n    expect(title).toBeInTheDocument();\n  });\n\n  test('should render alert message', async () => {\n    renderCertificateCriteriaModal();\n\n    const alert = await screen.findByText(CERTIFICATE_ALERT_MESSAGE);\n    expect(alert).toBeInTheDocument();\n  });\n\n  test.each`\n    label\n    ${'Tasks'}\n    ${'Minimum Score Per Task'}\n    ${'Minimum Total Score'}\n  `('should render field with $label label', async ({ label }) => {\n    renderCertificateCriteriaModal();\n\n    const field = await screen.findByText(label);\n    expect(field).toBeInTheDocument();\n  });\n\n  test('should render \"cancel\" button', async () => {\n    renderCertificateCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /cancel/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  test('should render \"issue certificates\" button', async () => {\n    renderCertificateCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /issue certificates/i });\n    expect(button).toBeInTheDocument();\n    expect(button).toBeDisabled();\n  });\n\n  test('should enable \"issue certificates\" button on valid criteria', async () => {\n    renderCertificateCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /issue certificates/i });\n    expect(button).toBeDisabled();\n\n    const minTotalScoreInput = await screen.findByLabelText('Minimum Total Score');\n    fireEvent.change(minTotalScoreInput, {\n      target: {\n        value: 5,\n      },\n    });\n    expect(button).toBeEnabled();\n  });\n\n  test('should call \"onClose\" function on \"cancel\" button click', async () => {\n    renderCertificateCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /cancel/i });\n    await user.click(button);\n\n    expect(props.onClose).toHaveBeenCalled();\n  });\n\n  test('should call \"onSubmit\" function on \"issue certificates\" button click', async () => {\n    renderCertificateCriteriaModal();\n\n    // Enable \"issue certificates\" button\n    const minTotalScoreInput = await screen.findByLabelText('Minimum Total Score');\n    fireEvent.change(minTotalScoreInput, {\n      target: {\n        value: 5,\n      },\n    });\n\n    const button = await screen.findByRole('button', { name: /issue certificates/i });\n    await user.click(button);\n\n    expect(props.onSubmit).toHaveBeenCalled();\n  });\n});\n\ndescribe('hasValidCriteria', () => {\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  test('should return \"false\" on empty values', () => {\n    expect(hasValidCriteria({} as FormValues)).toBe(false);\n  });\n\n  describe('tasksCriteriaValid (with minTotalScore > 0)', () => {\n    test('should return \"true\" on empty courseTaskIds array', () => {\n      const values = {\n        minScore: 0,\n        courseTaskIds: [],\n        minTotalScore: 5,\n      };\n\n      expect(hasValidCriteria(values)).toBe(true);\n    });\n\n    test('should return \"true\" on not empty courseTaskIds array & minScore per task > 0', () => {\n      const values = {\n        minScore: 5,\n        courseTaskIds: [1, 2],\n        minTotalScore: 5,\n      };\n\n      expect(hasValidCriteria(values)).toBe(true);\n    });\n\n    test('should return \"false\" on not empty courseTaskIds array & minScore per task = 0', () => {\n      const values = {\n        minScore: 0,\n        courseTaskIds: [1, 2],\n        minTotalScore: 5,\n      };\n\n      expect(hasValidCriteria(values)).toBe(false);\n    });\n  });\n\n  describe('minTotalScore (with truthy tasksCriteriaValid)', () => {\n    test('should return \"false\" on minTotalScore = 0', () => {\n      const values = {\n        minScore: 5,\n        courseTaskIds: [1, 2],\n        minTotalScore: 0,\n      };\n\n      expect(hasValidCriteria(values)).toBe(false);\n    });\n\n    test('should return \"true\" on minTotalScore > 0', () => {\n      const values = {\n        minScore: 5,\n        courseTaskIds: [1, 2],\n        minTotalScore: 5,\n      };\n\n      expect(hasValidCriteria(values)).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/CertificateCriteriaModal/CertificateCriteriaModal.tsx",
    "content": "import { Alert, Button, Col, Form, InputNumber, Modal, Row, Space } from 'antd';\nimport { useState } from 'react';\nimport { SelectCourseTasks } from '@client/modules/CourseManagement/components';\n\nexport type FormValues = {\n  courseTaskIds: number[];\n  minScore: number;\n  minTotalScore: number;\n};\n\ntype Criteria = Partial<FormValues>;\n\ntype Props = {\n  courseId: number;\n  onSubmit: (criteria: Criteria) => void;\n  onClose: () => void;\n  isModalOpen: boolean;\n};\n\nexport const CERTIFICATE_ALERT_MESSAGE = 'Certificates will be issued to all students meeting the criteria down below.';\n\nexport function CertificateCriteriaModal({ courseId, onSubmit, onClose, isModalOpen }: Props) {\n  const [form] = Form.useForm<FormValues>();\n  const [okEnabled, setOkEnabled] = useState(false);\n\n  return (\n    <Modal\n      width={600}\n      title=\"Certificate Criteria\"\n      onCancel={onClose}\n      open={isModalOpen}\n      bodyStyle={{ paddingBlock: 16 }}\n      footer={null}\n    >\n      <Form\n        layout=\"vertical\"\n        form={form}\n        onValuesChange={(_, values) => {\n          setOkEnabled(hasValidCriteria(values));\n        }}\n        onFinish={onSubmit}\n      >\n        <Row gutter={[0, 16]}>\n          <Col span={24}>\n            <Alert message={CERTIFICATE_ALERT_MESSAGE} showIcon />\n          </Col>\n          <Col span={24}>\n            <SelectCourseTasks courseId={courseId} label=\"Tasks\" />\n          </Col>\n          <Col span={24}>\n            <Form.Item name=\"minScore\" label=\"Minimum Score Per Task\" style={{ marginBottom: 0 }}>\n              <InputNumber style={{ width: '100%' }} type=\"number\" min={0} placeholder=\"Enter minimum score\" />\n            </Form.Item>\n          </Col>\n          <Col span={24}>\n            <Form.Item name=\"minTotalScore\" label=\"Minimum Total Score\" style={{ marginBottom: 0 }}>\n              <InputNumber style={{ width: '100%' }} type=\"number\" min={0} placeholder=\"Enter minimum score\" />\n            </Form.Item>\n          </Col>\n          <Col span={24}>\n            <Row justify=\"end\">\n              <Space wrap>\n                <Button onClick={onClose}>Cancel</Button>\n                <Button type=\"primary\" htmlType=\"submit\" disabled={!okEnabled}>\n                  Issue Certificates\n                </Button>\n              </Space>\n            </Row>\n          </Col>\n        </Row>\n      </Form>\n    </Modal>\n  );\n}\n\nexport function hasValidCriteria({ minScore, courseTaskIds, minTotalScore }: FormValues) {\n  const tasksCriteriaValid = !courseTaskIds?.length || !!minScore;\n\n  return tasksCriteriaValid && !!minTotalScore;\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/CourseEventModal/formState.ts",
    "content": "import { message } from 'antd';\nimport { CreateCourseEventDto, CreateEventDto, EventDto, EventsApi } from '@client/api';\nimport { EVENT_TYPES } from '@client/data/eventTypes';\nimport omit from 'lodash/omit';\nimport { CourseEvent, CourseService } from '@client/services/course';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport timezone from 'dayjs/plugin/timezone';\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\nconst eventsApi = new EventsApi();\n\nconst createRecord = (eventTemplateId: number, values: any): CreateCourseEventDto => {\n  const record = {\n    eventId: eventTemplateId,\n    special: values.special ? values.special.join(',') : '',\n    dateTime: values.dateTime ? dayjs(values.dateTime).tz(values.timeZone, true).format() : undefined,\n    endTime: values.endTime ? dayjs(values.endTime).tz(values.timeZone, true).format() : undefined,\n    place: values.place || null,\n    organizer: values.taskOwner?.id ? { id: values.taskOwner?.id } : undefined,\n  };\n  return record;\n};\n\nconst submitTemplateEvent = async (values: any, eventTemplate?: EventDto) => {\n  const templateEventData = {\n    name: eventTemplate ? eventTemplate?.name : values.event,\n    type: values.type,\n    descriptionUrl: values.descriptionUrl,\n    description: values.description,\n    disciplineId: values.disciplineId,\n  } as CreateEventDto;\n\n  if (!eventTemplate) {\n    try {\n      const res = await eventsApi.createEvent(templateEventData);\n      return res.data.id;\n    } catch {\n      message.error('Failed to create event template. Please try later.');\n    }\n  } else {\n    try {\n      const res = await eventsApi.updateEvent(eventTemplate.id, templateEventData);\n      return res.data.id;\n    } catch {\n      message.error('Failed to update event template. Please try later.');\n    }\n  }\n};\n\nexport async function submitEvent(\n  values: any,\n  eventTemplates: EventDto[],\n  courseId: number,\n  editableRecord?: Partial<CourseEvent>,\n): Promise<void> {\n  const currentEventTemplate = editableRecord?.event ?? eventTemplates.find(el => el.id === +values.event);\n  const eventTemplateId = await submitTemplateEvent(values, currentEventTemplate as EventDto);\n  if (!eventTemplateId) return;\n  const serviceCourse = new CourseService(courseId);\n  const record = createRecord(eventTemplateId, values);\n  if (editableRecord?.id) {\n    try {\n      await serviceCourse.updateCourseEvent(editableRecord.id, omit(record, 'eventId'));\n    } catch {\n      message.error('Failed to update event. Please try later.');\n    }\n  } else {\n    try {\n      await serviceCourse.createCourseEvent(record);\n    } catch {\n      message.error('Failed to create event. Please try later.');\n    }\n  }\n}\n\nexport function getInitialValues(modalData: Partial<CourseEvent>) {\n  const timeZone = 'UTC';\n  return {\n    ...modalData,\n    type: EVENT_TYPES.find(event => event.id === modalData.event?.type)?.id ?? null,\n    disciplineId: modalData?.event?.discipline?.id || modalData?.event?.disciplineId,\n    descriptionUrl: modalData.event?.descriptionUrl ? modalData.event.descriptionUrl : '',\n    description: modalData.event?.description ? modalData.event.description : '',\n    dateTime: dayjs.utc(modalData.dateTime ?? undefined),\n    endTime: dayjs.utc(modalData.endTime ?? undefined),\n    taskOwner: modalData.organizer ? { id: modalData.organizer.id } : undefined,\n    special: modalData.special ? modalData.special.split(',') : [],\n    timeZone,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/CourseEventModal/index.tsx",
    "content": "import { Col, DatePicker, Form, Input, Row, Select, Typography } from 'antd';\nimport { DisciplinesApi, EventsApi } from '@client/api';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { TIMEZONES } from '@client/configs/timezones';\nimport { EVENT_TYPES } from '@client/data/eventTypes';\nimport { SPECIAL_ENTITY_TAGS } from '@client/modules/Schedule/constants';\nimport { useCallback } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseEvent } from '@client/services/course';\nimport { UserService } from '@client/services/user';\nimport { urlPattern } from '@client/services/validators';\nimport { getInitialValues, submitEvent } from './formState';\n\nconst { Option } = Select;\nconst { Title } = Typography;\nconst { TextArea } = Input;\n\ntype Props = {\n  data: Partial<CourseEvent>;\n  onCancel: () => void;\n  onSubmit: () => void;\n  courseId: number;\n};\n\nconst eventsApi = new EventsApi();\nconst disciplineApi = new DisciplinesApi();\nconst userService = new UserService();\n\nexport function CourseEventModal({ data, onCancel, courseId, onSubmit }: Props) {\n  const [form] = Form.useForm();\n\n  const loadUsers = (searchText: string) => {\n    return userService.searchUser(searchText);\n  };\n\n  const loadData = async () => {\n    const [{ data: events = [] }, { data: disciplines = [] }] = await Promise.all([\n      eventsApi.getEvents(),\n      disciplineApi.getDisciplines(),\n    ]);\n    return {\n      events,\n      disciplines,\n    };\n  };\n\n  const { loading, value: { events = [], disciplines = [] } = {} } = useAsync(loadData, []);\n  const filterOption = useCallback(\n    (input: string, option?: { value: string }): boolean => {\n      if (!input) {\n        return false;\n      }\n      const event = events.find(e => e.id === Number(option?.value));\n      return event?.name.toLowerCase().includes(input.toLowerCase()) ?? false;\n    },\n    [events],\n  );\n\n  const handleModalSubmit = async (values: any) => {\n    await submitEvent(values, events, courseId, data);\n    onSubmit();\n  };\n\n  const typesList = EVENT_TYPES;\n  const entityTypes = typesList.map(tag => {\n    return (\n      <Option key={tag.id} value={tag.id}>\n        {tag.name}\n      </Option>\n    );\n  });\n\n  const onEventChange = (value: any) => {\n    const currentEvent = value.at(-1);\n    form.setFieldValue('event', currentEvent);\n    const currentEventTemplate = events.find(el => el.id === +currentEvent && el.name !== currentEvent);\n    form.setFieldValue('description', currentEventTemplate?.description);\n    form.setFieldValue('descriptionUrl', currentEventTemplate?.descriptionUrl);\n    form.setFieldValue('type', currentEventTemplate?.type);\n  };\n\n  return (\n    <ModalForm\n      form={form}\n      loading={loading}\n      getInitialValues={getInitialValues}\n      data={data}\n      title=\"Course Event\"\n      submit={handleModalSubmit}\n      cancel={onCancel}\n    >\n      {data.event?.id ? (\n        <Title level={4}>{data.event.name}</Title>\n      ) : (\n        <Form.Item name=\"event\" label=\"Event\" rules={[{ required: true, message: 'Please select an event' }]}>\n          <Select\n            mode=\"tags\"\n            maxTagCount={1}\n            filterOption={filterOption}\n            showSearch\n            placeholder=\"Select an event\"\n            onChange={onEventChange}\n          >\n            {events.map(event => (\n              <Option key={event.id}>{event.name}</Option>\n            ))}\n          </Select>\n        </Form.Item>\n      )}\n\n      <Form.Item name=\"type\" label=\"Type\" rules={[{ required: true }]}>\n        <Select placeholder=\"Choose event type\">{entityTypes}</Select>\n      </Form.Item>\n      <Form.Item\n        required\n        name=\"disciplineId\"\n        label=\"Discipline\"\n        rules={[{ required: true, message: 'Please select a discipline' }]}\n      >\n        <Select placeholder=\"Select a discipline\">\n          {disciplines.map(({ id, name }) => (\n            <Select.Option key={id} value={id}>\n              {name}\n            </Select.Option>\n          ))}\n        </Select>\n      </Form.Item>\n      <Form.Item name=\"special\" label=\"Special\">\n        <Select placeholder=\"Add tags\" mode=\"tags\" style={{ minWidth: 100 }} tokenSeparators={[',']} allowClear>\n          {SPECIAL_ENTITY_TAGS.map((tag: string) => (\n            <Option key={tag} value={tag}>\n              {tag}\n            </Option>\n          ))}\n        </Select>\n      </Form.Item>\n      <Form.Item name=\"timeZone\" label=\"TimeZone\">\n        <Select placeholder=\"Choose a timezone\">\n          {TIMEZONES.map(tz => (\n            <Option key={tz} value={tz}>\n              {tz === 'Europe/Kiev' ? 'Europe/Kyiv' : tz}\n            </Option>\n          ))}\n        </Select>\n      </Form.Item>\n\n      <Row gutter={24}>\n        <Col span={12}>\n          <Form.Item\n            name=\"dateTime\"\n            label=\"Date and Time\"\n            rules={[{ required: true, message: 'Please enter start date and time' }]}\n          >\n            <DatePicker\n              format=\"YYYY-MM-DD HH:mm\"\n              showTime={{ format: 'HH:mm' }}\n              placeholder=\"Select start date & time\"\n            />\n          </Form.Item>\n        </Col>\n        <Col span={12}>\n          <Form.Item\n            name=\"endTime\"\n            label=\"End Date and Time\"\n            rules={[{ required: true, message: 'Please enter end date and time' }]}\n          >\n            <DatePicker format=\"YYYY-MM-DD HH:mm\" showTime={{ format: 'HH:mm' }} placeholder=\"Select end date & time\" />\n          </Form.Item>\n        </Col>\n      </Row>\n\n      <Form.Item\n        name=\"descriptionUrl\"\n        label=\"Description URL\"\n        rules={[\n          {\n            required: true,\n            message: 'Please enter description URL',\n          },\n          {\n            message: 'Please enter valid URL',\n            pattern: urlPattern,\n          },\n        ]}\n      >\n        <Input placeholder=\"Enter description URL\" />\n      </Form.Item>\n\n      <Form.Item name={['taskOwner', 'id']} label=\"Task Owner\">\n        <UserSearch\n          placeholder=\"Select a task owner\"\n          defaultValues={data?.organizer ? [data.organizer] : []}\n          searchFn={loadUsers}\n        />\n      </Form.Item>\n      <Form.Item name=\"description\" label=\"Description\">\n        <TextArea placeholder=\"Add a brief description\" />\n      </Form.Item>\n\n      <Form.Item name=\"place\" label=\"Place\">\n        <Input placeholder=\"Enter event location\" />\n      </Form.Item>\n    </ModalForm>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/CourseModal/index.tsx",
    "content": "import { QuestionCircleOutlined } from '@ant-design/icons';\nimport { rsAppRegistryUrlPattern, weAreCommunityUrlPattern } from '@client/services/validators';\nimport useRequest from 'ahooks/lib/useRequest';\nimport {\n  Checkbox,\n  Col,\n  DatePicker,\n  Flex,\n  Form,\n  Input,\n  InputNumber,\n  Modal,\n  Radio,\n  Row,\n  Select,\n  Spin,\n  Typography,\n} from 'antd';\nimport { Tooltip } from 'antd/lib';\n\nimport { CoursesApi, CreateCourseDto, DisciplineDto, IdNameDto, UpdateCourseDto } from '@client/api';\nimport { DEFAULT_COURSE_ICONS } from '@client/configs/course-icons';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport { Course } from '@client/services/models';\nimport { PublicSvgIcon } from '@client/shared/components/Icons';\n\ndayjs.extend(utc);\n\nconst rsAppStudentRegistryBaseURL = 'https://app.rs.school/registry/student?course=';\n\nconst buildRSAppStudentRegistryURL = (alias: string) => `${rsAppStudentRegistryBaseURL}${alias}`;\n\nconst weAreCommunityOrRsAppRegistryRegex = new RegExp(\n  `^(${weAreCommunityUrlPattern.source})|(${rsAppRegistryUrlPattern.source})$`,\n);\n\nconst courseApi = new CoursesApi();\nconst courseIcons = Object.entries(DEFAULT_COURSE_ICONS).map(([key, config]) => ({ ...config, id: key }));\n\ntype CourseModalProps = {\n  onClose: () => void;\n  discordServers: IdNameDto[];\n  disciplines: DisciplineDto[];\n  courses: Course[];\n  courseId: number | null;\n};\n\ntype FormData = {\n  state: 'active' | 'planned' | 'completed';\n  range: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null;\n  minStudentsPerMentor: number;\n  certificateThreshold: number;\n  inviteOnly: boolean;\n  registrationEndDate: dayjs.Dayjs | null;\n\n  name?: string;\n  fullName?: string;\n  alias?: string;\n  description?: string;\n  descriptionUrl?: string;\n  customDescriptionUrl?: string;\n  year?: number;\n  startDate?: string;\n  endDate?: string;\n  locationName?: string;\n  discordServerId?: number;\n  completed?: boolean;\n  primarySkillId?: string;\n\n  logo?: string;\n  usePrivateRepositories?: boolean;\n  personalMentoring?: boolean;\n  personalMentoringStartDate?: string | null;\n  personalMentoringEndDate?: string | null;\n  personalMentoringDateRange: [dayjs.Dayjs | null, dayjs.Dayjs | null] | null;\n  certificateIssuer?: string;\n  discipline?: { id: number } | null;\n  courseId?: number;\n  wearecommunityUrl?: string;\n  anyCertificate?: boolean;\n  certificateDisciplines?: number[] | null;\n};\n\nexport function CourseModal(props: CourseModalProps) {\n  const [form] = Form.useForm();\n\n  const response = useRequest(\n    async () => {\n      if (props.courseId == null) {\n        return getInitialValues({});\n      }\n      const response = await courseApi.getCourse(props.courseId);\n      return getInitialValues(response.data);\n    },\n    { refreshDeps: [props.courseId] },\n  );\n\n  const updateResponse = useRequest(\n    async (formData: FormData) => {\n      const record = createRecord(formData);\n      if (props.courseId) {\n        await courseApi.updateCourse(props.courseId, record);\n      } else {\n        if (formData.courseId) {\n          await courseApi.copyCourse(formData.courseId, record as CreateCourseDto);\n        } else {\n          await courseApi.createCourse(record as CreateCourseDto);\n        }\n      }\n      props.onClose();\n    },\n    { manual: true },\n  );\n\n  const descriptionUrl = Form.useWatch('descriptionUrl', form);\n  const alias: string = Form.useWatch('alias', form);\n  const weAreCommunityUrl = Form.useWatch('wearecommunityUrl', form);\n  const anyCertificateChecked = Form.useWatch('anyCertificate', form);\n\n  const certificateOptions = [\n    ...props.disciplines.map(({ id, name }) => ({\n      value: id,\n      label: name,\n    })),\n  ];\n\n  const validateWeAreCommunityUrl = () => {\n    return (_: any, value: string) => {\n      if (!value) return Promise.resolve();\n\n      if (!weAreCommunityOrRsAppRegistryRegex.test(value)) {\n        return Promise.reject('Please enter RS APP or wearecommunity.io URL');\n      }\n\n      const matchedAlias: string | undefined = value.match(/\\?course=(.+)$/)?.[1];\n\n      if (alias && matchedAlias && matchedAlias !== alias) {\n        return Promise.reject(`URL must end with ${alias}`);\n      }\n\n      return Promise.resolve();\n    };\n  };\n\n  return (\n    <Modal\n      style={{ top: 20 }}\n      width={800}\n      open={true}\n      title={props.courseId ? 'Edit Course' : 'Add Course'}\n      okText=\"Save\"\n      onOk={async e => {\n        e.preventDefault();\n        const values = await form.validateFields().catch(() => null);\n        if (values == null) {\n          return;\n        }\n        form.submit();\n      }}\n      okButtonProps={{ disabled: response.loading }}\n      cancelButtonProps={{ disabled: response.loading }}\n      onCancel={props.onClose}\n    >\n      {response.loading ? (\n        <Row justify=\"center\">\n          <Col>\n            <Spin spinning={true}></Spin>\n          </Col>\n        </Row>\n      ) : null}\n\n      {response.data ? (\n        <Form<FormData>\n          initialValues={response.data}\n          layout=\"vertical\"\n          form={form}\n          onFinish={(values: FormData) => updateResponse.runAsync(values)}\n          style={{ paddingTop: 16 }}\n        >\n          <Row gutter={24}>\n            {props.courseId ? null : (\n              <Col span={24}>\n                <Form.Item name=\"courseId\" label=\"Copy Tasks, Schedule from:\">\n                  <Select\n                    placeholder=\"Select course template (Optional)\"\n                    options={props.courses.map(item => ({ label: item.name, value: item.id }))}\n                  ></Select>\n                </Form.Item>\n              </Col>\n            )}\n\n            <Col span={24}>\n              <Form.Item name=\"state\">\n                <Radio.Group>\n                  <Radio value=\"active\">Active</Radio>\n                  <Radio value=\"planned\">Planned</Radio>\n                  <Radio value=\"completed\">Completed</Radio>\n                </Radio.Group>\n              </Form.Item>\n            </Col>\n\n            <Col md={8} sm={12} span={24}>\n              <Form.Item name=\"name\" label=\" Course Name\" rules={[{ required: true, message: 'Please enter name' }]}>\n                <Input />\n              </Form.Item>\n            </Col>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item\n                name=\"fullName\"\n                label=\"Full Course Name\"\n                rules={[{ required: true, message: 'Please enter full name' }]}\n              >\n                <Input />\n              </Form.Item>\n            </Col>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item\n                name=\"discordServerId\"\n                label=\"Discord/Telegram channel\"\n                rules={[{ required: true, message: 'Please select discord/telegram channel' }]}\n              >\n                <Select\n                  placeholder=\"Select discord/telegram channel\"\n                  options={props.discordServers.map(({ id, name }) => ({ value: id, label: name }))}\n                />\n              </Form.Item>\n            </Col>\n\n            <Col md={8} sm={12} span={24}>\n              <Form.Item name=\"alias\" label=\"Alias\" rules={[{ required: true, message: 'Please enter alias' }]}>\n                <Input />\n              </Form.Item>\n            </Col>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item name=\"certificateIssuer\" label=\"Certificate Issuer\">\n                <Input />\n              </Form.Item>\n            </Col>\n\n            <Col md={8} sm={12} span={24}>\n              <Form.Item name=\"logo\" label=\"Course Logo\">\n                <Select\n                  options={courseIcons.map(icon => ({\n                    value: icon.id,\n                    label: (\n                      <Flex gap=\"small\" align=\"center\">\n                        <PublicSvgIcon src={icon.active} alt={icon.label} size={'1.5em'} />\n                        <span>{icon.label}</span>\n                      </Flex>\n                    ),\n                  }))}\n                  placeholder=\"Select logo\"\n                ></Select>\n              </Form.Item>\n            </Col>\n\n            <Col md={8} sm={12} span={24}>\n              <Form.Item\n                name={['discipline', 'id']}\n                label=\"Disciplines\"\n                rules={[{ required: true, message: 'Please select a discipline' }]}\n              >\n                <Select\n                  placeholder=\"Select a discipline\"\n                  options={props.disciplines.map(({ id, name }) => ({ value: id, label: name }))}\n                />\n              </Form.Item>\n            </Col>\n            <Col sm={12} span={24}>\n              <Form.Item\n                name=\"range\"\n                label=\"Start - End Date\"\n                rules={[{ required: true, type: 'array', message: 'Please enter course date range' }]}\n              >\n                <DatePicker.RangePicker />\n              </Form.Item>\n            </Col>\n          </Row>\n\n          <Row gutter={24}>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item\n                name=\"registrationEndDate\"\n                label=\"Registration End Date\"\n                getValueFromEvent={date => date?.utc().endOf('day')}\n              >\n                <DatePicker style={{ width: '100%' }} />\n              </Form.Item>\n            </Col>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item\n                name=\"certificateThreshold\"\n                label=\"Certificate Threshold\"\n                tooltip=\"Minimum score percentage required for students to qualify for a certificate.\"\n                rules={[\n                  {\n                    required: true,\n                    message: 'Please input the certificate threshold.',\n                    type: 'integer',\n                    min: 1,\n                    max: 100,\n                  },\n                ]}\n              >\n                <InputNumber step={5} min={1} max={100} addonAfter=\"%\" />\n              </Form.Item>\n            </Col>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item\n                name=\"minStudentsPerMentor\"\n                label=\"Min Students per Mentor\"\n                rules={[\n                  { min: 1, type: 'integer', message: 'Ensure that the input, if provided, is a positive integer.' },\n                ]}\n              >\n                <InputNumber step={1} />\n              </Form.Item>\n            </Col>\n          </Row>\n\n          <Row gutter={24}>\n            <Col sm={12} span={24}>\n              <Form.Item\n                name=\"descriptionUrl\"\n                label=\"Description Url\"\n                rules={[{ required: true, message: 'Please enter course description' }]}\n                shouldUpdate\n              >\n                <Select\n                  options={[\n                    { label: 'JavaScript Preschool RU', value: 'https://rs.school/courses/javascript-preschool-ru' },\n                    { label: 'JavaScript', value: 'https://rs.school/courses/javascript' },\n                    { label: 'JavaScript RU', value: 'https://rs.school/courses/javascript-ru' },\n                    { label: 'React', value: 'https://rs.school/courses/reactjs' },\n                    { label: 'Angular', value: 'https://rs.school/courses/angular' },\n                    { label: 'Node.js', value: 'https://rs.school/courses/nodejs' },\n                    { label: 'AWS Fundamentals', value: 'https://rs.school/courses/aws-fundamentals' },\n                    { label: 'AWS Cloud Developer', value: 'https://rs.school/courses/aws-cloud-developer' },\n                    { label: 'AWS DevOps', value: 'https://rs.school/courses/aws-devops' },\n                    { label: 'AWS AI', value: 'https://rs.school/courses/aws-ai' },\n                    { label: 'Custom', value: 'custom' },\n                  ]}\n                ></Select>\n              </Form.Item>\n            </Col>\n\n            <Col sm={12} span={24}>\n              <Form.Item\n                dependencies={['descriptionUrl']}\n                hidden={descriptionUrl !== 'custom'}\n                name=\"customDescriptionUrl\"\n                label=\"Custom Url\"\n              >\n                <Input />\n              </Form.Item>\n            </Col>\n          </Row>\n\n          <Row gutter={24}>\n            <Col sm={14} span={24}>\n              RS school certificates required for registration (by disciplines){' '}\n              <Tooltip title='If \"Any course\" checked, students will be able to register for the course if they have at least 1 RS School course certificate regardless of the discipline. Otherwise, students will be able to register for the course if they have at least 1 RS School course certificate for the one of the selected disciplines. If nothing is selected - no restrictions are applied.'>\n                <QuestionCircleOutlined style={{ opacity: 0.5 }} />\n              </Tooltip>\n              <Form.Item name=\"anyCertificate\" valuePropName=\"checked\">\n                <Checkbox>Any course</Checkbox>\n              </Form.Item>\n              <Form.Item name=\"certificateDisciplines\" hidden={anyCertificateChecked} style={{ marginTop: -16 }}>\n                <Select\n                  mode=\"multiple\"\n                  optionFilterProp=\"label\"\n                  placeholder=\"Select disciplines\"\n                  options={certificateOptions}\n                />\n              </Form.Item>\n            </Col>\n          </Row>\n\n          <Row gutter={24}>\n            <Col sm={14} span={24}>\n              <Form.Item\n                name=\"wearecommunityUrl\"\n                label=\"WeAreCommunity URL\"\n                rules={[\n                  () => ({\n                    validator: validateWeAreCommunityUrl(),\n                  }),\n                ]}\n              >\n                <Input title={weAreCommunityUrl || ''} placeholder=\"Enter URL\" />\n              </Form.Item>\n            </Col>\n          </Row>\n\n          <Row gutter={24}>\n            <Col sm={22} span={24}>\n              <Typography.Text style={{ whiteSpace: 'pre-line' }} type=\"secondary\">\n                {`This field specifies the URL that will be used for the “Enroll” button on the rs.school course page.\n                    – If you enter a WeAreCommunity URL, that link will be used.\n                    – Otherwise, the RS APP registration link will be used:`}\n              </Typography.Text>\n              {alias && (\n                <Typography.Text type=\"success\" style={{ display: 'block' }} copyable>\n                  {buildRSAppStudentRegistryURL(alias)}\n                </Typography.Text>\n              )}\n            </Col>\n          </Row>\n\n          <Form.Item style={{ marginTop: 8 }} name=\"usePrivateRepositories\" valuePropName=\"checked\">\n            <Checkbox>Use Private Repositories</Checkbox>\n          </Form.Item>\n\n          <Row gutter={24}>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item name=\"personalMentoring\" valuePropName=\"checked\">\n                <Checkbox>Personal mentoring</Checkbox>\n              </Form.Item>\n            </Col>\n            <Col md={8} sm={12} span={24}>\n              <Form.Item dependencies={['personalMentoring']}>\n                {({ getFieldValue }) =>\n                  getFieldValue('personalMentoring') ? (\n                    <Form.Item\n                      name=\"personalMentoringDateRange\"\n                      label=\"Personal Mentoring Dates\"\n                      rules={[{ required: true, type: 'array', message: 'Please enter mentoring dates' }]}\n                    >\n                      <DatePicker.RangePicker style={{ width: '100%' }} />\n                    </Form.Item>\n                  ) : null\n                }\n              </Form.Item>\n            </Col>\n          </Row>\n\n          <Form.Item name=\"inviteOnly\" valuePropName=\"checked\">\n            <Checkbox>Invite Only Course</Checkbox>\n          </Form.Item>\n        </Form>\n      ) : null}\n    </Modal>\n  );\n}\n\nfunction createRecord(values: FormData) {\n  const [startDate, endDate] = values.range as [dayjs.Dayjs, dayjs.Dayjs];\n  const [personalMentoringStartDate, personalMentoringEndDate] = values.personalMentoringDateRange\n    ? (values.personalMentoringDateRange as [dayjs.Dayjs, dayjs.Dayjs])\n    : [null, null];\n\n  const record: UpdateCourseDto = {\n    name: values.name,\n    fullName: values.fullName,\n    alias: values.alias,\n    startDate: startDate ? dayjs.utc(startDate).startOf('day').format() : undefined,\n    endDate: endDate ? dayjs.utc(endDate).startOf('day').format() : undefined,\n    registrationEndDate: values.registrationEndDate ? values.registrationEndDate.toISOString() : null,\n    completed: values.state === 'completed',\n    planned: values.state === 'planned',\n    inviteOnly: values.inviteOnly,\n    descriptionUrl: values.descriptionUrl === 'custom' ? values.customDescriptionUrl : values.descriptionUrl,\n    disciplineId: values.discipline?.id,\n    certificateIssuer: values.certificateIssuer,\n    discordServerId: values.discordServerId,\n    usePrivateRepositories: values.usePrivateRepositories,\n    personalMentoring: values.personalMentoring,\n    personalMentoringStartDate: personalMentoringStartDate\n      ? dayjs.utc(personalMentoringStartDate).startOf('day').format()\n      : null,\n    personalMentoringEndDate: personalMentoringEndDate\n      ? dayjs.utc(personalMentoringEndDate).startOf('day').format()\n      : null,\n    logo: values.logo,\n    minStudentsPerMentor: values.minStudentsPerMentor,\n    certificateThreshold: values.certificateThreshold,\n    wearecommunityUrl: values.wearecommunityUrl || buildRSAppStudentRegistryURL(values.alias ?? ''),\n    certificateDisciplines: values.anyCertificate\n      ? []\n      : values.certificateDisciplines?.length\n        ? values.certificateDisciplines.map(String)\n        : null,\n  };\n  return record;\n}\n\nfunction getDateRange(\n  startDate?: string | null,\n  endDate?: string | null,\n): [dayjs.Dayjs | null, dayjs.Dayjs | null] | null {\n  return startDate && endDate ? [startDate ? dayjs.utc(startDate) : null, endDate ? dayjs.utc(endDate) : null] : null;\n}\n\nfunction getInitialValues(modalData: Partial<Course>): FormData {\n  const range = getDateRange(modalData.startDate, modalData.endDate);\n  const personalMentoringDateRange =\n    getDateRange(modalData.personalMentoringStartDate, modalData.personalMentoringEndDate) || range;\n  return {\n    ...modalData,\n    anyCertificate: modalData.certificateDisciplines?.length === 0 ? true : false,\n    certificateDisciplines: modalData.certificateDisciplines ? modalData.certificateDisciplines : [],\n    wearecommunityUrl: modalData.wearecommunityUrl ?? undefined,\n    minStudentsPerMentor: modalData.minStudentsPerMentor || 2,\n    certificateThreshold: modalData.certificateThreshold ?? 70,\n    inviteOnly: !!modalData.inviteOnly,\n    state: modalData.completed ? 'completed' : modalData.planned ? 'planned' : 'active',\n    registrationEndDate: modalData.registrationEndDate ? dayjs.utc(modalData.registrationEndDate) : null,\n    range: range,\n    personalMentoringDateRange: personalMentoringDateRange,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/CourseTaskModal/index.tsx",
    "content": "import { Checkbox, Col, DatePicker, Divider, Form, Input, InputNumber, message, Row, Select, Typography } from 'antd';\nimport { CheckerEnum, CourseTaskDto, CourseTaskDtoTypeEnum, CreateCourseTaskDto, TaskDto, TasksApi } from '@client/api';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { tagsRenderer } from '@client/shared/components/Table';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { TASK_TYPES } from '@client/data/taskTypes';\nimport dayjs from 'dayjs';\nimport duration from 'dayjs/plugin/duration';\nimport utc from 'dayjs/plugin/utc';\nimport times from 'lodash/times';\nimport { useCallback, useEffect, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseTaskDetails } from '@client/services/course';\nimport { UserService } from '@client/services/user';\n\ndayjs.extend(duration);\ndayjs.extend(utc);\n\nconst { Option } = Select;\n\ntype Props = {\n  onCancel: () => void;\n  onSubmit: (record: CreateCourseTaskDto) => void;\n  data: Partial<CourseTaskDto> | null;\n};\n\nconst userService = new UserService();\nconst taskApi = new TasksApi();\n\nexport function CourseTaskModal(props: Props) {\n  const { data } = props;\n  const [changes, setChanges] = useState({} as Record<string, any>);\n  const [form] = Form.useForm();\n  const [isInvalidCrossCheckEndDate, setIsInvalidCrossCheckEndDate] = useState<boolean>(false);\n\n  const { loading, value: tasksResponse } = useAsync(() => taskApi.getTasks(), []);\n  const tasks = tasksResponse?.data ?? [];\n\n  useEffect(() => {\n    setChanges(data ? { ...data, changes } : {});\n  }, [data]);\n\n  const loadUsers = async (searchText: string) => {\n    return userService.searchUser(searchText);\n  };\n\n  const isInvalidCrossCheckDuration = (startDate: string, endDate: string) => {\n    const MIN_CROSSCHECK_DAYS_COUNT = 3;\n    const MIN_CROSSCHECK_DURATION = MIN_CROSSCHECK_DAYS_COUNT * 24;\n    return dayjs.duration(dayjs(endDate).diff(dayjs(startDate))).asHours() < MIN_CROSSCHECK_DURATION;\n  };\n\n  const handleModalSubmit = async (values: any) => {\n    const record = createRecord(values);\n    if (\n      record.checker === 'crossCheck' &&\n      isInvalidCrossCheckDuration(record.studentEndDate, record.crossCheckEndDate ?? '')\n    ) {\n      message.error('The minimum duration of a cross-check is 3 days.');\n      setIsInvalidCrossCheckEndDate(true);\n      return;\n    }\n    props.onSubmit(record);\n  };\n\n  const handleModalCancel = () => {\n    setChanges({});\n    props.onCancel();\n  };\n\n  const findTaskById = useCallback((id: number) => tasks.find(t => t.id === id), [tasks]);\n\n  const filterOption = useCallback(\n    (input: string, option?: { value: number }): boolean => {\n      if (!input || !option) {\n        return false;\n      }\n      const task = findTaskById(option.value);\n      return task?.name.toLowerCase().includes(input.toLowerCase()) ?? false;\n    },\n    [tasks],\n  );\n\n  const onTaskChange = (taskId: number) => {\n    const task = findTaskById(taskId);\n\n    form.setFieldsValue({ type: task?.type });\n  };\n\n  if (!data) {\n    return null;\n  }\n\n  return (\n    <ModalForm\n      loading={loading}\n      getInitialValues={getInitialValues}\n      data={data}\n      form={form}\n      onChange={values => setChanges({ checker: values.checker, type: values.type })}\n      title=\"Course Task\"\n      submit={handleModalSubmit}\n      cancel={handleModalCancel}\n    >\n      <Form.Item name=\"taskId\" label=\"Task\" rules={[{ required: true, message: 'Please select a task' }]}>\n        <Select filterOption={filterOption} showSearch placeholder=\"Please select a task\" onChange={onTaskChange}>\n          {tasks.map((task: TaskDto) => (\n            <Option key={task.id} value={task.id}>\n              {task.name} {tagsRenderer(task.tags)}\n            </Option>\n          ))}\n        </Select>\n      </Form.Item>\n      <Row gutter={24}>\n        <Col span={12}>\n          <Form.Item name=\"type\" label=\"Task Type\">\n            <Select placeholder=\"Please select type\">\n              {TASK_TYPES.map(({ id, name }) => (\n                <Option key={id} value={id}>\n                  {name}\n                </Option>\n              ))}\n            </Select>\n          </Form.Item>\n        </Col>\n        <Col span={12}>\n          <Form.Item name=\"checker\" required label=\"Checker\">\n            <Select placeholder=\"Please select who checks\">\n              <Option value={CheckerEnum.AutoTest}>Auto-Test</Option>\n              <Option value={CheckerEnum.Mentor}>Mentor</Option>\n              <Option value={CheckerEnum.Assigned}>Cross-Mentor</Option>\n              <Option value={CheckerEnum.TaskOwner}>Task Owner</Option>\n              <Option value={CheckerEnum.CrossCheck}>Cross-Check</Option>\n            </Select>\n          </Form.Item>\n        </Col>\n      </Row>\n      <Form.Item name={['taskOwner', 'id']} label=\"Task Owner\">\n        <UserSearch\n          placeholder=\"Please select a task owner\"\n          defaultValues={data?.taskOwner ? [data.taskOwner] : []}\n          searchFn={loadUsers}\n        />\n      </Form.Item>\n      <Row gutter={24}>\n        <Col span={18}>\n          <Form.Item\n            name=\"range\"\n            label=\"Start Date - End Date\"\n            rules={[{ required: true, type: 'array', message: 'Please enter start and end date' }]}\n          >\n            <DatePicker.RangePicker\n              showTime={{\n                format: 'HH:mm',\n              }}\n              format={value => value.utc().format('YYYY-MM-DD HH:mm')}\n            />\n          </Form.Item>\n        </Col>\n        <Col span={6}>\n          <Form.Item name=\"timeZone\" label=\"TimeZone\">\n            <Select defaultValue=\"UTC\">\n              <Option value=\"UTC\">UTC</Option>\n            </Select>\n          </Form.Item>\n        </Col>\n      </Row>\n\n      {changes.type === CourseTaskDtoTypeEnum.StageInterview || changes.type === CourseTaskDtoTypeEnum.Interview ? (\n        <Row gutter={24}>\n          <Col>\n            <Form.Item\n              name=\"registrationStartDate\"\n              label=\"Registration Start Date\"\n              rules={[{ required: false, message: 'Please enter start date and time' }]}\n            >\n              <DatePicker\n                format=\"YYYY-MM-DD HH:mm\"\n                showTime={{ format: 'HH:mm' }}\n                placeholder=\"Select registration start date\"\n              />\n            </Form.Item>\n          </Col>\n        </Row>\n      ) : null}\n\n      <Row gutter={24}>\n        <Col span={12}>\n          <Form.Item name=\"maxScore\" label=\"Score\" rules={[{ required: true, message: 'Please enter max score' }]}>\n            <InputNumber step={1} min={0} />\n          </Form.Item>\n        </Col>\n        <Col span={12}>\n          <Form.Item\n            name=\"scoreWeight\"\n            label=\"Score Weight\"\n            rules={[{ required: true, message: 'Please enter score weight' }]}\n          >\n            <InputNumber step={0.1} />\n          </Form.Item>\n        </Col>\n      </Row>\n      {changes?.checker === 'crossCheck' ? (\n        <>\n          <Divider style={{ marginTop: 0, marginBottom: 8 }} />\n          <Typography.Title level={4}>Cross-Check</Typography.Title>\n          <Row gutter={24}>\n            <Col span={12}>\n              <Form.Item\n                name=\"crossCheckEndDate\"\n                label=\"Cross-Check End Date\"\n                validateStatus={isInvalidCrossCheckEndDate ? 'error' : undefined}\n                rules={[{ required: true, message: 'Please enter cross-check end date' }]}\n                tooltip=\"Cross-Check End Date must be later than the End Date of the task. The minimum duration of a cross-check is 3 days. The cross-check will be completed at 23:59 UTC on the chosen day.\"\n              >\n                <DatePicker />\n              </Form.Item>\n            </Col>\n            <Col span={12}>\n              <Form.Item\n                name=\"pairsCount\"\n                label=\"Cross-Check Pairs Count\"\n                rules={[{ required: true, message: 'Please enter cross-check pairs count' }]}\n              >\n                <Select placeholder=\"Cross-Check Pairs Count\">\n                  {times(10, num => (\n                    <Option key={num} value={num + 1}>\n                      {num + 1}\n                    </Option>\n                  ))}\n                </Select>\n              </Form.Item>\n            </Col>\n          </Row>\n          <Form.Item name=\"submitText\" label=\"Submit Text\">\n            <Input.TextArea placeholder=\"Free form text to display on submit form\" />\n          </Form.Item>\n          <Form.Item name={['validations', 'githubIdInUrl']} valuePropName=\"checked\">\n            <Checkbox>Require GitHub Username in URL</Checkbox>\n          </Form.Item>\n          <Form.Item name={['validations', 'githubPrInUrl']} valuePropName=\"checked\">\n            <Checkbox>Require GitHub Pull Request in URL</Checkbox>\n          </Form.Item>\n        </>\n      ) : null}\n    </ModalForm>\n  );\n}\n\nfunction createRecord(values: any): CreateCourseTaskDto {\n  const [startDate, endDate] = values.range as [dayjs.Dayjs, dayjs.Dayjs];\n  const crossCheckEndDate = values.crossCheckEndDate as dayjs.Dayjs | null;\n\n  const data: CreateCourseTaskDto = {\n    studentStartDate: startDate.utc().format(),\n    studentEndDate: endDate.utc().format(),\n    crossCheckEndDate: crossCheckEndDate ? crossCheckEndDate.utc().hour(23).minute(59).second(59).format() : undefined,\n    taskId: values.taskId,\n    taskOwnerId: values.taskOwner?.id,\n    checker: values.checker,\n    scoreWeight: values.scoreWeight,\n    maxScore: values.maxScore,\n    type: values.type,\n    pairsCount: values.pairsCount,\n    submitText: values.submitText,\n    validations: values.validations,\n    studentRegistrationStartDate: values.registrationStartDate,\n  };\n  return data;\n}\n\nfunction getInitialValues(modalData: Partial<CourseTaskDetails>) {\n  const data = {\n    ...modalData,\n    timeZone: 'UTC',\n    taskOwnerId: modalData.taskOwner ? modalData.taskOwner.id : undefined,\n    maxScore: modalData.maxScore || 100,\n    scoreWeight: modalData.scoreWeight ?? 1,\n    crossCheckEndDate: modalData.crossCheckEndDate ? dayjs.utc(modalData.crossCheckEndDate) : null,\n    range:\n      modalData.studentStartDate && modalData.studentEndDate\n        ? [dayjs.utc(modalData.studentStartDate), dayjs.utc(modalData.studentEndDate)]\n        : [dayjs().utc().hour(0).minute(0).second(0).utc(), dayjs().utc().hour(23).minute(59).second(59)],\n    checker: modalData.checker || CheckerEnum.AutoTest,\n    registrationStartDate: modalData.studentRegistrationStartDate\n      ? dayjs.utc(modalData.studentRegistrationStartDate)\n      : null,\n  };\n  return data;\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/CoursesListModal/index.tsx",
    "content": "import { Form, Select } from 'antd';\nimport { CourseDto, CoursesApi } from '@client/api';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { useCallback, PropsWithChildren } from 'react';\nimport { useAsync } from 'react-use';\n\nconst { Option } = Select;\n\ntype Props = PropsWithChildren<{\n  onCancel: () => void;\n  onSubmit: (record: Pick<CourseDto, 'id'>) => void;\n  data: Partial<Pick<CourseDto, 'id'>> | null;\n  okText?: string;\n}>;\n\nconst coursesApi = new CoursesApi();\n\nexport function CoursesListModal(props: Props) {\n  const { loading, value } = useAsync(() => coursesApi.getCourses(), []);\n\n  const courses = value?.data ?? [];\n\n  const handleModalSubmit = async (values: any) => {\n    const record = createRecord(values);\n    props.onSubmit(record);\n  };\n\n  const handleModalCancel = () => {\n    props.onCancel();\n  };\n\n  const filterOption = useCallback(\n    (input: string, option?: { value: number }): boolean => {\n      if (!input) {\n        return false;\n      }\n      const task = courses.find(t => t.id === Number(option?.value));\n      return task?.name.toLowerCase().includes(input.toLowerCase()) ?? false;\n    },\n    [courses],\n  );\n\n  if (!props.data) {\n    return null;\n  }\n\n  return (\n    <ModalForm\n      data={props.data}\n      okText={props.okText}\n      loading={loading}\n      title=\"Courses\"\n      submit={handleModalSubmit}\n      cancel={handleModalCancel}\n    >\n      {props.children}\n      <Form.Item name=\"courseId\" label=\"Course\" rules={[{ required: true, message: 'Please select a course' }]}>\n        <Select filterOption={filterOption} showSearch placeholder=\"Please select a course\">\n          {courses.map(task => (\n            <Option key={task.id} value={task.id}>\n              {task.name}\n            </Option>\n          ))}\n        </Select>\n      </Form.Item>\n    </ModalForm>\n  );\n}\n\nfunction createRecord(values: { courseId: number }) {\n  return { id: values.courseId };\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/ExpelCriteriaModal/ExpelCriteriaModal.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { EXPEL_ALERT_MESSAGE, ExpelCriteriaModal, FormValues, hasValidCriteria } from './ExpelCriteriaModal';\nimport userEvent from '@testing-library/user-event';\nimport * as ReactUse from 'react-use';\n\nconst props = {\n  courseId: 1,\n  onSubmit: vi.fn(),\n  onClose: vi.fn(),\n  isModalOpen: true,\n};\n\nconst renderExpelCriteriaModal = () => {\n  render(<ExpelCriteriaModal {...props} />);\n};\n\ndescribe('ExpelCriteriaModal', () => {\n  beforeAll(() => {\n    // mock CoursesTasksApi call\n    vi.spyOn(ReactUse, 'useAsync').mockReturnValue({\n      value: [\n        {\n          name: 'course 1',\n          id: 1,\n        },\n      ],\n      loading: false,\n    });\n  });\n\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  const user = userEvent.setup();\n\n  test('should render modal title', async () => {\n    renderExpelCriteriaModal();\n\n    const title = await screen.findByText('Expel Criteria');\n    expect(title).toBeInTheDocument();\n  });\n\n  test('should render alert message', async () => {\n    renderExpelCriteriaModal();\n\n    const alert = await screen.findByText(EXPEL_ALERT_MESSAGE);\n    expect(alert).toBeInTheDocument();\n  });\n\n  test.each`\n    label\n    ${\"Didn't Complete Following Tasks\"}\n    ${'Minimum Total Score'}\n    ${'Expel Reason'}\n  `('should render field with $label label', async ({ label }) => {\n    renderExpelCriteriaModal();\n\n    const field = await screen.findByText(label);\n    expect(field).toBeInTheDocument();\n  });\n\n  test('should render checkbox', async () => {\n    renderExpelCriteriaModal();\n\n    const checkbox = await screen.findByRole('checkbox');\n    expect(checkbox).toBeInTheDocument();\n  });\n\n  test('should render \"cancel\" button', async () => {\n    renderExpelCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /cancel/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  test('should render \"expel students\" button', async () => {\n    renderExpelCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /expel students/i });\n    expect(button).toBeInTheDocument();\n    expect(button).toBeDisabled();\n  });\n\n  test('should enable \"expel students\" button on valid criteria', async () => {\n    renderExpelCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /expel students/i });\n    expect(button).toBeDisabled();\n\n    const minTotalScoreInput = await screen.findByLabelText('Minimum Total Score');\n    fireEvent.change(minTotalScoreInput, {\n      target: {\n        value: 5,\n      },\n    });\n    expect(button).toBeEnabled();\n  });\n\n  test('should call \"onClose\" function on \"cancel\" button click', async () => {\n    renderExpelCriteriaModal();\n\n    const button = await screen.findByRole('button', { name: /cancel/i });\n    await user.click(button);\n\n    expect(props.onClose).toHaveBeenCalled();\n  });\n\n  test('should render error message when expel reason not provided', async () => {\n    renderExpelCriteriaModal();\n\n    // Enable \"expel students\" button\n    const minTotalScoreInput = await screen.findByLabelText('Minimum Total Score');\n    fireEvent.change(minTotalScoreInput, {\n      target: {\n        value: 5,\n      },\n    });\n\n    const button = await screen.findByRole('button', { name: /expel students/i });\n    await user.click(button);\n\n    const errorMessage = await screen.findByText('Please provide the expel reason');\n\n    expect(props.onSubmit).not.toHaveBeenCalled();\n    expect(errorMessage).toBeInTheDocument();\n  });\n\n  test('should call \"onSubmit\" function on \"expel students\" button click', async () => {\n    renderExpelCriteriaModal();\n\n    // Enable \"expel students\" button\n    const minTotalScoreInput = await screen.findByLabelText('Minimum Total Score');\n    fireEvent.change(minTotalScoreInput, {\n      target: {\n        value: 5,\n      },\n    });\n\n    // fill in required field\n    const reasonTextAreal = await screen.findByLabelText('Expel Reason');\n    fireEvent.change(reasonTextAreal, {\n      target: {\n        value: 'reason',\n      },\n    });\n\n    const button = await screen.findByRole('button', { name: /expel students/i });\n    await user.click(button);\n\n    expect(props.onSubmit).toHaveBeenCalled();\n  });\n});\n\ndescribe('hasValidCriteria', () => {\n  test('should return \"false\" on empty values', () => {\n    expect(hasValidCriteria({} as FormValues)).toBe(false);\n  });\n\n  test('should return \"false\" on minScore = 0', () => {\n    const values = {\n      minScore: 0,\n    } as FormValues;\n\n    expect(hasValidCriteria(values)).toBe(false);\n  });\n\n  test('should return \"true\" on minScore > 0', () => {\n    const values = {\n      minScore: 5,\n    } as FormValues;\n\n    expect(hasValidCriteria(values)).toBe(true);\n  });\n\n  test('should return \"false\" when courseTasksIds array is empty', () => {\n    const values = {\n      courseTaskIds: [] as number[],\n    } as FormValues;\n\n    expect(hasValidCriteria(values)).toBe(false);\n  });\n\n  test('should return \"true\" when courseTasksIds array is not empty', () => {\n    const values = {\n      courseTaskIds: [1, 2],\n    } as FormValues;\n\n    expect(hasValidCriteria(values)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/ExpelCriteriaModal/ExpelCriteriaModal.tsx",
    "content": "import { Form, Input, InputNumber, Alert, Checkbox, Modal, Row, Col, Button, Space } from 'antd';\nimport { useState } from 'react';\nimport { SelectCourseTasks } from '@client/modules/CourseManagement/components';\n\nexport type FormValues = {\n  courseTaskIds: number[];\n  minScore: number;\n  keepWithMentor: boolean;\n  reason: string;\n};\n\ntype Criteria = Partial<FormValues> & { reason: string };\n\ntype Props = {\n  courseId: number;\n  onSubmit: (expelCriteria: Criteria) => void;\n  onClose: () => void;\n  isModalOpen: boolean;\n};\n\nexport const EXPEL_ALERT_MESSAGE = 'All students meeting the criteria below will be expelled from the course.';\n\nexport function ExpelCriteriaModal({ courseId, onSubmit, onClose, isModalOpen }: Props) {\n  const [form] = Form.useForm<FormValues>();\n  const [okEnabled, setOkEnabled] = useState(false);\n\n  return (\n    <Modal\n      width={600}\n      title=\"Expel Criteria\"\n      onCancel={onClose}\n      open={isModalOpen}\n      bodyStyle={{ paddingBlock: 16 }}\n      footer={null}\n    >\n      <Form\n        layout=\"vertical\"\n        form={form}\n        onValuesChange={(_, values) => {\n          setOkEnabled(hasValidCriteria(values));\n        }}\n        onFinish={onSubmit}\n      >\n        <Row gutter={[0, 16]}>\n          <Col span={24}>\n            <Alert type=\"warning\" message={EXPEL_ALERT_MESSAGE} showIcon />\n          </Col>\n          <Col span={24}>\n            <SelectCourseTasks courseId={courseId} label=\"Didn't Complete Following Tasks\" />\n          </Col>\n          <Col span={24}>\n            <Form.Item name=\"minScore\" label=\"Minimum Total Score\" style={{ marginBottom: 0 }}>\n              <InputNumber style={{ width: '100%' }} type=\"number\" min={0} placeholder=\"Enter minimum score\" />\n            </Form.Item>\n          </Col>\n          <Col span={24}>\n            <Form.Item name=\"keepWithMentor\" valuePropName=\"checked\" style={{ marginBottom: 0 }}>\n              <Checkbox>Don't expel students with assigned mentor</Checkbox>\n            </Form.Item>\n          </Col>\n          <Col span={24}>\n            <Form.Item\n              name=\"reason\"\n              rules={[{ required: true, message: 'Please provide the expel reason' }]}\n              label=\"Expel Reason\"\n              required\n            >\n              <Input.TextArea placeholder=\"Specify the student expel reason\" />\n            </Form.Item>\n          </Col>\n          <Col span={24}>\n            <Row justify=\"end\">\n              <Space wrap>\n                <Button onClick={onClose}>Cancel</Button>\n                <Button type=\"primary\" htmlType=\"submit\" disabled={!okEnabled} danger>\n                  Expel Students\n                </Button>\n              </Space>\n            </Row>\n          </Col>\n        </Row>\n      </Form>\n    </Modal>\n  );\n}\n\nexport function hasValidCriteria({ minScore, courseTaskIds }: FormValues) {\n  return !!minScore || courseTaskIds?.length > 0;\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/ExpelledStudentsStats.tsx",
    "content": "import { Table, Typography, Tag, Button, Row } from 'antd';\nimport { ColumnsType, ColumnType } from 'antd/es/table';\nimport React, { useMemo } from 'react';\nimport { PublicSvgIcon } from '@client/shared/components/Icons';\nimport { DEFAULT_COURSE_ICONS } from '@client/configs/course-icons';\nimport { dateUtcRenderer } from '@client/shared/components/Table';\nimport { useExpelledStats } from '@client/modules/CourseManagement/hooks/useExpelledStats';\nimport { ExpelledStatsDto } from '@client/api';\n\nconst { Title, Text } = Typography;\n\ntype Props = {\n  courseId?: number;\n};\n\nconst getColumns = (handleDelete: (id: string) => void, isDeleting: boolean): ColumnsType<ExpelledStatsDto> => [\n  {\n    title: 'Course',\n    dataIndex: ['course', 'name'],\n    key: 'courseName',\n    render: (_text, record) => (\n      <div>\n        <PublicSvgIcon size=\"25px\" src={DEFAULT_COURSE_ICONS[record.course.logo]?.active} />\n        <Text strong style={{ marginLeft: 15 }}>\n          {record.course.alias}\n        </Text>\n        <br />\n        <Text type=\"secondary\">{record.course.fullName || record.course.name}</Text>\n      </div>\n    ),\n  },\n  {\n    title: 'Student GitHub',\n    dataIndex: ['user', 'githubId'],\n    key: 'githubId',\n    render: githubId => (\n      <a href={`https://github.com/${githubId}`} target=\"_blank\" rel=\"noopener noreferrer\">\n        {githubId}\n      </a>\n    ),\n  },\n  {\n    title: 'Reasons for Leaving',\n    dataIndex: 'reasonForLeaving',\n    key: 'reasons',\n    render: (reasons?: string[]) => (\n      <>\n        {reasons?.map(reason => (\n          <Tag key={reason}>{reason.replace(/_/g, ' ')}</Tag>\n        ))}\n      </>\n    ),\n  },\n  {\n    title: 'Other Comments',\n    dataIndex: 'otherComment',\n    key: 'otherComment',\n  },\n  {\n    title: 'Date',\n    dataIndex: 'submittedAt',\n    key: 'date',\n    render: dateUtcRenderer,\n  },\n  {\n    title: 'Action',\n    key: 'action',\n    render: (_text, record) => (\n      <Button danger onClick={() => handleDelete(record.id)} loading={isDeleting}>\n        Delete\n      </Button>\n    ),\n  },\n];\n\nconst ExpelledStudentsStats: React.FC<Props> = ({ courseId }) => {\n  const { data, error, loading, isDeleting, handleDelete } = useExpelledStats(courseId);\n  const [csvUrl, setCsvUrl] = React.useState<string | null>(null);\n  const downloadRef = React.useRef<HTMLAnchorElement>(null);\n\n  const columns = useMemo(() => getColumns(handleDelete, isDeleting), [handleDelete, isDeleting]);\n\n  if (error) {\n    return <Typography.Paragraph>Failed to load statistics.</Typography.Paragraph>;\n  }\n\n  const escapeCSVValue = (value: string): string => {\n    if (value.includes(',') || value.includes('\"')) {\n      return `\"${value.replace(/\"/g, '\"\"')}\"`;\n    }\n    return value;\n  };\n\n  const getValueFromDataIndex = (row: ExpelledStatsDto, dataIndex: unknown): string => {\n    if (Array.isArray(dataIndex)) {\n      let current: unknown = row;\n\n      for (const key of dataIndex) {\n        const keyAsString = String(key);\n        current = current ? (current as Record<string, unknown>)[keyAsString] : undefined;\n      }\n\n      return current !== undefined && current !== null ? String(current) : '';\n    }\n\n    if (typeof dataIndex === 'string' || typeof dataIndex === 'number') {\n      const value = row[dataIndex as keyof ExpelledStatsDto];\n      return value !== undefined && value !== null ? String(value) : '';\n    }\n\n    return '';\n  };\n\n  const getSpecialColumnValue = (row: ExpelledStatsDto, columnKey: string): string => {\n    switch (columnKey) {\n      case 'reasons':\n        return row.reasonForLeaving ? row.reasonForLeaving.map(r => r.replace(/_/g, ' ')).join('; ') : '';\n      case 'date':\n        return row.submittedAt ? new Date(row.submittedAt).toLocaleString() : '';\n      case 'courseName':\n        return row.course ? row.course.alias : '';\n      case 'githubId':\n        return row.user ? row.user.githubId : '';\n      default:\n        return '';\n    }\n  };\n\n  const formatRowToCsv = (row: ExpelledStatsDto, exportableColumns: ColumnType<ExpelledStatsDto>[]): string => {\n    return exportableColumns\n      .map(col => {\n        let value = '';\n\n        if (col.dataIndex) {\n          value = getValueFromDataIndex(row, col.dataIndex);\n        } else if (col.key) {\n          value = getSpecialColumnValue(row, String(col.key));\n        }\n\n        return escapeCSVValue(value);\n      })\n      .join(',');\n  };\n\n  const handleExportCsv = () => {\n    if (!data || data.length === 0) {\n      return;\n    }\n\n    const exportableColumns = columns.filter(\n      (col): col is ColumnType<ExpelledStatsDto> => 'dataIndex' in col && col.dataIndex !== undefined,\n    );\n\n    const headers = exportableColumns\n      .map(col => {\n        if (Array.isArray(col.title)) {\n          return col.title.join(' ');\n        }\n        return col.title;\n      })\n      .filter(Boolean)\n      .join(',');\n\n    const csvRows = data.map(row => formatRowToCsv(row, exportableColumns));\n\n    const csvContent = [headers, ...csvRows].join('\\n');\n    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });\n    const url = URL.createObjectURL(blob);\n    setCsvUrl(url);\n\n    setTimeout(() => {\n      if (downloadRef.current) {\n        downloadRef.current.click();\n        URL.revokeObjectURL(url);\n        setCsvUrl(null);\n      }\n    }, 0);\n  };\n\n  return (\n    <>\n      <div style={{ marginTop: 24 }}>\n        <Row justify=\"space-between\" align=\"middle\" style={{ marginBottom: 16 }} />\n        <Title level={4} style={{ margin: 0 }}>\n          Detailed Statistics on Student Departures\n        </Title>\n        <Button type=\"primary\" onClick={handleExportCsv}>\n          Export CSV\n        </Button>\n        <a ref={downloadRef} href={csvUrl || ''} download=\"expelled_students_stats.csv\" style={{ display: 'none' }}>\n          Download\n        </a>\n      </div>\n      <Table\n        loading={loading}\n        dataSource={data || []}\n        columns={columns}\n        rowKey=\"id\"\n        pagination={{ pageSize: 20 }}\n        size=\"small\"\n      />\n    </>\n  );\n};\n\nexport default ExpelledStudentsStats;\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/SelectCourseTasks/SelectCourseTasks.test.tsx",
    "content": "import { Form } from 'antd';\nimport { SelectCourseTasks } from './SelectCourseTasks';\nimport { render, screen } from '@testing-library/react';\nimport * as ReactUse from 'react-use';\nimport userEvent from '@testing-library/user-event';\n\nconst renderSelectCourseTasks = () => {\n  render(\n    <Form>\n      <SelectCourseTasks courseId={1} label=\"Task\" />\n    </Form>,\n  );\n};\n\ndescribe('SelectCourseTasks', () => {\n  beforeAll(() => {\n    // mock CoursesTasksApi call\n    vi.spyOn(ReactUse, 'useAsync').mockReturnValue({\n      value: [\n        {\n          name: 'course 1',\n          id: 1,\n        },\n        {\n          name: 'course 2',\n          id: 2,\n        },\n      ],\n      loading: false,\n    });\n  });\n\n  const user = userEvent.setup();\n\n  test('should render field with \"Task\" label', async () => {\n    renderSelectCourseTasks();\n\n    const field = await screen.findByLabelText('Task');\n    expect(field).toBeInTheDocument();\n  });\n\n  test('should render options on select click', async () => {\n    renderSelectCourseTasks();\n\n    const field = await screen.findByLabelText('Task');\n\n    await user.click(field);\n\n    const options = await screen.findAllByRole('option');\n    expect(options.length).toBe(2);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/SelectCourseTasks/SelectCourseTasks.tsx",
    "content": "import { Form, FormItemProps, Select } from 'antd';\nimport { CoursesTasksApi } from '@client/api';\nimport { useAsync } from 'react-use';\n\ntype Props = FormItemProps & {\n  courseId: number;\n};\n\nconst courseTasksApi = new CoursesTasksApi();\n\nexport function SelectCourseTasks({ courseId, ...props }: Props) {\n  const { value: courseTasks = [], loading } = useAsync(async () => {\n    const { data } = await courseTasksApi.getCourseTasks(courseId);\n    return data;\n  }, [courseId]);\n\n  return (\n    <Form.Item name=\"courseTaskIds\" style={{ marginBottom: 0 }} {...props}>\n      <Select\n        mode=\"multiple\"\n        placeholder=\"Select tasks\"\n        loading={loading}\n        optionFilterProp=\"label\"\n        options={courseTasks.map(({ name, id }) => ({\n          label: name,\n          value: id,\n        }))}\n      />\n    </Form.Item>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CourseManagement/components/index.ts",
    "content": "export { SelectCourseTasks } from './SelectCourseTasks/SelectCourseTasks';\nexport { CertificateCriteriaModal } from './CertificateCriteriaModal/CertificateCriteriaModal';\nexport { ExpelCriteriaModal } from './ExpelCriteriaModal/ExpelCriteriaModal';\n"
  },
  {
    "path": "client/src/modules/CourseManagement/hooks/useExpelledStats.ts",
    "content": "import { CourseStatsApi, ExpelledStatsDto } from '@client/api';\nimport { useRequest } from 'ahooks';\n\nconst courseStatsApi = new CourseStatsApi();\n\nconst fetchExpelledStats = async (courseId: number): Promise<ExpelledStatsDto[]> => {\n  const response = await courseStatsApi.getCourseExpelledStats(courseId);\n  return response.data;\n};\n\nexport const useExpelledStats = (courseId?: number) => {\n  const { data, error, loading, refresh } = useRequest(() => fetchExpelledStats(courseId as number), {\n    ready: !!courseId,\n    refreshDeps: [courseId],\n  });\n\n  const { runAsync: handleDelete, loading: isDeleting } = useRequest(\n    async (id: string) => courseStatsApi.deleteExpelledStat(id),\n    { onSuccess: refresh },\n  );\n\n  return {\n    data,\n    error,\n    loading,\n    isDeleting,\n    handleDelete,\n  };\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/CountriesChart/CountriesChart.tsx",
    "content": "import { Bar, BarConfig } from '@ant-design/plots';\nimport { Flex, Image, Typography } from 'antd';\nimport { CountryStatDto } from '@client/api';\nimport { useCallback, useMemo } from 'react';\nimport { Colors } from '../../data';\n\ntype Props = {\n  data: CountryStatDto[];\n  activeCount: number;\n  xAxisTitle: string;\n  color?: Colors;\n};\n\n/*\n * Defining the Datum type manually as it cannot be imported directly from @ant-design/plots.\n * This type is inferred from the 'data' property of the Bar component's first parameter.\n * This approach is necessary because @ant-design/plots does not explicitly export the Datum type.\n * Use this type definition cautiously and review it if the library updates.\n */\ntype Datum = Parameters<typeof Bar>[0]['data'][number];\n\nconst { Text } = Typography;\n\nfunction CountriesChart({ data, activeCount, xAxisTitle, color = Colors.Blue }: Props) {\n  const tooltipFormatter = useCallback(\n    (datum: Datum) => {\n      const percentage = activeCount ? Math.ceil((datum.count / activeCount) * 100) : 0;\n      return {\n        name: xAxisTitle,\n        value: `${datum.count} (${percentage}%)`,\n      };\n    },\n    [activeCount],\n  );\n\n  const config: BarConfig = useMemo(\n    () => ({\n      data,\n      yField: 'countryName',\n      xField: 'count',\n      yAxis: {\n        label: { autoRotate: false },\n      },\n      tooltip: { formatter: tooltipFormatter },\n      xAxis: { title: { text: xAxisTitle } },\n      scrollbar: { type: 'vertical' },\n      //Why this affects the size of the chart, I don't know. Do not delete.\n      seriesField: 'type',\n      color: () => color,\n    }),\n    [data, tooltipFormatter],\n  );\n\n  if (!data.length) {\n    return (\n      <Flex vertical gap=\"middle\" align=\"center\" justify=\"center\">\n        <Text strong>No student data available to display</Text>\n        <Image preview={false} src=\"/static/svg/err.svg\" alt=\"Error 404\" width={175} height={175} />\n      </Flex>\n    );\n  }\n\n  return <Bar {...config} />;\n}\n\nexport default CountriesChart;\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/DonutChart/DonutChart.tsx",
    "content": "import { Pie, PieConfig } from '@ant-design/plots';\n\ntype Props = {\n  data: {\n    type: string;\n    value: number;\n  }[];\n  config?: Partial<PieConfig>;\n};\n\nconst DonutChart = ({ data, config = {} }: Props) => {\n  const pieConfig: PieConfig = {\n    ...config,\n    data,\n    angleField: 'value',\n    colorField: 'type',\n    innerRadius: 0.6,\n    label: false,\n    legend: {\n      color: {\n        title: false,\n        position: 'right',\n        rowPadding: 5,\n      },\n    },\n  };\n  return <Pie {...pieConfig} />;\n};\n\nexport default DonutChart;\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/EpamMentorsStatsCard/EpamMentorsStatsCard.tsx",
    "content": "import { Card, Typography } from 'antd';\nimport { CourseMentorsStatsDto } from '@client/api';\nimport { Colors } from '../../data';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  mentorsStats: CourseMentorsStatsDto;\n};\n\nconst { Text } = Typography;\n\nconst LiquidChart = dynamicWithSkeleton(() => import('../LiquidChart/LiquidChart'));\n\nexport const EpamMentorsStatsCard = ({ mentorsStats }: Props) => {\n  return (\n    <Card title=\"Epam Mentors\">\n      <Text strong>\n        Epam Mentors: {mentorsStats.epamMentorsCount} / {mentorsStats.mentorsActiveCount}\n      </Text>\n      <div style={{ height: 180, width: '100%' }}>\n        <LiquidChart\n          count={mentorsStats.epamMentorsCount}\n          total={mentorsStats.mentorsActiveCount}\n          color={Colors.Purple}\n        />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/EpamMentorsStatsCard/index.tsx",
    "content": "export { EpamMentorsStatsCard } from './EpamMentorsStatsCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/LiquidChart/LiquidChart.tsx",
    "content": "import { Liquid, LiquidConfig } from '@ant-design/plots';\nimport { Colors } from '../../data';\nimport { GlobalToken, theme } from 'antd';\n\ntype Props = {\n  count: number;\n  total: number;\n  color?: Colors;\n  background?: GlobalToken;\n};\n\nfunction LiquidChart({ count, total, color = Colors.Blue, background }: Props) {\n  const { token } = theme.useToken();\n  const percent = count / total;\n  const config: LiquidConfig = {\n    theme: {\n      background: background || token.colorBgContainer,\n    },\n    percent: percent,\n    style: {\n      fill: color,\n      outlineBorder: 4,\n      outlineDistance: 8,\n      outlineStroke: color,\n      waveLength: 128,\n      contentText: `${(percent * 100).toFixed(2)}%`,\n    },\n  };\n  return <Liquid {...config} />;\n}\n\nexport default LiquidChart;\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/MentorsCountriesCard/MentorsCountriesCard.tsx",
    "content": "import { Card } from 'antd';\nimport { CountriesStatsDto } from '@client/api';\nimport { Colors } from '../../data';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  countriesStats: CountriesStatsDto;\n  activeCount: number;\n};\n\nconst CountriesChart = dynamicWithSkeleton(() => import('../CountriesChart/CountriesChart'));\n\nexport const MentorsCountriesCard = ({ countriesStats, activeCount }: Props) => {\n  const { countries } = countriesStats;\n  return (\n    <Card title=\"Mentors Countries\">\n      <div style={{ height: 350, width: '100%' }}>\n        <CountriesChart\n          data={countries}\n          activeCount={activeCount}\n          xAxisTitle={'Number of Mentors'}\n          color={Colors.Purple}\n        />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/MentorsCountriesCard/index.ts",
    "content": ""
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StatCards/StatCards.module.css",
    "content": ".masonry {\n  display: flex;\n  margin-left: -24px;\n  width: auto;\n  min-height: 85vh;\n}\n\n.masonryColumn {\n  padding-left: 24px;\n  background-clip: padding-box;\n}\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StatCards/StatCards.tsx",
    "content": "import { CourseAggregateStatsDto, CoursesTasksApi, CourseTaskDto } from '@client/api';\nimport { StudentsCountriesCard } from '@client/modules/CourseStatistics/components/StudentsCountriesCard';\nimport { StudentsStatsCard } from '@client/modules/CourseStatistics/components/StudentsStatsCard';\nimport { MentorsCountriesCard } from '@client/modules/CourseStatistics/components/MentorsCountriesCard/MentorsCountriesCard';\nimport { EpamMentorsStatsCard } from '@client/modules/CourseStatistics/components/EpamMentorsStatsCard';\nimport { StudentsWithMentorsCard } from '@client/modules/CourseStatistics/components/StudentsWithMentorsCard';\nimport { StudentsWithCertificateCard } from '@client/modules/CourseStatistics/components/StudentsWithCertificateCard';\nimport { StudentsEligibleForCertificationCard } from '@client/modules/CourseStatistics/components/StudentsEligibleForCertificationCard';\nimport { TaskPerformanceCard } from '@client/modules/CourseStatistics/components/TaskPerformanceCard';\nimport { StudentsCertificatesCountriesCard } from '@client/modules/CourseStatistics/components/StudentsCertificatesCountriesCard';\nimport Masonry from 'react-masonry-css';\nimport { useAsync } from 'react-use';\nimport styles from './StatCards.module.css';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\ntype StatCardsProps = {\n  coursesData?: CourseAggregateStatsDto;\n};\n\nconst gapSize = 24;\n\nconst masonryBreakPoints = {\n  default: 4,\n  1100: 3,\n  700: 2,\n  500: 1,\n};\n\nconst coursesTasksApi = new CoursesTasksApi();\n\nexport function StatCards({ coursesData }: StatCardsProps) {\n  const { course } = useActiveCourseContext();\n  const { value: courseTasks } = useAsync(async () => {\n    if (course?.id) {\n      const { data } = await coursesTasksApi.getCourseTasks(course.id);\n\n      return data;\n    }\n  }, [course]);\n\n  const cards = [\n    coursesData?.studentsCountries && {\n      title: 'studentsCountriesCard',\n      component: (\n        <StudentsCountriesCard\n          studentsCountriesStats={coursesData.studentsCountries}\n          activeStudentsCount={coursesData.studentsStats.activeStudentsCount}\n        />\n      ),\n    },\n    coursesData?.studentsStats.totalStudents && {\n      title: 'studentsStatsCard',\n      component: <StudentsStatsCard studentsStats={coursesData.studentsStats} />,\n    },\n    coursesData?.mentorsCountries &&\n      coursesData.mentorsStats.mentorsActiveCount && {\n        title: 'mentorsCountriesCard',\n        component: (\n          <MentorsCountriesCard\n            countriesStats={coursesData.mentorsCountries}\n            activeCount={coursesData.mentorsStats.mentorsActiveCount}\n          />\n        ),\n      },\n    coursesData?.mentorsStats.epamMentorsCount && {\n      title: 'mentorsStatsCard',\n      component: <EpamMentorsStatsCard mentorsStats={coursesData.mentorsStats} />,\n    },\n    coursesData?.studentsStats.studentsWithMentorCount && {\n      title: 'studentsWithMentorStatsCard',\n      component: <StudentsWithMentorsCard studentsStats={coursesData.studentsStats} />,\n    },\n    coursesData?.studentsStats.certifiedStudentsCount && {\n      title: 'studentsWithCertificateStatsCard',\n      component: <StudentsWithCertificateCard studentsStats={coursesData.studentsStats} />,\n    },\n    !coursesData?.studentsStats.certifiedStudentsCount &&\n      coursesData?.studentsStats.eligibleForCertificationCount && {\n        title: 'StudentsEligibleForCertificationCard',\n        component: <StudentsEligibleForCertificationCard studentsStats={coursesData.studentsStats} />,\n      },\n    coursesData?.courseTasks &&\n      courseTasks && {\n        title: 'taskPerformanceCard',\n        component: (\n          <TaskPerformanceCard\n            tasks={courseTasks.filter((task: CourseTaskDto) => coursesData.courseTasks?.some(ct => ct.id === task.id))}\n          />\n        ),\n      },\n    coursesData?.studentsCertificatesCountries &&\n      coursesData.studentsStats.certifiedStudentsCount && {\n        title: 'studentsCertificatesCountriesCard',\n        component: (\n          <StudentsCertificatesCountriesCard\n            studentsCertificatesCountriesStats={coursesData.studentsCertificatesCountries}\n            certificatesCount={coursesData.studentsStats.certifiedStudentsCount}\n          />\n        ),\n      },\n  ].filter(Boolean);\n\n  return (\n    <>\n      <Masonry\n        breakpointCols={masonryBreakPoints}\n        className={styles.masonry as string}\n        columnClassName={styles.masonryColumn as string}\n      >\n        {cards.map(({ title, component }) => (\n          <div style={{ marginBottom: gapSize }} key={title}>\n            {component}\n          </div>\n        ))}\n      </Masonry>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StatCards/index.ts",
    "content": "export { StatCards } from './StatCards';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StatScopeSelector/StatScopeSelector.tsx",
    "content": "import { DatePicker, DatePickerProps, Flex, Space, Switch, SwitchProps } from 'antd';\nimport type { Dayjs } from 'dayjs';\nimport { StatScope } from '@client/modules/CourseStatistics/constants';\nimport dayjs from 'dayjs';\n\ntype StatScopeSelectorProps = {\n  statScope: StatScope;\n  handleYearSelection: DatePickerProps<Dayjs>['onChange'];\n  handleStatScope: SwitchProps['onChange'];\n  selectedYear?: number;\n};\n\nexport function StatScopeSelector({\n  statScope,\n  handleYearSelection,\n  handleStatScope,\n  selectedYear,\n}: StatScopeSelectorProps) {\n  const date = selectedYear ? dayjs(String(selectedYear)) : dayjs(new Date());\n  return (\n    <Flex\n      wrap=\"wrap\"\n      justify=\"space-between\"\n      align=\"center\"\n      gap=\"1rem\"\n      style={{ paddingBottom: '1rem', minHeight: '3rem' }}\n    >\n      <Space>\n        {statScope === StatScope.Timeline && (\n          <DatePicker allowClear={false} onChange={handleYearSelection} picker=\"year\" defaultValue={date} />\n        )}\n      </Space>\n      <Switch\n        checkedChildren=\"Current\"\n        unCheckedChildren=\"Timeline\"\n        checked={statScope === StatScope.Current}\n        onChange={handleStatScope}\n      />\n    </Flex>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StatScopeSelector/index.ts",
    "content": "export { StatScopeSelector } from './StatScopeSelector';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsCertificatesCountriesCard/StudentsCertificatesCountriesCard.tsx",
    "content": "import { Card } from 'antd';\nimport { CountriesStatsDto } from '@client/api';\nimport { Colors } from '@client/modules/CourseStatistics/data';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  studentsCertificatesCountriesStats: CountriesStatsDto;\n  certificatesCount: number;\n};\n\nconst CountriesChart = dynamicWithSkeleton(() => import('../CountriesChart/CountriesChart'));\n\nexport const StudentsCertificatesCountriesCard = ({ studentsCertificatesCountriesStats, certificatesCount }: Props) => {\n  const { countries } = studentsCertificatesCountriesStats;\n  return (\n    <Card title=\"Certificates Countries\">\n      <div style={{ height: 350, width: '100%' }}>\n        <CountriesChart\n          data={countries}\n          activeCount={certificatesCount}\n          xAxisTitle={'Number of Certificates'}\n          color={Colors.Lime}\n        />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsCertificatesCountriesCard/index.tsx",
    "content": "export { StudentsCertificatesCountriesCard } from './StudentsCertificatesCountriesCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsCountriesCard/StudentsCountriesCard.tsx",
    "content": "import { Card } from 'antd';\nimport { CountriesStatsDto } from '@client/api';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  studentsCountriesStats: CountriesStatsDto;\n  activeStudentsCount: number;\n};\n\nconst CountriesChart = dynamicWithSkeleton(() => import('../CountriesChart/CountriesChart'));\n\nexport const StudentsCountriesCard = ({ studentsCountriesStats, activeStudentsCount }: Props) => {\n  const { countries } = studentsCountriesStats;\n  return (\n    <Card title=\"Students Countries\">\n      <div style={{ height: 350, width: '100%' }}>\n        <CountriesChart data={countries} activeCount={activeStudentsCount} xAxisTitle={'Number of Students'} />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsCountriesCard/index.tsx",
    "content": "export { StudentsCountriesCard } from './StudentsCountriesCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsEligibleForCertificationCard/StudentsEligibleForCertificationCard.tsx",
    "content": "import { Card, Typography } from 'antd';\nimport { CourseStatsDto } from '@client/api';\nimport { Colors } from '../../data';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  studentsStats: CourseStatsDto;\n};\n\nconst { Text } = Typography;\n\nconst LiquidChart = dynamicWithSkeleton(() => import('../LiquidChart/LiquidChart'));\n\nexport const StudentsEligibleForCertificationCard = ({ studentsStats }: Props) => {\n  return (\n    <Card title=\"Eligible for Certification\">\n      <Text strong>\n        Eligible for Certification: {studentsStats.eligibleForCertificationCount} / {studentsStats.activeStudentsCount}\n      </Text>\n      <div style={{ height: 180, width: '100%' }}>\n        <LiquidChart\n          count={studentsStats.eligibleForCertificationCount}\n          total={studentsStats.activeStudentsCount}\n          color={Colors.Lime}\n        />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsEligibleForCertificationCard/index.tsx",
    "content": "export { StudentsEligibleForCertificationCard } from './StudentsEligibleForCertificationCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsStatsCard/StudentsStatsCard.tsx",
    "content": "import { Card, Typography } from 'antd';\nimport { CourseStatsDto } from '@client/api';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  studentsStats: CourseStatsDto;\n};\n\nconst { Text } = Typography;\n\nconst StudentsStatsChart = dynamicWithSkeleton(() => import('../LiquidChart/LiquidChart'));\n\nexport const StudentsStatsCard = ({ studentsStats }: Props) => {\n  return (\n    <Card title=\"Active Students\">\n      <Text strong>\n        Active Students: {studentsStats.activeStudentsCount} / {studentsStats.totalStudents}\n      </Text>\n      <div style={{ height: 180, width: '100%' }}>\n        <StudentsStatsChart count={studentsStats.activeStudentsCount} total={studentsStats.totalStudents} />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsStatsCard/index.tsx",
    "content": "export { StudentsStatsCard } from './StudentsStatsCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsWithCertificateCard/StudentsWithCertificateCard.tsx",
    "content": "import { Card, Typography } from 'antd';\nimport { CourseStatsDto } from '@client/api';\nimport { Colors } from '../../data';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  studentsStats: CourseStatsDto;\n};\n\nconst { Text } = Typography;\n\nconst LiquidChart = dynamicWithSkeleton(() => import('../LiquidChart/LiquidChart'));\n\nexport const StudentsWithCertificateCard = ({ studentsStats }: Props) => {\n  return (\n    <Card title=\"Students With Certificate\">\n      <Text strong>\n        Students With Certificate: {studentsStats.certifiedStudentsCount} / {studentsStats.activeStudentsCount}\n      </Text>\n      <div style={{ height: 180, width: '100%' }}>\n        <LiquidChart\n          count={studentsStats.certifiedStudentsCount}\n          total={studentsStats.activeStudentsCount}\n          color={Colors.Lime}\n        />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsWithCertificateCard/index.tsx",
    "content": "export { StudentsWithCertificateCard } from './StudentsWithCertificateCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsWithMentorsCard/StudentsWithMentorsCard.tsx",
    "content": "import { Card, Typography } from 'antd';\nimport { CourseStatsDto } from '@client/api';\nimport { Colors } from '../../data';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\ntype Props = {\n  studentsStats: CourseStatsDto;\n};\n\nconst { Text } = Typography;\n\nconst LiquidChart = dynamicWithSkeleton(() => import('../LiquidChart/LiquidChart'));\n\nexport const StudentsWithMentorsCard = ({ studentsStats }: Props) => {\n  return (\n    <Card title=\"Students With Mentor\">\n      <Text strong>\n        Students With Mentor: {studentsStats.studentsWithMentorCount} / {studentsStats.activeStudentsCount}\n      </Text>\n      <div style={{ height: 180, width: '100%' }}>\n        <LiquidChart\n          count={studentsStats.studentsWithMentorCount}\n          total={studentsStats.activeStudentsCount}\n          color={Colors.Gold}\n        />\n      </div>\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/StudentsWithMentorsCard/index.tsx",
    "content": "export { StudentsWithMentorsCard } from './StudentsWithMentorsCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/TaskPerformanceCard/TaskPerformanceCard.tsx",
    "content": "import { Card, Flex, Form, Image, Select, Typography } from 'antd';\nimport { CourseStatsApi, CourseTaskDto, TaskPerformanceStatsDto } from '@client/api';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { Colors, StudentPerformanceDescription, StudentPerformanceType } from '../../data';\nimport { PieConfig } from '@ant-design/plots';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\nconst courseStatsApi = new CourseStatsApi();\n\ntype Props = {\n  tasks: CourseTaskDto[];\n};\n\nconst { Text } = Typography;\n\nconst DonutChart = dynamicWithSkeleton(() => import('../DonutChart/DonutChart'));\n\nexport const TaskPerformanceCard = ({ tasks }: Props) => {\n  const { course } = useActiveCourseContext();\n\n  const [taskId, setTaskId] = useState<number>();\n\n  const { value: taskPerformanceStats } = useAsync(async () => {\n    if (taskId) {\n      const { data } = await courseStatsApi.getTaskPerformance(course.id, taskId);\n      return data;\n    }\n  }, [taskId]);\n\n  return (\n    <Card title=\"Task Performance\">\n      <Form>\n        <Form.Item name=\"courseTaskIds\">\n          <Select\n            placeholder=\"Select tasks\"\n            showSearch\n            optionFilterProp=\"label\"\n            onChange={(value: number) => setTaskId(value)}\n            options={tasks.map(({ name, id }) => ({\n              label: name,\n              value: id,\n            }))}\n          />\n        </Form.Item>\n      </Form>\n      <div style={{ height: 250, width: '100%' }}>\n        {taskPerformanceStats?.totalAchievement ? (\n          <DonutChart data={getChartData(taskPerformanceStats)} config={getChartConfig()} />\n        ) : (\n          <Flex vertical align=\"center\" justify=\"center\">\n            <Text>No data available for this task, please select another task.</Text>\n            <Image preview={false} src=\"/static/svg/err.svg\" alt=\"Error 404\" width={175} height={175} />\n          </Flex>\n        )}\n      </div>\n    </Card>\n  );\n};\n\nfunction getPerformanceDescriptionByType(type: string) {\n  switch (type) {\n    case StudentPerformanceType.Minimal:\n      return StudentPerformanceDescription.Minimal;\n    case StudentPerformanceType.Low:\n      return StudentPerformanceDescription.Low;\n    case StudentPerformanceType.Moderate:\n      return StudentPerformanceDescription.Moderate;\n    case StudentPerformanceType.High:\n      return StudentPerformanceDescription.High;\n    case StudentPerformanceType.Exceptional:\n      return StudentPerformanceDescription.Exceptional;\n    case StudentPerformanceType.Perfect:\n      return StudentPerformanceDescription.PerfectScore;\n    default:\n      return StudentPerformanceDescription.Unknown;\n  }\n}\n\nfunction getChartConfig(): Partial<PieConfig> {\n  return {\n    tooltip: {\n      items: [\n        (d: Record<string, string | number>) => ({\n          name: getPerformanceDescriptionByType(d.type as string),\n          value: d.value,\n        }),\n      ],\n    },\n    scale: {\n      color: {\n        range: [Colors.Volcano, Colors.Orange, Colors.Blue, Colors.Lime, Colors.Purple, Colors.Magenta],\n      },\n    },\n  };\n}\n\nfunction getChartData(taskPerformanceStats: TaskPerformanceStatsDto) {\n  return [\n    { type: StudentPerformanceType.Minimal, value: taskPerformanceStats.minimalAchievement },\n    { type: StudentPerformanceType.Low, value: taskPerformanceStats.lowAchievement },\n    { type: StudentPerformanceType.Moderate, value: taskPerformanceStats.moderateAchievement },\n    { type: StudentPerformanceType.High, value: taskPerformanceStats.highAchievement },\n    { type: StudentPerformanceType.Exceptional, value: taskPerformanceStats.exceptionalAchievement },\n    { type: StudentPerformanceType.Perfect, value: taskPerformanceStats.perfectScores },\n  ].filter(({ value }) => value > 0);\n}\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/components/TaskPerformanceCard/index.tsx",
    "content": "export { TaskPerformanceCard } from './TaskPerformanceCard';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/constants.ts",
    "content": "export enum StatScope {\n  Current = 'Current',\n  Timeline = 'Timeline',\n}\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/data.ts",
    "content": "export enum Colors {\n  Purple = '#9254de',\n  Gold = '#faad14',\n  Blue = '#1677ff',\n  Volcano = '#fa541c',\n  Lime = '#a0d911',\n  Orange = '#fa8c16',\n  Gray = '#d9d9d9',\n  Magenta = '#c41d7f',\n}\n\nexport enum StudentPerformanceDescription {\n  Minimal = 'Number of students scoring between 1% and 20% of the maximum points',\n  Low = 'Number of students scoring between 21% and 50% of the maximum points',\n  Moderate = 'Number of students scoring between 51% and 70% of the maximum points',\n  High = 'Number of students scoring between 71% and 90% of the maximum points',\n  Exceptional = 'Number of students scoring between 91% and 99% of the maximum points',\n  PerfectScore = 'Number of students achieving a perfect score of 100%',\n  Unknown = 'Unknown performance category',\n}\n\nexport enum StudentPerformanceType {\n  Minimal = 'Minimal',\n  Low = 'Low',\n  Moderate = 'Moderate',\n  High = 'High',\n  Exceptional = 'Exceptional',\n  Perfect = 'Perfect',\n}\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/hooks/index.ts",
    "content": "export { useCoursesStats } from './useCourseStats';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/hooks/useCourseStats.tsx",
    "content": "import { useMessage } from '@client/hooks';\nimport { CourseAggregateStatsDto, CourseStatsApi } from '@client/api';\nimport { useRequest } from 'ahooks';\n\nconst courseStatsApi = new CourseStatsApi();\n\ntype CourseStatsParams = {\n  ids?: number[];\n  year?: number;\n};\n\nasync function fetchCourseStats({\n  ids = [],\n  year = 0,\n}: CourseStatsParams): Promise<CourseAggregateStatsDto | undefined> {\n  try {\n    const { data } = await courseStatsApi.getCoursesStats(ids, year);\n    return data;\n  } catch (err) {\n    console.error(\"Couldn't get course(s) stats\", err);\n    throw err;\n  }\n}\n\nexport function useCoursesStats({ ids, year }: CourseStatsParams) {\n  const { message } = useMessage();\n\n  const { data, loading } = useRequest(() => fetchCourseStats({ ids, year }), {\n    ready: Boolean((ids && ids.length) || year),\n    refreshDeps: [ids, year],\n    retryCount: 3,\n    onError: () => {\n      message.error(\"Can't load courses data. Please try later.\");\n    },\n  });\n\n  return { loading, coursesData: data };\n}\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/index.tsx",
    "content": "export { default as CourseStatistics } from './pages/CourseStatistics';\n"
  },
  {
    "path": "client/src/modules/CourseStatistics/pages/CourseStatistics.tsx",
    "content": "import { PageLayout } from '@client/shared/components/PageLayout';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { DatePickerProps, Empty, theme } from 'antd';\nimport type { Dayjs } from 'dayjs';\nimport { useContext, useState } from 'react';\nimport { useCoursesStats } from '../hooks';\nimport { StatScope } from '../constants';\nimport { StatScopeSelector } from '../components/StatScopeSelector';\nimport { StatCards } from '../components/StatCards';\nimport { useRouter, useSearchParams } from 'next/navigation';\nimport dayjs from 'dayjs';\n\nfunction CourseStatistic() {\n  const { isAdmin } = useContext(SessionContext);\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const params = new URLSearchParams(searchParams.toString());\n  const [statScope, setStatScope] = useState<StatScope>(params.get('course') ? StatScope.Current : StatScope.Timeline);\n  const { course } = useActiveCourseContext();\n  const { token } = theme.useToken();\n  const [ids, setIds] = useState<number[]>([course.id]);\n  const selectedYear = Number(params.get('year'));\n  const { loading, coursesData } = useCoursesStats({ ids, year: selectedYear });\n\n  const handleStatScope = (value: boolean) => {\n    if (value) {\n      setStatScope(StatScope.Current);\n      setIds([course.id]);\n      params.set('course', course.alias);\n      params.delete('year');\n    } else {\n      setStatScope(StatScope.Timeline);\n      setIds([]);\n      params.delete('course');\n      params.set('year', dayjs(new Date()).year().toString());\n    }\n    router.push(`?${params.toString()}`);\n  };\n\n  const handleYearSelection: DatePickerProps<Dayjs>['onChange'] = date => {\n    if (!date || Array.isArray(date)) {\n      return;\n    }\n    params.set('year', date.year().toString());\n    router.push(`?${params.toString()}`);\n  };\n\n  return (\n    <PageLayout\n      loading={loading}\n      title={`Course${statScope === StatScope.Timeline ? 's' : ''} Statistics`}\n      showCourseName={statScope === StatScope.Current}\n      background={token.colorBgLayout}\n    >\n      {isAdmin && (\n        <StatScopeSelector\n          statScope={statScope}\n          handleStatScope={handleStatScope}\n          handleYearSelection={handleYearSelection}\n          selectedYear={selectedYear}\n        />\n      )}\n      {statScope === StatScope.Timeline && !selectedYear ? (\n        <Empty description=\"No data available.\" />\n      ) : (\n        <StatCards coursesData={coursesData} />\n      )}\n    </PageLayout>\n  );\n}\n\nexport default CourseStatistic;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/AddCriteriaForCrossCheck.tsx",
    "content": "import { Button, Form, Input, InputNumber } from 'antd';\nimport { ChangeEventHandler, useMemo, useState } from 'react';\nimport { CrossCheckCriteriaType, IAddCriteriaForCrossCheck } from '@client/services/course';\nimport { CriteriaTypeSelect } from './CriteriaTypeSelect';\nimport { TaskType } from './constants';\nimport { useMessage } from '@client/hooks';\n\nconst { Item } = Form;\nconst { TextArea } = Input;\n\nexport const AddCriteriaForCrossCheck = ({ onCreate }: IAddCriteriaForCrossCheck) => {\n  const [type, setType] = useState<CrossCheckCriteriaType>('title');\n  const [max, setMax] = useState(0);\n  const [maxPenalty, setMaxPenalty] = useState(0);\n  const [text, setText] = useState('');\n  const DEFAULT_KEY = '0';\n  const DEFAULT_INDEX = 0;\n\n  const { message } = useMessage();\n\n  const clearInputs = () => {\n    setMax(0);\n    setMaxPenalty(0);\n    setText('');\n  };\n\n  const onSave = () => {\n    const criteriaDetails =\n      type === TaskType.Title\n        ? {\n            key: DEFAULT_KEY,\n            text: text,\n            type: type,\n            index: DEFAULT_INDEX,\n          }\n        : {\n            key: DEFAULT_KEY,\n            max: type === TaskType.Penalty ? -Math.abs(maxPenalty) : max,\n            text: text,\n            type: type,\n            index: DEFAULT_INDEX,\n          };\n    onCreate(criteriaDetails);\n    clearInputs();\n    message.success('Criteria added.');\n  };\n\n  function changeMax(value: number | null) {\n    setMax(value ?? 0);\n  }\n\n  function changeMaxPenalty(value: number | null) {\n    setMaxPenalty(value ?? 0);\n  }\n\n  function changeType(value: CrossCheckCriteriaType) {\n    setType(value);\n  }\n\n  const changeText: ChangeEventHandler<HTMLTextAreaElement> = event => {\n    setText(event.target.value);\n  };\n\n  const canSave: boolean = useMemo(() => {\n    if (type === TaskType.Title) {\n      return !!text;\n    }\n\n    if (type === TaskType.Penalty) {\n      return !!(text && maxPenalty);\n    }\n\n    return !!(text && max);\n  }, [type, max, maxPenalty, text]);\n\n  return (\n    <>\n      <Item label=\"Criteria Type\">\n        <CriteriaTypeSelect onChange={changeType} />\n      </Item>\n\n      {type === TaskType.Subtask && (\n        <Item label=\"Add Max Score\">\n          <InputNumber type=\"number\" value={max} min={0} step={1} onChange={changeMax} />\n        </Item>\n      )}\n\n      {type === TaskType.Penalty && (\n        <Item label=\"Add Max Penalty\">\n          <InputNumber type=\"number\" value={maxPenalty} min={0} step={1} onChange={changeMaxPenalty} />\n        </Item>\n      )}\n\n      <Item label=\"Add Text\">\n        <TextArea\n          rows={3}\n          style={{ maxWidth: 1200 }}\n          placeholder=\"Add description\"\n          value={text}\n          onChange={changeText}\n        ></TextArea>\n      </Item>\n\n      <div style={{ textAlign: 'right' }}>\n        <Button type=\"primary\" onClick={onSave} disabled={!canSave}>\n          Add New Criteria\n        </Button>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/CriteriaActions.tsx",
    "content": "import { FC } from 'react';\nimport { CriteriaDto } from '../../api';\nimport { Popconfirm, Typography, Space } from 'antd';\n\ninterface CriteriaActionsProps {\n  editing: boolean;\n  record: CriteriaDto;\n  editingKey: string;\n  save: (key: string) => void;\n  remove: (key: string) => void;\n  cancel: () => void;\n  edit: (record: CriteriaDto) => void;\n}\n\nexport const CriteriaActions: FC<CriteriaActionsProps> = ({\n  editing,\n  cancel,\n  remove,\n  edit,\n  save,\n  record,\n  editingKey,\n}) =>\n  editing ? (\n    <Space direction=\"horizontal\">\n      <Typography.Link onClick={() => save(record.key)}>Save</Typography.Link>\n      <Typography.Link type=\"secondary\" onClick={cancel}>\n        Cancel\n      </Typography.Link>\n    </Space>\n  ) : (\n    <Space direction=\"horizontal\">\n      <Typography.Link disabled={!!editingKey} onClick={() => edit(record)}>\n        Edit\n      </Typography.Link>\n      <Popconfirm\n        title=\"Are you sure to delete this criteria?\"\n        okText=\"Delete\"\n        okButtonProps={{\n          danger: true,\n        }}\n        onConfirm={() => remove(record.key)}\n      >\n        <Typography.Link disabled={!!editingKey} type=\"danger\">\n          Delete\n        </Typography.Link>\n      </Popconfirm>\n    </Space>\n  );\n"
  },
  {
    "path": "client/src/modules/CrossCheck/CriteriaTypeSelect.tsx",
    "content": "import { Select } from 'antd';\nimport { FC } from 'react';\nimport { TaskType } from './constants';\nimport { SelectProps } from 'antd/lib';\n\nconst options = Object.entries(TaskType).map(([label, value]) => ({ label, value }));\n\ntype CriteriaTypeSelectProps = SelectProps;\n\nexport const CriteriaTypeSelect: FC<CriteriaTypeSelectProps> = props => (\n  <Select placeholder=\"Select type\" {...props}>\n    {options.map(({ label, value }) => (\n      <Select.Option data-testid={label} value={value} key={label}>\n        {label}\n      </Select.Option>\n    ))}\n  </Select>\n);\n"
  },
  {
    "path": "client/src/modules/CrossCheck/DeleteAllCrossCheckCriteriaButton.tsx",
    "content": "import DeleteOutlined from '@ant-design/icons/DeleteOutlined';\nimport { Button, Popconfirm } from 'antd';\nimport { CriteriaDto } from '@client/api';\n\ninterface Props {\n  setDataCriteria: (criteria: CriteriaDto[]) => void;\n}\nexport function DeleteAllCrossCheckCriteriaButton({ setDataCriteria }: Props) {\n  const deleteAllCrossCheckCriteria = () => {\n    setDataCriteria([]);\n  };\n  return (\n    <div style={{ textAlign: 'right' }}>\n      <Popconfirm title=\"Are you sure you want to delete all items?\" onConfirm={deleteAllCrossCheckCriteria}>\n        <Button icon={<DeleteOutlined style={{ marginRight: 5 }} />} style={{ marginTop: 15 }} danger>\n          Delete all\n        </Button>\n      </Popconfirm>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/EditableCellForCrossCheck.tsx",
    "content": "import { CriteriaDto } from '@client/api';\nimport React from 'react';\nimport { EditableCriteriaInput } from './EditableCriteriaInput';\nimport { EditableTableColumnsDataIndex } from './constants';\nimport { theme } from 'antd';\n\ninterface EditableCellProps extends React.HTMLAttributes<HTMLElement> {\n  editing: boolean;\n  dataIndex: EditableTableColumnsDataIndex;\n  record: CriteriaDto;\n  index: number;\n  children: React.ReactNode;\n  onSelectChange: (value: string) => void;\n}\n\nexport const EditableCellForCrossCheck: React.FC<EditableCellProps> = ({\n  editing,\n  dataIndex,\n  children,\n  record,\n  onSelectChange,\n  ...props\n}) => {\n  const hasMax = record?.max !== 0;\n  const { token } = theme.useToken();\n\n  return (\n    <td\n      {...props}\n      title={hasMax ? '' : 'Check points for this line'}\n      style={{ color: hasMax ? token.colorTextBase : token.colorWarning }}\n    >\n      {editing ? (\n        <EditableCriteriaInput dataIndex={dataIndex} onSelectChange={onSelectChange} type={record?.type} />\n      ) : (\n        children\n      )}\n    </td>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/EditableCriteriaInput.tsx",
    "content": "import { Form, Input, InputNumber } from 'antd';\nimport { FC } from 'react';\nimport { CriteriaTypeSelect } from './CriteriaTypeSelect';\nimport { EditableTableColumnsDataIndex, TaskType } from './constants';\nimport { CriteriaDtoTypeEnum } from '@client/api';\n\ninterface EditableCriteriaInputProps {\n  type?: CriteriaDtoTypeEnum;\n  dataIndex: EditableTableColumnsDataIndex;\n  onSelectChange: (value: string) => void;\n}\n\nexport const EditableCriteriaInput: FC<EditableCriteriaInputProps> = ({ dataIndex, onSelectChange, type }) => {\n  switch (dataIndex) {\n    case EditableTableColumnsDataIndex.Max:\n      return type !== TaskType.Title ? (\n        <Form.Item name={dataIndex} style={{ margin: 0 }}>\n          <InputNumber style={{ width: 65 }} />\n        </Form.Item>\n      ) : null;\n    case EditableTableColumnsDataIndex.Type:\n      return (\n        <Form.Item name={dataIndex} style={{ margin: 0 }}>\n          <CriteriaTypeSelect onChange={onSelectChange} />\n        </Form.Item>\n      );\n    case EditableTableColumnsDataIndex.Text:\n      return (\n        <Form.Item name={dataIndex} style={{ margin: 0 }}>\n          <Input.TextArea rows={3} />\n        </Form.Item>\n      );\n    default:\n      return null;\n  }\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/EditableTableForCrossCheck.tsx",
    "content": "import { Form } from 'antd';\nimport type { DragEndEvent } from '@dnd-kit/core';\nimport { DndContext } from '@dnd-kit/core';\nimport { restrictToVerticalAxis } from '@dnd-kit/modifiers';\nimport { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';\nimport { Dispatch, SetStateAction, useState } from 'react';\nimport { EditableCellForCrossCheck } from './EditableCellForCrossCheck';\nimport { CriteriaDto, CriteriaDtoTypeEnum } from '@client/api';\nimport { CriteriaActions } from './CriteriaActions';\nimport { EditableTableColumnsDataIndex } from './constants';\nimport { DragSortTable } from './components/DragSortTable';\nimport { arrayMoveImmutable } from './utils/arrayMoveImmutable';\nimport { DragHandle } from './components/DragHandle';\n\ninterface IEditableTableProps {\n  dataCriteria: CriteriaDto[];\n  setDataCriteria: Dispatch<SetStateAction<CriteriaDto[]>>;\n}\n\nexport const EditableTable = ({ dataCriteria, setDataCriteria }: IEditableTableProps) => {\n  const [form] = Form.useForm();\n  const [editingKey, setEditingKey] = useState('');\n  const [originData, setOriginData] = useState<CriteriaDto[]>([]);\n\n  const isEditing = (record: CriteriaDto) => record.key === editingKey;\n\n  const edit = (record: CriteriaDto) => {\n    setOriginData(dataCriteria);\n    form.setFieldsValue(record);\n    setEditingKey(record.key);\n  };\n\n  const remove = (key: string) => {\n    setDataCriteria(dataCriteria.filter(item => item.key !== key));\n  };\n\n  const save = async (key: string) => {\n    const formData = await form.validateFields();\n    const newData = dataCriteria.map(criteria => (criteria.key === key ? { ...criteria, ...formData } : criteria));\n\n    setDataCriteria(newData);\n    setEditingKey('');\n  };\n\n  const cancel = () => {\n    setDataCriteria(originData);\n    setEditingKey('');\n  };\n\n  const changeTaskType = async (value: string) => {\n    const newData = dataCriteria.map(criteria =>\n      criteria.key === editingKey\n        ? {\n            ...criteria,\n            type: value as CriteriaDtoTypeEnum,\n            max: value === CriteriaDtoTypeEnum.Title ? undefined : criteria.max,\n          }\n        : criteria,\n    );\n    setDataCriteria(newData);\n  };\n\n  const onDragEnd = ({ active, over }: DragEndEvent) => {\n    if (active.id !== over?.id) {\n      setDataCriteria(previous => {\n        const activeIndex = previous.findIndex(i => i.key === active.id);\n        const overIndex = previous.findIndex(i => i.key === over?.id);\n        return arrayMoveImmutable(previous, activeIndex, overIndex);\n      });\n    }\n  };\n\n  const columns = [\n    {\n      title: '⇅',\n      dataIndex: 'drag',\n      width: 40,\n      align: 'center' as const,\n      render: (_: unknown, record: CriteriaDto) => <DragHandle id={record.key} />,\n    },\n    {\n      title: 'Type',\n      dataIndex: EditableTableColumnsDataIndex.Type,\n      width: '18%',\n      editable: true,\n    },\n    {\n      title: 'Max',\n      dataIndex: EditableTableColumnsDataIndex.Max,\n      width: '10%',\n      editable: true,\n    },\n    {\n      title: 'Text',\n      dataIndex: EditableTableColumnsDataIndex.Text,\n      width: '52%',\n      editable: true,\n    },\n    {\n      title: 'Actions',\n      dataIndex: EditableTableColumnsDataIndex.Actions,\n      width: '20%',\n      render: (_: unknown, record: CriteriaDto) => (\n        <CriteriaActions\n          editing={isEditing(record)}\n          record={record}\n          editingKey={editingKey}\n          cancel={cancel}\n          edit={edit}\n          remove={remove}\n          save={save}\n        />\n      ),\n    },\n  ];\n\n  const mergedColumns = columns.map(col => {\n    if (!col.editable) {\n      return col;\n    }\n\n    return {\n      ...col,\n      onCell: (record: CriteriaDto) => ({\n        record,\n        dataIndex: col.dataIndex,\n        title: col.title,\n        editing: isEditing(record),\n        onSelectChange: changeTaskType,\n      }),\n    };\n  });\n\n  return (\n    <Form form={form} component={false}>\n      <DndContext modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>\n        <SortableContext items={dataCriteria.map(i => i.key)} strategy={verticalListSortingStrategy}>\n          <DragSortTable\n            rowKey=\"key\"\n            components={{\n              body: {\n                cell: EditableCellForCrossCheck,\n              },\n            }}\n            style={{ wordBreak: 'break-word', fontStyle: 'normal' }}\n            size=\"small\"\n            dataSource={dataCriteria}\n            columns={mergedColumns}\n            rowClassName=\"editable-row\"\n            pagination={false}\n          />\n        </SortableContext>\n      </DndContext>\n    </Form>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/ExportJSONButton.tsx",
    "content": "import FileOutlined from '@ant-design/icons/FileOutlined';\nimport { Button } from 'antd';\nimport { CriteriaDto } from '@client/api';\nimport omit from 'lodash/omit';\nimport { CriteriaJSONType } from './UploadCriteriaJSON';\nimport { TaskType } from './constants';\n\ninterface Props {\n  dataCriteria: CriteriaDto[];\n}\n\nexport function ExportJSONButton({ dataCriteria }: Props) {\n  const transformCriteriaData = (criteria: CriteriaDto[]) => {\n    const transformedCriteria = criteria.map(item => {\n      let editedItem: Partial<CriteriaJSONType> = { ...item };\n\n      if (editedItem.type === TaskType.Title) {\n        editedItem.title = editedItem.text;\n        delete editedItem.text;\n      }\n      editedItem = omit(editedItem, ['key', 'index']);\n      return editedItem;\n    });\n    return { criteria: transformedCriteria };\n  };\n\n  const criteriaStringify = encodeURIComponent(JSON.stringify(transformCriteriaData(dataCriteria)));\n  const href = `data:text/json;charset=utf-8,${criteriaStringify}`;\n\n  return (\n    <div style={{ textAlign: 'right' }}>\n      <Button icon={<FileOutlined style={{ marginRight: 5 }} />} style={{ marginTop: 15 }}>\n        <a href={href} download=\"crossCheckCriteria.json\">\n          Export JSON\n        </a>\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/UploadCriteriaJSON.tsx",
    "content": "import { Button, Upload } from 'antd';\nimport { UploadOutlined } from '@ant-design/icons';\nimport { UploadChangeParam, UploadFile } from 'antd/lib/upload';\nimport { CriteriaDto } from '@client/api';\nimport { CrossCheckCriteriaType } from '@client/services/course';\nimport { TaskType } from './constants';\nimport { useMessage } from '@client/hooks';\n\ninterface IUploadCriteriaJSON {\n  onLoad: (data: CriteriaDto[]) => void;\n}\n\nexport type CriteriaJSONType = {\n  type: CrossCheckCriteriaType;\n  max?: number;\n  text?: string;\n  title?: string;\n};\n\nexport const UploadCriteriaJSON = ({ onLoad }: IUploadCriteriaJSON) => {\n  const { message } = useMessage();\n\n  const handleChange = (info: UploadChangeParam<UploadFile<any>>) => {\n    if (info.file.status === 'done') {\n      const fileReader = new FileReader();\n      fileReader.readAsText(info.file.originFileObj as Blob, 'UTF-8');\n      fileReader.onload = (e: Event) => {\n        const target = e.target as Element & { result: string };\n        const { criteria } = JSON.parse(target.result) as { criteria: CriteriaDto[] };\n        const transformedCriteria = criteria?.map((item: CriteriaJSONType) => {\n          if (item.type === TaskType.Title) {\n            return { type: item.type, text: item.title };\n          }\n          return item;\n        });\n        if (!transformedCriteria?.length) {\n          message.warning(`There is no criteria for downloading`);\n          return;\n        }\n        message.success(`${info.file.name} file uploaded successfully`);\n        onLoad(transformedCriteria as CriteriaDto[]);\n      };\n    }\n  };\n\n  return (\n    <Upload\n      data-testid=\"uploader\"\n      accept=\".JSON\"\n      onChange={handleChange}\n      // This is to override default behavior of the uploader (send request to the server)\n      // We don't need it, because we handle file client-side\n      customRequest={opts => opts.onSuccess?.(null)}\n    >\n      <Button\n        icon={<UploadOutlined />}\n        title='required format: \\n{ criteria: {\"type\": \"string\", \"max\": number, \"text\": \"string\"}}'\n      >\n        Click to Upload Criteria (JSON)\n      </Button>\n    </Upload>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/__tests__/AddCriteriaForCrossCheck.test.tsx",
    "content": "import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { AddCriteriaForCrossCheck } from '../AddCriteriaForCrossCheck';\nimport userEvent from '@testing-library/user-event';\n\nconst addCriteria = vi.fn();\n\ndescribe('AddCriteriaForCrossCheck', () => {\n  test('should match shapshot', () => {\n    const view = render(<AddCriteriaForCrossCheck onCreate={addCriteria} />);\n    expect(view).toMatchSnapshot();\n  });\n\n  test('should render \"Add New Criteria\" button', () => {\n    render(<AddCriteriaForCrossCheck onCreate={addCriteria} />);\n    const element = screen.getByText(/Add New Criteria/i);\n    expect(element).toBeInTheDocument();\n  });\n\n  test('should call addCriteria when \"Add new criteria\" button was clicked', async () => {\n    render(<AddCriteriaForCrossCheck onCreate={addCriteria} />);\n    const selectCriteriaType = screen.getByRole('combobox');\n    fireEvent.mouseDown(selectCriteriaType);\n    const optionTitle = screen.getByTestId('Title');\n    fireEvent.click(optionTitle);\n\n    const descriptionInput = screen.getByPlaceholderText('Add description');\n    fireEvent.change(descriptionInput, { target: { value: 'test' } });\n\n    const button = screen.getByRole('button', { name: /Add New Criteria/i });\n    fireEvent.click(button);\n\n    await waitFor(() => {\n      expect(addCriteria).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  test('should render textarea', () => {\n    render(<AddCriteriaForCrossCheck onCreate={addCriteria} />);\n\n    const textarea = screen.getByPlaceholderText('Add description');\n    expect(textarea).toBeInTheDocument();\n  });\n\n  test('should change textarea value on typing', async () => {\n    const expectedString = 'test value';\n    render(<AddCriteriaForCrossCheck onCreate={addCriteria} />);\n\n    const textarea = screen.getByPlaceholderText<HTMLInputElement>('Add description');\n    await userEvent.type(textarea, expectedString);\n\n    expect(textarea.value).toEqual(expectedString);\n  });\n\n  test('should select criteria', async () => {\n    render(<AddCriteriaForCrossCheck onCreate={addCriteria} />);\n    const selectCriteriaType = screen.getByRole('combobox');\n    expect(selectCriteriaType).toBeInTheDocument();\n    fireEvent.mouseDown(selectCriteriaType);\n\n    const element = screen.getByRole('option', { name: 'Subtask' });\n    expect(element).toBeInTheDocument();\n  });\n\n  test('input with adding max score renders only after user select criteria type Subtask', async () => {\n    render(<AddCriteriaForCrossCheck onCreate={addCriteria} />);\n    const selectCriteriaType = screen.getByRole('combobox');\n\n    const inputMaxScore = screen.queryByLabelText('Add Max Score');\n    expect(inputMaxScore).not.toBeInTheDocument();\n\n    fireEvent.mouseDown(selectCriteriaType);\n    const optionSubtask = screen.getByTestId('Subtask');\n    fireEvent.click(optionSubtask);\n    expect(screen.getByText('Add Max Score')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/CrossCheck/__tests__/ExportJSONButton.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { CriteriaDto, CriteriaDtoTypeEnum } from '@client/api';\nimport { ExportJSONButton } from '../ExportJSONButton';\n\nconst dataCriteria = [\n  {\n    key: '0',\n    index: 0,\n    type: CriteriaDtoTypeEnum.Title,\n    text: 'Its title',\n  },\n  {\n    key: '1',\n    index: 1,\n    type: CriteriaDtoTypeEnum.Subtask,\n    text: 'Its subtask',\n    max: 10,\n  },\n  {\n    key: '2',\n    index: 2,\n    type: CriteriaDtoTypeEnum.Penalty,\n    text: 'Its penalty',\n    max: -5,\n  },\n] as CriteriaDto[];\n\ndescribe('ExportJSONButton', () => {\n  test('contains following text', () => {\n    render(<ExportJSONButton dataCriteria={dataCriteria} />);\n    expect(screen.getByText('Export JSON')).toBeInTheDocument();\n  });\n\n  test('should render correctly with empty dataCriteria', () => {\n    render(<ExportJSONButton dataCriteria={[]} />);\n    const link = screen.getByRole('link');\n    expect(link).toHaveAttribute('download', 'crossCheckCriteria.json');\n  });\n});\n"
  },
  {
    "path": "client/src/modules/CrossCheck/__tests__/UploadCriteriaJSON.test.tsx",
    "content": "import { UploadCriteriaJSON } from '../UploadCriteriaJSON';\nimport { fireEvent, render, screen, waitFor } from '@testing-library/react';\n\nconst onLoad = vi.fn();\n\ndescribe('UploadCriteriaJSON', () => {\n  test('contains following element', () => {\n    render(<UploadCriteriaJSON onLoad={onLoad} />);\n    const element = screen.getByText('Click to Upload Criteria (JSON)');\n    expect(element).toBeInTheDocument();\n  });\n\n  test('upload file', async () => {\n    render(<UploadCriteriaJSON onLoad={onLoad} />);\n    global.URL.createObjectURL = vi.fn();\n\n    const file = new File(['{test: 1}'], 'test.json', { type: 'application/json' });\n    const input = screen.getByTestId('uploader') as HTMLInputElement;\n\n    fireEvent.change(input, { target: { files: [file] } });\n\n    await waitFor(() => {\n      expect(input.files).toHaveLength(1);\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/CrossCheck/__tests__/__snapshots__/AddCriteriaForCrossCheck.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`AddCriteriaForCrossCheck > should match shapshot 1`] = `\n{\n  \"asFragment\": [Function],\n  \"baseElement\": <body>\n    <div>\n      <div\n        class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n      >\n        <div\n          class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-col ant-form-item-label css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <label\n              class=\"\"\n              title=\"Criteria Type\"\n            >\n              Criteria Type\n            </label>\n          </div>\n          <div\n            class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <div\n              class=\"ant-form-item-control-input\"\n            >\n              <div\n                class=\"ant-form-item-control-input-content\"\n              >\n                <div\n                  class=\"ant-select ant-select-outlined ant-select-in-form-item css-var-root ant-select-css-var css-dev-only-do-not-override-1enej14 ant-select-single ant-select-show-arrow\"\n                >\n                  <div\n                    class=\"ant-select-content\"\n                  >\n                    <div\n                      class=\"ant-select-placeholder\"\n                      style=\"visibility: visible;\"\n                    >\n                      Select type\n                    </div>\n                    <input\n                      aria-autocomplete=\"list\"\n                      aria-expanded=\"false\"\n                      aria-haspopup=\"listbox\"\n                      autocomplete=\"off\"\n                      class=\"ant-select-input\"\n                      id=\"test-id\"\n                      readonly=\"\"\n                      role=\"combobox\"\n                      type=\"search\"\n                      value=\"\"\n                    />\n                  </div>\n                  <div\n                    class=\"ant-select-suffix\"\n                  >\n                    <span\n                      aria-label=\"down\"\n                      class=\"anticon anticon-down\"\n                      role=\"img\"\n                    >\n                      <svg\n                        aria-hidden=\"true\"\n                        data-icon=\"down\"\n                        fill=\"currentColor\"\n                        focusable=\"false\"\n                        height=\"1em\"\n                        viewBox=\"64 64 896 896\"\n                        width=\"1em\"\n                      >\n                        <path\n                          d=\"M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z\"\n                        />\n                      </svg>\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div\n        class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n      >\n        <div\n          class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-col ant-form-item-label css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <label\n              class=\"\"\n              title=\"Add Text\"\n            >\n              Add Text\n            </label>\n          </div>\n          <div\n            class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <div\n              class=\"ant-form-item-control-input\"\n            >\n              <div\n                class=\"ant-form-item-control-input-content\"\n              >\n                <textarea\n                  class=\"ant-input css-dev-only-do-not-override-1enej14 ant-input-outlined css-var-root ant-input-css-var\"\n                  placeholder=\"Add description\"\n                  rows=\"3\"\n                  style=\"max-width: 1200px;\"\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div\n        style=\"text-align: right;\"\n      >\n        <button\n          class=\"ant-btn css-dev-only-do-not-override-1enej14 css-var-root ant-btn-primary ant-btn-color-primary ant-btn-variant-solid\"\n          disabled=\"\"\n          type=\"button\"\n        >\n          <span>\n            Add New Criteria\n          </span>\n        </button>\n      </div>\n    </div>\n  </body>,\n  \"container\": <div>\n    <div\n      class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n    >\n      <div\n        class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          class=\"ant-col ant-form-item-label css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <label\n            class=\"\"\n            title=\"Criteria Type\"\n          >\n            Criteria Type\n          </label>\n        </div>\n        <div\n          class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-form-item-control-input\"\n          >\n            <div\n              class=\"ant-form-item-control-input-content\"\n            >\n              <div\n                class=\"ant-select ant-select-outlined ant-select-in-form-item css-var-root ant-select-css-var css-dev-only-do-not-override-1enej14 ant-select-single ant-select-show-arrow\"\n              >\n                <div\n                  class=\"ant-select-content\"\n                >\n                  <div\n                    class=\"ant-select-placeholder\"\n                    style=\"visibility: visible;\"\n                  >\n                    Select type\n                  </div>\n                  <input\n                    aria-autocomplete=\"list\"\n                    aria-expanded=\"false\"\n                    aria-haspopup=\"listbox\"\n                    autocomplete=\"off\"\n                    class=\"ant-select-input\"\n                    id=\"test-id\"\n                    readonly=\"\"\n                    role=\"combobox\"\n                    type=\"search\"\n                    value=\"\"\n                  />\n                </div>\n                <div\n                  class=\"ant-select-suffix\"\n                >\n                  <span\n                    aria-label=\"down\"\n                    class=\"anticon anticon-down\"\n                    role=\"img\"\n                  >\n                    <svg\n                      aria-hidden=\"true\"\n                      data-icon=\"down\"\n                      fill=\"currentColor\"\n                      focusable=\"false\"\n                      height=\"1em\"\n                      viewBox=\"64 64 896 896\"\n                      width=\"1em\"\n                    >\n                      <path\n                        d=\"M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z\"\n                      />\n                    </svg>\n                  </span>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div\n      class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n    >\n      <div\n        class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n      >\n        <div\n          class=\"ant-col ant-form-item-label css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <label\n            class=\"\"\n            title=\"Add Text\"\n          >\n            Add Text\n          </label>\n        </div>\n        <div\n          class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-form-item-control-input\"\n          >\n            <div\n              class=\"ant-form-item-control-input-content\"\n            >\n              <textarea\n                class=\"ant-input css-dev-only-do-not-override-1enej14 ant-input-outlined css-var-root ant-input-css-var\"\n                placeholder=\"Add description\"\n                rows=\"3\"\n                style=\"max-width: 1200px;\"\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <div\n      style=\"text-align: right;\"\n    >\n      <button\n        class=\"ant-btn css-dev-only-do-not-override-1enej14 css-var-root ant-btn-primary ant-btn-color-primary ant-btn-variant-solid\"\n        disabled=\"\"\n        type=\"button\"\n      >\n        <span>\n          Add New Criteria\n        </span>\n      </button>\n    </div>\n  </div>,\n  \"debug\": [Function],\n  \"findAllByAltText\": [Function],\n  \"findAllByDisplayValue\": [Function],\n  \"findAllByLabelText\": [Function],\n  \"findAllByPlaceholderText\": [Function],\n  \"findAllByRole\": [Function],\n  \"findAllByTestId\": [Function],\n  \"findAllByText\": [Function],\n  \"findAllByTitle\": [Function],\n  \"findByAltText\": [Function],\n  \"findByDisplayValue\": [Function],\n  \"findByLabelText\": [Function],\n  \"findByPlaceholderText\": [Function],\n  \"findByRole\": [Function],\n  \"findByTestId\": [Function],\n  \"findByText\": [Function],\n  \"findByTitle\": [Function],\n  \"getAllByAltText\": [Function],\n  \"getAllByDisplayValue\": [Function],\n  \"getAllByLabelText\": [Function],\n  \"getAllByPlaceholderText\": [Function],\n  \"getAllByRole\": [Function],\n  \"getAllByTestId\": [Function],\n  \"getAllByText\": [Function],\n  \"getAllByTitle\": [Function],\n  \"getByAltText\": [Function],\n  \"getByDisplayValue\": [Function],\n  \"getByLabelText\": [Function],\n  \"getByPlaceholderText\": [Function],\n  \"getByRole\": [Function],\n  \"getByTestId\": [Function],\n  \"getByText\": [Function],\n  \"getByTitle\": [Function],\n  \"queryAllByAltText\": [Function],\n  \"queryAllByDisplayValue\": [Function],\n  \"queryAllByLabelText\": [Function],\n  \"queryAllByPlaceholderText\": [Function],\n  \"queryAllByRole\": [Function],\n  \"queryAllByTestId\": [Function],\n  \"queryAllByText\": [Function],\n  \"queryAllByTitle\": [Function],\n  \"queryByAltText\": [Function],\n  \"queryByDisplayValue\": [Function],\n  \"queryByLabelText\": [Function],\n  \"queryByPlaceholderText\": [Function],\n  \"queryByRole\": [Function],\n  \"queryByTestId\": [Function],\n  \"queryByText\": [Function],\n  \"queryByTitle\": [Function],\n  \"rerender\": [Function],\n  \"unmount\": [Function],\n}\n`;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/CriteriaForm.tsx",
    "content": "import { Col, Row, Typography, Rate, Input, Avatar, Card } from 'antd';\nimport { Comment } from '@client/components/Comment';\nimport { useCallback } from 'react';\nimport { FrownTwoTone, MehTwoTone, SmileTwoTone } from '@ant-design/icons';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\n\nimport { CrossCheckCriteria, CrossCheckComment } from '@client/services/course';\n\nfunction RateIcon({ index, value }: { index: number; value: number }) {\n  const color = index + 1 <= value ? colors[index] : '#aaa';\n  switch (index) {\n    case 0:\n      return <FrownTwoTone style={{ fontSize: 20 }} twoToneColor={color} />;\n    case 1:\n      return <MehTwoTone style={{ fontSize: 20 }} twoToneColor={color} />;\n    default:\n      return <SmileTwoTone style={{ fontSize: 20 }} twoToneColor={color} />;\n  }\n}\n\nconst colors = ['red', 'orange', '#52c41a'];\n\ntype Review = {\n  percentage: number;\n  criteriaId: string;\n};\n\nenum ReviewValue {\n  No = 'No',\n  Partial = 'Partial',\n  Done = 'Done',\n}\n\ntype Props = {\n  authorId: number;\n  authorGithubId?: string;\n  comments: CrossCheckComment[];\n  criteria: CrossCheckCriteria[];\n  selfReview?: Review[];\n  value?: Review[];\n  onChange?: (review: Review[], comments: CrossCheckComment[]) => void;\n  reviewComments: CrossCheckComment[];\n};\n\nexport function CriteriaForm({ authorId, comments, reviewComments, criteria, onChange, selfReview, value }: Props) {\n  const hasSelfReview = selfReview != null;\n\n  const onReviewCriteria = useCallback(\n    (criteriaId: string, percentage: number) => {\n      const newReview: Review[] = criteria\n        .filter(d => d.type != 'title')\n        .map(d => ({\n          criteriaId: d.criteriaId,\n          percentage:\n            criteriaId === d.criteriaId\n              ? percentage\n              : (value?.find(v => v.criteriaId === d.criteriaId)?.percentage ?? 0),\n        }));\n      onChange?.(newReview, reviewComments);\n    },\n    [criteria, onChange, reviewComments, value],\n  );\n\n  const onReviewCommentChange = useCallback(\n    (criteriaId: string, text: string) => {\n      const newComments: CrossCheckComment[] = criteria\n        .filter(d => d.type != 'title')\n        .map(d => {\n          const comment = reviewComments?.find(v => v.authorId === authorId && v.criteriaId === d.criteriaId);\n          return {\n            authorId: authorId,\n            timestamp: comment?.timestamp ?? Date.now(),\n            criteriaId: d.criteriaId,\n            text: criteriaId === d.criteriaId ? text : (comment?.text ?? ''),\n          };\n        })\n        .filter(c => c.text);\n      onChange?.(value ?? [], newComments);\n    },\n    [criteria, onChange, value, reviewComments, authorId],\n  );\n\n  return criteria && criteria.length > 0 ? (\n    <>\n      {criteria.map((item, i) => {\n        if (item.type === 'title') {\n          return (\n            <div key={i}>\n              <Typography.Title style={{ color: '#444' }} key={item.title} level={3}>\n                {item.title}\n              </Typography.Title>\n            </div>\n          );\n        }\n        const currentReview = value?.find(r => r.criteriaId === item.criteriaId) ?? {\n          criteriaId: item.criteriaId,\n          percentage: 0,\n        };\n        const selfReviewPercentage = selfReview?.find(r => r.criteriaId === item.criteriaId)?.percentage ?? 0;\n\n        return (\n          <Card size=\"small\" key={i} style={{ marginBottom: 24 }}>\n            <div key={item.criteriaId}>\n              <div style={{ display: 'flex', marginBottom: 12, minHeight: 60 }}>\n                <div style={{ height: 60, paddingLeft: 12, paddingRight: 12 }}>\n                  <div style={{ fontSize: 24, fontWeight: 600 }}>\n                    <Avatar style={{ color: '#f56a00', backgroundColor: '#fde3cf' }} size={48}>\n                      {item.max}\n                    </Avatar>\n                  </div>\n                </div>\n                <Typography.Text style={{ flex: 1 }}>{item.text}</Typography.Text>\n              </div>\n              <Row>\n                <Col flex={1}>\n                  <div>\n                    <Typography.Text strong>Your Review</Typography.Text>\n                  </div>\n                  <div>\n                    <Rate\n                      character={props => (\n                        <RateIcon key={props.index ?? 0} value={props.value ?? 0} index={props.index ?? 0} />\n                      )}\n                      onChange={value => onReviewCriteria(item.criteriaId, convertValueToPercentage(value))}\n                      value={convertPercentageToValue(currentReview.percentage)}\n                      count={3}\n                      tooltips={[ReviewValue.No, ReviewValue.Partial, ReviewValue.Done]}\n                    />\n                  </div>\n                </Col>\n                {hasSelfReview && (\n                  <Col flex={1}>\n                    <div>\n                      <Typography.Text strong>Self Review</Typography.Text>\n                    </div>\n                    <div style={{ marginTop: 6 }}>\n                      <RateIcon\n                        value={convertPercentageToValue(selfReviewPercentage)}\n                        index={convertPercentageToValue(selfReviewPercentage) - 1}\n                      />\n                    </div>\n                  </Col>\n                )}\n              </Row>\n              <Row>\n                <Col flex={1}>\n                  <div style={{ marginTop: 16 }}>\n                    <Typography.Title level={5}>Comments</Typography.Title>\n                  </div>\n                  <div>\n                    {comments\n                      ?.filter(c => c.criteriaId === item.criteriaId)\n                      .map(c => (\n                        <Comment\n                          key={c.criteriaId}\n                          author={c.authorGithubId}\n                          avatar={<GithubAvatar githubId={c.authorGithubId} size={32} />}\n                          content={<p>{c.text || '-'}</p>}\n                          datetime={new Date(c.timestamp).toLocaleString()}\n                        />\n                      ))}\n                  </div>\n                  <div style={{ marginTop: 8 }}>\n                    <Typography.Text strong>Your comment</Typography.Text>\n                    <Input.TextArea onChange={event => onReviewCommentChange(item.criteriaId, event.target.value)} />\n                  </div>\n                </Col>\n              </Row>\n            </div>\n          </Card>\n        );\n      })}\n    </>\n  ) : null;\n}\n\nfunction convertPercentageToValue(percentage: number) {\n  if (percentage === 0) {\n    return 1;\n  }\n  if (percentage <= 0.5) {\n    return 2;\n  }\n  return 3;\n}\n\nfunction convertValueToPercentage(value: number) {\n  if (value === 1) {\n    return 0;\n  }\n  if (value === 2) {\n    return 0.5;\n  }\n  return 1;\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/CrossCheckAssignmentLink.tsx",
    "content": "import { Typography } from 'antd';\nimport { StudentBasic } from '@client/services/models';\nimport { StudentDiscord } from '@client/components/StudentDiscord';\n\nexport type AssignmentLink = { student: StudentBasic; url: string };\n\nexport function CrossCheckAssignmentLink({ assignment }: { assignment?: AssignmentLink }) {\n  if (!assignment) {\n    return null;\n  }\n\n  const {\n    student: { discord },\n    url: solutionUrl,\n  } = assignment;\n\n  return (\n    <div style={{ marginTop: 16 }}>\n      <StudentDiscord discord={discord} textPrefix=\"Student Discord:\" />\n      <Typography.Paragraph style={{ marginTop: 14 }}>\n        Solution:{' '}\n        <Typography.Link target=\"_blank\" href={solutionUrl}>\n          {solutionUrl}\n        </Typography.Link>\n      </Typography.Paragraph>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/CrossCheckCriteriaForm.module.css",
    "content": ".skipModal {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/CrossCheckCriteriaForm.tsx",
    "content": "import { useEffect } from 'react';\nimport { Typography, InputNumber, Button, Modal, List } from 'antd';\nimport { isEqual } from 'lodash';\n\nimport { SubtaskCriteria } from './criteria/SubtaskCriteria';\nimport { TitleCriteria } from './criteria/TitleCriteria';\nimport { PenaltyCriteria } from './criteria/PenaltyCriteria';\nimport { CrossCheckCriteriaDataDto, CrossCheckSolutionReviewDto } from '@client/api';\nimport { TaskType } from '../constants';\nimport styles from './CrossCheckCriteriaForm.module.css';\n\nconst { Text, Title } = Typography;\n\nexport interface CriteriaFormProps {\n  maxScore: number | undefined;\n  score: number;\n  setScore: (value: number) => void;\n  criteriaData: CrossCheckCriteriaDataDto[];\n  setCriteriaData: (newData: CrossCheckCriteriaDataDto[]) => void;\n  initialData?: CrossCheckSolutionReviewDto;\n  setIsSkipped: (value: boolean) => void;\n  isSkipped: boolean;\n}\n\nexport function CrossCheckCriteriaForm({\n  maxScore,\n  score,\n  setScore,\n  criteriaData,\n  setCriteriaData,\n  initialData,\n  setIsSkipped,\n  isSkipped,\n}: CriteriaFormProps) {\n  const [modal, contextHolder] = Modal.useModal();\n\n  const maxScoreValue = maxScore ?? 100;\n  const maxScoreLabel = maxScoreValue ? ` (Max ${maxScoreValue} points)` : '';\n  const penaltyData: CrossCheckCriteriaDataDto[] =\n    criteriaData?.filter(item => item.type.toLowerCase() === TaskType.Penalty) ?? [];\n\n  useEffect(() => {\n    const criteria = initialData?.criteria ? [...initialData.criteria] : undefined;\n    const sortedInitialData = criteria?.sort((a, b) => (a.key > b.key ? 1 : -1));\n    const sortedCriteriaData = [...criteriaData].sort((a, b) => (a.key > b.key ? 1 : -1));\n\n    if (!isEqual(sortedInitialData, sortedCriteriaData)) {\n      const totalPoints = criteriaData.reduce((acc, criteria) => {\n        return criteria.point ? acc + criteria.point : acc;\n      }, 0);\n      setScore(totalPoints > 0 ? totalPoints : 0);\n    } else {\n      setScore(initialData?.score ?? 0);\n    }\n  }, [criteriaData, initialData]);\n\n  function updateCriteriaData(updatedEntry: CrossCheckCriteriaDataDto) {\n    const index = criteriaData.findIndex(item => item.key === updatedEntry.key);\n    const updatedData = [...criteriaData];\n    updatedData.splice(index, 1, updatedEntry);\n    setCriteriaData(updatedData);\n  }\n\n  const skipConfirmation = () => {\n    if (isSkipped) {\n      setIsSkipped(false);\n    } else {\n      modal.confirm({\n        onOk: () => setIsSkipped(true),\n        title: 'Skip Task for Checking',\n        okText: 'Yes, skip form',\n        cancelText: 'Back to review',\n        content: (\n          <>\n            <div className={styles.skipModal}>\n              <Text>Are you sure you want to skip cross check form?</Text>\n              <Text>Possible reasons:</Text>\n              <List\n                size=\"small\"\n                dataSource={['- Task not done (Submitted but empty)', '- Submitted broken link']}\n                renderItem={item => <List.Item>{item}</List.Item>}\n              />\n            </div>\n          </>\n        ),\n      });\n    }\n  };\n\n  return (\n    <div style={{ margin: '0 auto' }}>\n      {criteriaData?.length ? (\n        <Button style={{ marginBottom: '16px' }} type=\"primary\" onClick={skipConfirmation}>\n          {isSkipped ? 'Show' : 'Skip'} cross check form\n        </Button>\n      ) : null}\n      {contextHolder}\n      {!isSkipped && (\n        <div>\n          {!!criteriaData?.length && (\n            <>\n              <Title level={4}>Criteria</Title>\n              {criteriaData\n                ?.filter(\n                  (item: CrossCheckCriteriaDataDto) =>\n                    item.type.toLowerCase() === TaskType.Title || item.type.toLowerCase() === TaskType.Subtask,\n                )\n                .map((item: CrossCheckCriteriaDataDto) => {\n                  return item.type.toLowerCase() === TaskType.Title ? (\n                    <TitleCriteria key={item.key} titleData={item} />\n                  ) : (\n                    <SubtaskCriteria key={item.key} subtaskData={item} updateCriteriaData={updateCriteriaData} />\n                  );\n                })}\n            </>\n          )}\n          {!!penaltyData?.length && (\n            <>\n              <Title level={4}>Penalty</Title>\n              {penaltyData?.map((item: CrossCheckCriteriaDataDto) => (\n                <PenaltyCriteria key={item.key} penaltyData={item} updateCriteriaData={updateCriteriaData} />\n              ))}\n            </>\n          )}\n        </div>\n      )}\n      <Title level={4}>{maxScoreLabel}</Title>\n      <InputNumber\n        value={score}\n        onChange={num => setScore(Number(num || 0))}\n        step={1}\n        min={0}\n        max={maxScoreValue}\n        decimalSeparator={','}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/CrossCheckHistory.tsx",
    "content": "import { Dispatch, SetStateAction } from 'react';\nimport { ClockCircleOutlined, EditFilled, EditOutlined } from '@ant-design/icons';\nimport { Button, Col, Row, Spin, Tag, Timeline, Typography } from 'antd';\nimport { useSolutionReviewSettings } from '@client/modules/CrossCheck/hooks';\nimport { markdownLabel } from '@client/shared/components/Forms/PreparedComment';\nimport { SolutionReview } from '@client/modules/CrossCheck/components/SolutionReview';\nimport { SolutionReviewSettingsPanel } from '@client/modules/CrossCheck/components/SolutionReviewSettingsPanel';\nimport { CrossCheckMessageDtoRoleEnum, CrossCheckSolutionReviewDto } from '@client/api';\nimport { useMessage } from '@client/hooks';\n\ntype CrossCheckHistoryState = {\n  loading: boolean;\n  data: CrossCheckSolutionReviewDto[];\n};\n\ntype Props = {\n  sessionId: number;\n  courseId: number;\n  courseTaskId: number | null;\n  state: CrossCheckHistoryState;\n  sessionGithubId: string;\n  maxScore: number | undefined;\n  setHistoricalCommentSelected: Dispatch<SetStateAction<string>>;\n};\n\nexport function CrossCheckHistory(props: Props) {\n  const { notification } = useMessage();\n  const courseTaskId = props.courseTaskId;\n  const solutionReviewSettings = useSolutionReviewSettings();\n\n  const handleClickAmendButton = (historicalComment: string) => {\n    const commentWithoutMarkdownLabel = historicalComment.slice(markdownLabel.length);\n    props.setHistoricalCommentSelected(commentWithoutMarkdownLabel);\n\n    notification.success({ message: 'Pasted to comment field', duration: 2 });\n  };\n\n  return (\n    <Spin spinning={props.state.loading}>\n      <Typography.Title style={{ marginTop: 24 }} level={4}>\n        History\n      </Typography.Title>\n\n      {props.state.data.length > 0 && (\n        <Row style={{ marginBottom: '16px' }}>\n          <Col>\n            <SolutionReviewSettingsPanel settings={solutionReviewSettings} />\n          </Col>\n        </Row>\n      )}\n\n      <Timeline>\n        {props.state.data.map((review, index) => {\n          const isActiveReview = index === 0;\n\n          return (\n            <Timeline.Item\n              key={index}\n              color={isActiveReview ? 'green' : 'gray'}\n              dot={<ClockCircleOutlined style={{ fontSize: '16px' }} />}\n            >\n              <Row>\n                <Col>{isActiveReview ? <Tag color=\"success\">active review</Tag> : <Tag>outdated review</Tag>}</Col>\n\n                {review.author && (\n                  <Col>\n                    <Tag color={isActiveReview ? 'warning' : ''}>your name is visible</Tag>\n                  </Col>\n                )}\n              </Row>\n\n              <Row>\n                <Col span={24}>\n                  <SolutionReview\n                    sessionId={props.sessionId}\n                    sessionGithubId={props.sessionGithubId}\n                    courseId={props.courseId}\n                    reviewNumber={0}\n                    settings={solutionReviewSettings}\n                    courseTaskId={courseTaskId}\n                    review={review}\n                    isActiveReview={isActiveReview}\n                    isMessageSendingPanelVisible={isActiveReview}\n                    currentRole={CrossCheckMessageDtoRoleEnum.Reviewer}\n                    maxScore={props.maxScore}\n                  >\n                    <Row style={{ marginTop: 16 }}>\n                      <Col>\n                        <Button\n                          size=\"middle\"\n                          type={isActiveReview ? 'primary' : 'default'}\n                          htmlType=\"button\"\n                          icon={isActiveReview ? <EditFilled /> : <EditOutlined />}\n                          onClick={() => handleClickAmendButton(review.comment)}\n                        >\n                          Amend comment\n                        </Button>\n                      </Col>\n                    </Row>\n                  </SolutionReview>\n                </Col>\n              </Row>\n            </Timeline.Item>\n          );\n        })}\n      </Timeline>\n    </Spin>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/DragHandle.module.css",
    "content": ".dragHandle {\n  cursor: grab;\n}\n\n.dragHandle:active {\n  cursor: grabbing;\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/DragHandle.tsx",
    "content": "import { useSortable } from '@dnd-kit/sortable';\nimport { MenuOutlined } from '@ant-design/icons';\n\nimport { Button } from 'antd';\nimport styles from './DragHandle.module.css';\n\nexport const DragHandle = ({ id }: { id: string }) => {\n  const { attributes, listeners, setActivatorNodeRef } = useSortable({ id });\n\n  return (\n    <span ref={setActivatorNodeRef} {...attributes} {...listeners}>\n      <Button className={styles.dragHandle} type=\"text\" icon={<MenuOutlined />} />\n    </span>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/DragSortTable.tsx",
    "content": "import { Table } from 'antd';\nimport type { TableProps } from 'antd';\nimport { useSortable } from '@dnd-kit/sortable';\nimport { CSS } from '@dnd-kit/utilities';\nimport React from 'react';\n\ninterface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {\n  'data-row-key': string;\n}\n\nconst DragSortTableRow = ({ children, ...props }: RowProps) => {\n  const { attributes, setNodeRef, transform, transition, isDragging } = useSortable({\n    id: props['data-row-key'],\n  });\n\n  const style: React.CSSProperties = {\n    ...props.style,\n    transform: CSS.Transform.toString(transform),\n    transition,\n    ...(isDragging ? { position: 'relative', zIndex: 9999 } : {}),\n  };\n\n  return (\n    <tr {...props} ref={setNodeRef} style={style} {...attributes}>\n      {children}\n    </tr>\n  );\n};\n\nexport const DragSortTable = <T extends object>(props: TableProps<T>) => {\n  return (\n    <Table\n      {...props}\n      components={{\n        ...props.components,\n        body: {\n          ...props.components?.body,\n          row: DragSortTableRow,\n        },\n      }}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/Message/Message.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport Message, { MessageProps } from './Message';\nimport { CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nconst messageProps: MessageProps = {\n  reviewNumber: 1,\n  message: {\n    timestamp: '2022-03-15T00:00:00.000Z',\n    content: 'Lorem ipsum',\n    author: { id: 12, githubId: 'John Doe' },\n    role: CrossCheckMessageDtoRoleEnum.Student,\n    isReviewerRead: true,\n    isStudentRead: true,\n  },\n  currentRole: CrossCheckMessageDtoRoleEnum.Student,\n  settings: {\n    areContactsVisible: true,\n  },\n};\n\ndescribe('Message', () => {\n  test('Should render author name', () => {\n    render(<Message {...messageProps} />);\n\n    expect(screen.getByText('John Doe')).toBeInTheDocument();\n  });\n\n  test('Should render formatted timestamp', () => {\n    render(<Message {...messageProps} />);\n\n    expect(screen.getByText('2022-03-15 00:00')).toBeInTheDocument();\n  });\n\n  test('Should render role tag', () => {\n    render(<Message {...messageProps} />);\n\n    expect(screen.getByText(messageProps.message.role)).toBeInTheDocument();\n  });\n\n  test('Should render prepared comment with correct content', () => {\n    render(<Message {...messageProps} />);\n\n    const comment = screen.getByText('Lorem ipsum');\n    expect(comment).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/Message/Message.tsx",
    "content": "import { CheckOutlined } from '@ant-design/icons';\nimport { Badge, Col, Row, Tag, Tooltip, Typography } from 'antd';\nimport { Comment } from '@client/components/Comment';\nimport { formatDateTime } from '@client/services/formatter';\nimport { ROLE_TAG_COLOR, SolutionReviewSettings } from '@client/modules/CrossCheck/constants';\nimport PreparedComment from '@client/shared/components/Forms/PreparedComment';\nimport { UserAvatar } from '../UserAvatar';\nimport { Username } from '../Username';\nimport { CrossCheckMessageDto, CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nconst { Text } = Typography;\n\nexport type MessageProps = {\n  reviewNumber?: number;\n  message: CrossCheckMessageDto;\n  currentRole: CrossCheckMessageDtoRoleEnum;\n  settings: SolutionReviewSettings;\n};\n\nfunction Message(props: MessageProps) {\n  const { reviewNumber, message, settings } = props;\n  const { timestamp, content, author, role, isReviewerRead, isStudentRead } = message;\n  const { areContactsVisible } = settings;\n  const isBadgeDotVisible = getBadgeDotVisibility(props);\n\n  return (\n    <Comment\n      avatar={\n        <Tooltip title={isBadgeDotVisible ? 'Unread message' : ''} placement=\"topLeft\">\n          <Badge dot={isBadgeDotVisible}>\n            <UserAvatar author={author} role={role} areContactsVisible={areContactsVisible} size={24} />\n          </Badge>\n        </Tooltip>\n      }\n      content={\n        <>\n          <Row>\n            <Col>\n              <Username\n                reviewNumber={reviewNumber ?? 0}\n                author={author}\n                role={role}\n                areContactsVisible={settings.areContactsVisible}\n              />\n            </Col>\n          </Row>\n\n          <Row>\n            <Col>\n              <Tag color={ROLE_TAG_COLOR[role]}>{role}</Tag>\n            </Col>\n          </Row>\n\n          <Row gutter={16} style={{ marginBottom: 8 }}>\n            <Col>\n              <Text type=\"secondary\" style={{ marginBottom: 8, fontSize: 12 }}>\n                {formatDateTime(timestamp)}\n              </Text>\n            </Col>\n\n            {(isReviewerRead || isStudentRead) && (\n              <Row>\n                {isReviewerRead && (\n                  <Col>\n                    <Tooltip title={'Reviewer read this message'} placement=\"bottom\">\n                      <CheckOutlined style={{ color: '#0f8ee8' }} />\n                    </Tooltip>\n                  </Col>\n                )}\n\n                {isStudentRead && (\n                  <Col>\n                    <Tooltip title={'Student read this message'} placement=\"bottom\">\n                      <CheckOutlined style={{ color: '#52C41A' }} />\n                    </Tooltip>\n                  </Col>\n                )}\n              </Row>\n            )}\n          </Row>\n\n          <Row>\n            <Col>\n              <PreparedComment text={content} />\n            </Col>\n          </Row>\n        </>\n      }\n    />\n  );\n}\n\nfunction getBadgeDotVisibility(props: MessageProps): boolean {\n  const { message, currentRole } = props;\n  const { isReviewerRead, isStudentRead } = message;\n\n  switch (currentRole) {\n    case CrossCheckMessageDtoRoleEnum.Reviewer:\n      return !isReviewerRead;\n\n    case CrossCheckMessageDtoRoleEnum.Student:\n      return !isStudentRead;\n\n    default:\n      return false;\n  }\n}\n\nexport default Message;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/Message/index.ts",
    "content": "export { default as Message } from './Message';\nexport { type MessageProps } from './Message';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/MessageSendingPanel/MessageSendingPanel.tsx",
    "content": "import { useState, useRef, useEffect, KeyboardEvent } from 'react';\nimport { MessageFilled, SendOutlined } from '@ant-design/icons';\nimport { Button, Col, Form, Input, InputRef, Row, Typography } from 'antd';\nimport { Comment } from '@client/components/Comment';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport { CrossCheckMessageAuthor } from '@client/services/course';\nimport { UserAvatar } from '../UserAvatar';\nimport { CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nconst { Text } = Typography;\n\nexport type MessageSendingPanelProps = {\n  sessionId: number;\n  sessionGithubId: string;\n  author: CrossCheckMessageAuthor | null;\n  currentRole: CrossCheckMessageDtoRoleEnum;\n  areContactsVisible: boolean;\n};\n\nfunction MessageSendingPanel(props: MessageSendingPanelProps) {\n  const { sessionId, sessionGithubId, author, currentRole, areContactsVisible } = props;\n\n  const form = Form.useFormInstance();\n  const inputValue = Form.useWatch('content', form);\n  const inputRef = useRef<InputRef>(null);\n  const [isPanelOpen, setIsPanelOpen] = useState<boolean>(false);\n  const [isPreviewVisible, setIsPreviewVisible] = useState<boolean>(false);\n\n  useEffect(() => {\n    if (isPanelOpen) {\n      inputRef.current?.focus({\n        cursor: 'end',\n      });\n    }\n  }, [isPanelOpen]);\n\n  const handleEnterButton = (event: KeyboardEvent<HTMLInputElement>) => {\n    event.preventDefault();\n    setIsPanelOpen(true);\n  };\n\n  const changePanelOpenness = () => {\n    setIsPanelOpen(previous => !previous);\n  };\n\n  const changePreviewVisibility = () => {\n    setIsPreviewVisible(previous => !previous);\n  };\n\n  return (\n    <Comment\n      avatar={\n        <UserAvatar\n          author={\n            currentRole === CrossCheckMessageDtoRoleEnum.Reviewer\n              ? author && { id: sessionId, githubId: sessionGithubId }\n              : { id: sessionId, githubId: sessionGithubId }\n          }\n          role={currentRole}\n          areContactsVisible={areContactsVisible}\n          size={24}\n        />\n      }\n      content={\n        <>\n          {!isPanelOpen && (\n            <Row>\n              <Col span={24}>\n                <Input\n                  type=\"text\"\n                  maxLength={0}\n                  placeholder=\"Leave a message\"\n                  suffix={<SendOutlined />}\n                  onClick={changePanelOpenness}\n                  onPressEnter={handleEnterButton}\n                  style={{ maxWidth: 512 }}\n                />\n              </Col>\n            </Row>\n          )}\n\n          {isPanelOpen && (\n            <>\n              <Row>\n                {isPreviewVisible && (\n                  <Col span={24}>\n                    <Text>\n                      <ReactMarkdown rehypePlugins={[remarkGfm]}>\n                        {inputValue === '' ? 'Nothing to preview' : inputValue}\n                      </ReactMarkdown>\n                    </Text>\n                  </Col>\n                )}\n\n                <Col span={24} hidden={isPreviewVisible}>\n                  <Form.Item name=\"content\" rules={[{ required: true, message: 'Please enter message' }]}>\n                    <Input.TextArea\n                      ref={inputRef}\n                      rows={3}\n                      showCount\n                      maxLength={1024}\n                      placeholder=\"Leave a message\"\n                      style={{ maxWidth: 512 }}\n                    />\n                  </Form.Item>\n                </Col>\n              </Row>\n\n              <Row style={{ marginBottom: 4 }}>\n                <Col>\n                  <Text type=\"secondary\">notification of messages: off</Text>\n                </Col>\n              </Row>\n\n              <Row gutter={[8, 8]}>\n                <Col>\n                  <Button type=\"primary\" htmlType=\"submit\" icon={<MessageFilled />}>\n                    Send message\n                  </Button>\n                </Col>\n\n                <Col>\n                  <Button type=\"default\" danger onClick={changePanelOpenness}>\n                    Cancel\n                  </Button>\n                </Col>\n\n                <Col>\n                  <Button type=\"default\" onClick={changePreviewVisibility}>\n                    {isPreviewVisible ? 'Write' : 'Preview'}\n                  </Button>\n                </Col>\n              </Row>\n            </>\n          )}\n        </>\n      }\n    />\n  );\n}\n\nexport default MessageSendingPanel;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/MessageSendingPanel/index.ts",
    "content": "export { default as MessageSendingPanel } from './MessageSendingPanel';\nexport { type MessageSendingPanelProps } from './MessageSendingPanel';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/SolutionReview.module.css",
    "content": ".container :global(.ant-comment-inner) {\n  padding: 0 !important;\n}\n\n.container :global(.ant-comment-avatar) {\n  position: sticky !important;\n  top: 16px;\n  align-self: start;\n}\n\n.container :global(.ant-comment-avatar img) {\n  width: 100% !important;\n  height: 100% !important;\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/SolutionReview.tsx",
    "content": "import { Alert, Button, Col, Divider, Form, notification, Row, Spin, Typography } from 'antd';\nimport { Comment } from '@client/components/Comment';\nimport PreparedComment, { markdownLabel } from '@client/shared/components/Forms/PreparedComment';\nimport { ScoreIcon } from '@client/shared/components/Icons/ScoreIcon';\nimport { SolutionReviewSettings } from '@client/modules/CrossCheck/constants';\nimport { useEffect, useMemo, useState } from 'react';\nimport { CourseService } from '@client/services/course';\nimport { formatDateTime } from '@client/services/formatter';\nimport { CrossCheckCriteriaModal } from '../criteria/CrossCheckCriteriaModal';\nimport { StudentDiscord } from '@client/components/StudentDiscord';\nimport { getAmountUnreadMessages, getHowManyUnreadMessagesText } from './helpers';\nimport { Message } from './Message';\nimport { MessageSendingPanel } from './MessageSendingPanel';\nimport { UserAvatar } from './UserAvatar';\nimport { Username } from './Username';\nimport { CrossCheckCriteriaDataDto, CrossCheckMessageDtoRoleEnum, CrossCheckSolutionReviewDto } from '@client/api';\nimport { useMessage } from '@client/hooks';\nimport styles from './SolutionReview.module.css';\n\nconst { Text } = Typography;\n\nexport type SolutionReviewProps = {\n  children?: JSX.Element;\n  sessionId: number;\n  sessionGithubId: string;\n  courseId: number;\n  reviewNumber: number;\n  settings: SolutionReviewSettings;\n  courseTaskId: number | null;\n  review: CrossCheckSolutionReviewDto;\n  isActiveReview: boolean;\n  isMessageSendingPanelVisible?: boolean;\n  currentRole: CrossCheckMessageDtoRoleEnum;\n  maxScore?: number;\n};\n\nfunction SolutionReview(props: SolutionReviewProps) {\n  const {\n    children,\n    sessionId,\n    sessionGithubId,\n    courseId,\n    reviewNumber,\n    settings,\n    courseTaskId,\n    review,\n    isActiveReview,\n    isMessageSendingPanelVisible = true,\n    currentRole,\n    maxScore,\n  } = props;\n  const { id, dateTime, author, comment, score, messages, criteria } = review;\n\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const [modalData, setModaldata] = useState<CrossCheckCriteriaDataDto[]>([]);\n\n  const showModal = (modalData: CrossCheckCriteriaDataDto[]) => {\n    setIsModalVisible(true);\n    setModaldata(modalData);\n  };\n\n  const [loading, setLoading] = useState<boolean>(false);\n  const [form] = Form.useForm();\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const amountUnreadMessages = getAmountUnreadMessages(currentRole, messages);\n  const howManyUnreadMessagesText = getHowManyUnreadMessagesText(amountUnreadMessages);\n  const { message } = useMessage();\n\n  useEffect(() => {\n    if (!courseTaskId || !amountUnreadMessages) return;\n\n    notification.info({\n      message: howManyUnreadMessagesText,\n    });\n\n    (async () => {\n      try {\n        await courseService.updateTaskSolutionResultMessages(id, courseTaskId, {\n          role: currentRole,\n        });\n      } catch {\n        message.error('An error occurred. Please try later.');\n      }\n    })();\n  }, [courseTaskId, amountUnreadMessages]);\n\n  const handleSubmit = async (values: { content: string }) => {\n    setLoading(true);\n\n    const { content } = values;\n\n    if (courseTaskId) {\n      try {\n        await courseService.postTaskSolutionResultMessages(id, courseTaskId, {\n          content: `${markdownLabel}${content}`,\n          role: currentRole,\n        });\n\n        message.success('The message has been sent.');\n        form.resetFields(['content']);\n      } catch {\n        message.error('An error occurred. Please try later.');\n      } finally {\n        setLoading(false);\n      }\n    }\n  };\n\n  return (\n    <Spin spinning={loading} className={styles.container}>\n      <CrossCheckCriteriaModal modalInfo={modalData} isModalVisible={isModalVisible} showModal={setIsModalVisible} />\n      <Row style={{ margin: '8px 0' }}>\n        <Col span={24}>\n          <Divider style={{ margin: 0 }} />\n        </Col>\n      </Row>\n\n      {amountUnreadMessages > 0 && (\n        <Row>\n          <Col>\n            <Alert message={howManyUnreadMessagesText} type=\"info\" showIcon />\n          </Col>\n        </Row>\n      )}\n\n      <Row style={{ margin: '16px 0' }}>\n        <Col span={24}>\n          <Comment\n            avatar={\n              <UserAvatar\n                author={author}\n                role={CrossCheckMessageDtoRoleEnum.Reviewer}\n                areContactsVisible={settings.areContactsVisible}\n                size={32}\n              />\n            }\n            content={\n              <>\n                <Row>\n                  <Col>\n                    <Username\n                      reviewNumber={reviewNumber}\n                      author={author}\n                      role={CrossCheckMessageDtoRoleEnum.Reviewer}\n                      areContactsVisible={settings.areContactsVisible}\n                    />\n                  </Col>\n                </Row>\n\n                <Row>\n                  <Text type=\"secondary\" style={{ fontSize: 12 }}>\n                    {formatDateTime(dateTime)}\n                  </Text>\n                </Row>\n\n                <Row gutter={4} align=\"middle\" style={{ marginTop: 8 }}>\n                  <Col>\n                    <ScoreIcon maxScore={maxScore} score={score} isOutdatedScore={!isActiveReview} />\n                  </Col>\n                  <Col>\n                    <Text>{score}</Text>\n                  </Col>\n                </Row>\n\n                <Row style={{ marginBottom: 10 }}>\n                  <Text style={{ fontSize: 12, lineHeight: '12px' }} type=\"secondary\">\n                    maximum score: {maxScore ?? 'unknown'}\n                  </Text>\n                </Row>\n\n                <Row style={{ marginBottom: 10 }}>\n                  {!!criteria?.length && <Button onClick={() => showModal(criteria)}>Show detailed feedback</Button>}\n                </Row>\n\n                <Row>\n                  <Col>\n                    <PreparedComment text={comment} />\n                  </Col>\n                </Row>\n\n                <Row>\n                  {settings.areContactsVisible && author && (\n                    <StudentDiscord discord={author.discord} textPrefix=\"Student Discord:\" />\n                  )}\n                </Row>\n              </>\n            }\n          >\n            {children}\n\n            {messages.map((message, index) => (\n              <Row key={index} style={{ margin: '16px 0' }}>\n                <Col>\n                  <Message\n                    reviewNumber={reviewNumber}\n                    message={message}\n                    currentRole={currentRole}\n                    settings={settings}\n                  />\n                </Col>\n              </Row>\n            ))}\n\n            {isMessageSendingPanelVisible && (\n              <Row style={{ marginTop: 16 }}>\n                <Col span={24}>\n                  <Form form={form} onFinish={handleSubmit} initialValues={{ content: '' }}>\n                    <MessageSendingPanel\n                      sessionId={sessionId}\n                      sessionGithubId={sessionGithubId}\n                      author={author}\n                      currentRole={currentRole}\n                      areContactsVisible={settings.areContactsVisible}\n                    />\n                  </Form>\n                </Col>\n              </Row>\n            )}\n          </Comment>\n        </Col>\n      </Row>\n    </Spin>\n  );\n}\n\nexport default SolutionReview;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/UserAvatar/UserAvatar.tsx",
    "content": "import { CDN_AVATARS_URL } from '@client/configs/cdn';\nimport { Avatar } from 'antd';\nimport { CrossCheckMessageAuthor } from '@client/services/course';\nimport { AVATAR_ICON_PATH } from '@client/modules/CrossCheck/constants';\nimport { CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nexport type UserAvatarProps = {\n  author: CrossCheckMessageAuthor | null;\n  role: CrossCheckMessageDtoRoleEnum;\n  areContactsVisible: boolean;\n  size: 24 | 32;\n};\n\nfunction UserAvatar(props: UserAvatarProps) {\n  const { size } = props;\n\n  return <Avatar size={size} src={createAvatarPath(props)} />;\n}\n\nfunction createAvatarPath(props: UserAvatarProps): string {\n  const { author, role, areContactsVisible, size } = props;\n\n  switch (role) {\n    case CrossCheckMessageDtoRoleEnum.Reviewer:\n      if (author && areContactsVisible) {\n        return `${CDN_AVATARS_URL}/${author.githubId}.png?size=${size * 2}`;\n      }\n      return AVATAR_ICON_PATH.expert;\n\n    case CrossCheckMessageDtoRoleEnum.Student:\n    default:\n      if (author && areContactsVisible) {\n        return `${CDN_AVATARS_URL}/${author.githubId}.png?size=${size * 2}`;\n      }\n      return AVATAR_ICON_PATH.thanks;\n  }\n}\n\nexport default UserAvatar;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/UserAvatar/index.ts",
    "content": "export { default as UserAvatar } from './UserAvatar';\nexport { type UserAvatarProps } from './UserAvatar';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/Username/Username.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { CrossCheckMessageAuthor } from '@client/services/course';\nimport { Username } from '.';\nimport { CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nconst mockAuthor: CrossCheckMessageAuthor = {\n  id: 2345,\n  githubId: 'test-github-1234',\n};\n\ndescribe('Username', () => {\n  test.each`\n    reviewNumber | author        | role                                     | areContactsVisible | expectedUsername\n    ${0}         | ${null}       | ${CrossCheckMessageDtoRoleEnum.Reviewer} | ${true}            | ${'Reviewer 1'}\n    ${1}         | ${null}       | ${CrossCheckMessageDtoRoleEnum.Reviewer} | ${false}           | ${'Reviewer 2'}\n    ${2}         | ${mockAuthor} | ${CrossCheckMessageDtoRoleEnum.Reviewer} | ${true}            | ${'test-github-1234'}\n    ${3}         | ${mockAuthor} | ${CrossCheckMessageDtoRoleEnum.Reviewer} | ${false}           | ${'Reviewer 4 (hidden)'}\n    ${4}         | ${null}       | ${CrossCheckMessageDtoRoleEnum.Student}  | ${true}            | ${'Student'}\n    ${5}         | ${null}       | ${CrossCheckMessageDtoRoleEnum.Student}  | ${false}           | ${'Student'}\n    ${6}         | ${mockAuthor} | ${CrossCheckMessageDtoRoleEnum.Student}  | ${true}            | ${'test-github-1234'}\n    ${7}         | ${mockAuthor} | ${CrossCheckMessageDtoRoleEnum.Student}  | ${false}           | ${'Student (hidden)'}\n  `(\n    `should display \"$expectedUsername\" if:\n    \"reviewNumber\" = \"$reviewNumber\", \"author\" = \"$author\", \"role\" = \"$role\", \"areContactsVisible\" = \"$areContactsVisible\"`,\n    ({ reviewNumber, author, role, areContactsVisible, expectedUsername }) => {\n      render(\n        <Username reviewNumber={reviewNumber} author={author} role={role} areContactsVisible={areContactsVisible} />,\n      );\n\n      const username = screen.getByText(expectedUsername);\n\n      expect(username).toBeInTheDocument();\n    },\n  );\n});\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/Username/Username.tsx",
    "content": "import { Typography } from 'antd';\nimport { CrossCheckMessageAuthor } from '@client/services/course';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nconst { Text } = Typography;\n\nexport type UsernameProps = {\n  reviewNumber: number;\n  author: CrossCheckMessageAuthor | null;\n  role: CrossCheckMessageDtoRoleEnum;\n  areContactsVisible: boolean;\n};\n\nfunction Username(props: UsernameProps) {\n  const { author, areContactsVisible } = props;\n\n  return author && areContactsVisible ? (\n    <GithubUserLink value={author.githubId} isUserIconHidden={true} />\n  ) : (\n    <Text>{createFakeUsername(props)}</Text>\n  );\n}\n\nfunction createFakeUsername(props: UsernameProps): string {\n  const { reviewNumber, author, role, areContactsVisible } = props;\n\n  switch (role) {\n    case CrossCheckMessageDtoRoleEnum.Reviewer:\n      return `Reviewer ${reviewNumber + 1}${author && !areContactsVisible ? ' (hidden)' : ''}`;\n\n    case CrossCheckMessageDtoRoleEnum.Student:\n    default:\n      return `Student${author && !areContactsVisible ? ' (hidden)' : ''}`;\n  }\n}\n\nexport default Username;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/Username/index.ts",
    "content": "export { default as Username } from './Username';\nexport { type UsernameProps } from './Username';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/helpers.ts",
    "content": "import { CrossCheckMessageDto, CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nexport const getAmountUnreadMessages = (\n  currentRole: CrossCheckMessageDtoRoleEnum,\n  messages: CrossCheckMessageDto[],\n): number => {\n  switch (currentRole) {\n    case CrossCheckMessageDtoRoleEnum.Reviewer:\n      return messages.filter(messages => !messages.isReviewerRead).length;\n\n    case CrossCheckMessageDtoRoleEnum.Student:\n      return messages.filter(messages => !messages.isStudentRead).length;\n\n    default:\n      return 0;\n  }\n};\n\nexport const getHowManyUnreadMessagesText = (amountUnreadMessages: number): string => {\n  return `You have ${amountUnreadMessages} unread ${amountUnreadMessages > 1 ? 'messages' : 'message'}`;\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReview/index.ts",
    "content": "export { default as SolutionReview } from './SolutionReview';\nexport { type SolutionReviewProps } from './SolutionReview';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReviewSettingsPanel/SolutionReviewSettingsPanel.tsx",
    "content": "import { Col, Row, Switch, Typography } from 'antd';\nimport { SolutionReviewSettings } from '@client/modules/CrossCheck/constants';\n\nconst { Text } = Typography;\n\nexport type SolutionReviewSettingsPanelProps = {\n  settings: SolutionReviewSettings;\n};\n\nfunction SolutionReviewSettingsPanel(props: SolutionReviewSettingsPanelProps) {\n  const { settings } = props;\n  const { areContactsVisible, setAreContactsVisible } = settings;\n\n  const handleContactsVisibilityChange = () => {\n    setAreContactsVisible?.(!areContactsVisible);\n  };\n\n  return (\n    <Row gutter={8}>\n      <Col>\n        <Text>Contacts</Text>\n      </Col>\n      <Col>\n        <Switch size={'small'} defaultChecked={areContactsVisible} onChange={handleContactsVisibilityChange} />\n      </Col>\n    </Row>\n  );\n}\n\nexport default SolutionReviewSettingsPanel;\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SolutionReviewSettingsPanel/index.ts",
    "content": "export { default as SolutionReviewSettingsPanel } from './SolutionReviewSettingsPanel';\nexport { type SolutionReviewSettingsPanelProps } from './SolutionReviewSettingsPanel';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/SubmittedStatus.tsx",
    "content": "import { Alert } from 'antd';\nimport { formatDate } from '@client/services/formatter';\nimport { TaskSolution } from '@client/services/course';\n\ntype Props = {\n  solution: TaskSolution | null;\n  deadlinePassed: boolean;\n  taskExists: boolean;\n};\n\nexport function SubmittedStatus(props: Props) {\n  const { taskExists, solution, deadlinePassed } = props;\n\n  if (!taskExists) return null;\n\n  if (!solution) {\n    const deadlinePassedMessage = 'Submission deadline has already passed';\n    const tipMessage = 'Try to submit your solution as soon as possible';\n    const message = `You haven't submitted solution. ${deadlinePassed ? deadlinePassedMessage : tipMessage}`;\n    return <Alert message={message} type=\"warning\" showIcon style={{ marginBottom: 8 }} />;\n  }\n\n  return (\n    <Alert\n      message={\n        <>\n          Submitted{' '}\n          <a className=\"crosscheck-submitted-link\" target=\"_blank\" href={solution.url}>\n            {solution.url}\n          </a>{' '}\n          on {formatDate(solution.updatedDate)}.\n        </>\n      }\n      type=\"success\"\n      showIcon\n      style={{ marginBottom: 8 }}\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/criteria/CrossCheckCriteria.tsx",
    "content": "import { theme, Typography } from 'antd';\nimport { CrossCheckCriteriaDataDto } from '@client/api';\nimport { getCriteriaStatusColor } from '@client/modules/CrossCheck';\nimport { TaskType } from '../../constants';\n\nconst { Text, Title } = Typography;\n\ntype Props = {\n  criteria: CrossCheckCriteriaDataDto[] | null;\n};\n\nexport function CrossCheckCriteria({ criteria }: Props) {\n  if (!criteria?.length) return null;\n  const penaltyData = criteria.filter(\n    criteriaItem => criteriaItem.type.toLocaleLowerCase() === TaskType.Penalty && criteriaItem.point,\n  );\n\n  const { token } = theme.useToken();\n\n  return (\n    <>\n      {criteria\n        .filter(criteriaItem => criteriaItem.type.toLocaleLowerCase() === TaskType.Subtask)\n        .map(criteriaItem => {\n          const colorToken = getCriteriaStatusColor(criteriaItem.point ?? 0, criteriaItem.max);\n\n          return (\n            <div\n              key={criteriaItem.key}\n              style={{\n                border: `1px solid ${token.colorBorder}`,\n                margin: '24px 0',\n                paddingBottom: '14px',\n                backgroundColor: token[colorToken],\n              }}\n            >\n              <div\n                style={{\n                  display: 'block',\n                  fontSize: '14px',\n                  background: token.colorBgLayout,\n                  borderBottom: `1px solid ${token.colorBorder}`,\n                  padding: '14px 12px',\n                  marginBottom: '14px',\n                }}\n              >\n                <Text>{criteriaItem.text}</Text>\n              </div>\n\n              {criteriaItem.textComment && (\n                <div style={{ padding: '0 12px', fontSize: '16px' }}>\n                  <Text strong={true}>Comment:</Text>\n                  {criteriaItem.textComment?.split('\\n').map((textLine, k) => (\n                    <p key={k} style={{ margin: '0px 0 5px 0' }}>\n                      {textLine}\n                    </p>\n                  ))}\n                </div>\n              )}\n              <div style={{ fontSize: '16px', padding: '0 12px' }}>\n                <Text strong={true}>Points for criteria: {`${criteriaItem.point ?? 0}/${criteriaItem.max}`}</Text>\n              </div>\n            </div>\n          );\n        })}\n      {penaltyData?.length ? (\n        <div style={{ marginTop: '20px' }}>\n          <Title level={4}>Penalty</Title>\n          {penaltyData?.map(criteriaItem => (\n            <div\n              key={criteriaItem.key}\n              style={{\n                display: 'inline-block',\n                width: '100%',\n                backgroundColor: token.red3,\n                border: `1px solid ${token.colorBorder}`,\n                padding: '14px 12px',\n              }}\n            >\n              <Text>\n                {criteriaItem.text} {criteriaItem.point ?? 0}\n              </Text>\n            </div>\n          ))}\n        </div>\n      ) : null}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/criteria/CrossCheckCriteriaModal.tsx",
    "content": "import { Modal } from 'antd';\nimport { CrossCheckCriteria } from './CrossCheckCriteria';\nimport { CrossCheckCriteriaDataDto } from '@client/api';\n\ntype Props = {\n  modalInfo: CrossCheckCriteriaDataDto[] | null;\n  isModalVisible: boolean;\n  showModal: (isModalVisible: boolean) => void;\n};\n\nexport function CrossCheckCriteriaModal({ modalInfo, isModalVisible, showModal }: Props) {\n  const handleOk = () => {\n    showModal(false);\n  };\n\n  const handleCancel = () => {\n    showModal(false);\n  };\n\n  return (\n    <Modal title=\"Feedback\" open={isModalVisible} onOk={handleOk} onCancel={handleCancel} width={1000}>\n      <CrossCheckCriteria criteria={modalInfo} />\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/criteria/PenaltyCriteria.tsx",
    "content": "import { Radio, RadioChangeEvent, theme, Typography } from 'antd';\nimport { CrossCheckCriteriaDataDto } from '@client/api';\n\nconst { Text } = Typography;\nconst { Group } = Radio;\n\nenum HasPenalty {\n  Yes = 'yes',\n  No = 'no',\n}\n\ninterface PenaltyCriteriaProps {\n  penaltyData: CrossCheckCriteriaDataDto;\n  updateCriteriaData: (updatedEntry: CrossCheckCriteriaDataDto) => void;\n}\n\nexport function PenaltyCriteria({ penaltyData, updateCriteriaData }: PenaltyCriteriaProps) {\n  const hasPenalty = Boolean(penaltyData.point && penaltyData.point !== 0);\n  const penaltyScore = -Math.abs(penaltyData.max!);\n\n  const updatePenalty = (event: RadioChangeEvent) => {\n    const pointsForPenalty = event.target.value === HasPenalty.Yes ? penaltyScore : 0;\n    const updatedEntry = { ...penaltyData, point: pointsForPenalty };\n    updateCriteriaData(updatedEntry);\n  };\n\n  const { token } = theme.useToken();\n\n  return (\n    <div\n      style={{\n        display: 'flex',\n        justifyContent: 'space-between',\n        alignItems: 'center',\n        gap: '10px',\n        background: token.colorBgLayout,\n        borderBottom: `1px solid ${token.colorBorder}`,\n        margin: '24px 0',\n        padding: '14px 12px',\n      }}\n    >\n      <Text>\n        {penaltyData.text} ({penaltyScore} points)\n      </Text>\n\n      <Group style={{ minWidth: '120px' }} value={hasPenalty ? 'yes' : 'no'} onChange={updatePenalty}>\n        <Radio value=\"yes\">Yes</Radio>\n        <Radio value=\"no\">No</Radio>\n      </Group>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/criteria/SubtaskCriteria.tsx",
    "content": "import { Input, Typography, InputNumber, Slider, theme } from 'antd';\nimport { useMemo } from 'react';\nimport isUndefined from 'lodash/isUndefined';\nimport isNil from 'lodash/isNil';\nimport { CrossCheckCriteriaDataDto } from '@client/api';\nimport { getCriteriaStatusColor } from '@client/modules/CrossCheck';\n\nconst { TextArea } = Input;\nconst { Text } = Typography;\n\ninterface SubtaskCriteriaProps {\n  subtaskData: CrossCheckCriteriaDataDto;\n  updateCriteriaData: (updatedEntry: CrossCheckCriteriaDataDto) => void;\n}\n\nexport function SubtaskCriteria({ subtaskData, updateCriteriaData }: SubtaskCriteriaProps) {\n  const maxScore = subtaskData.max;\n  const comment = subtaskData.textComment;\n  const criteriaScore = subtaskData.point;\n  const colorToken = getCriteriaStatusColor(criteriaScore ?? 0, maxScore);\n\n  const updateSubtaskData = ({ textComment, point }: { textComment?: string; point?: number | null }) => {\n    const updatedEntry = {\n      ...subtaskData,\n      ...(isUndefined(textComment) ? undefined : { textComment }),\n      ...(isNil(point) ? undefined : { point }),\n    };\n    updateCriteriaData(updatedEntry);\n  };\n\n  const statusCommentRequired = useMemo(() => {\n    if (criteriaScore !== undefined && maxScore) {\n      const commentNotMatchRules = comment ? comment.length < 10 : true;\n      return criteriaScore < maxScore && commentNotMatchRules;\n    }\n    return false;\n  }, [criteriaScore, comment, maxScore]);\n\n  const { token } = theme.useToken();\n\n  return (\n    <div style={{ border: `1px solid ${token.colorBorder}`, margin: '24px 0', background: token[colorToken] }}>\n      <div\n        style={{\n          borderBottom: `1px solid ${token.colorBorder}`,\n          padding: '14px 12px',\n        }}\n      >\n        <Text>{subtaskData.text}</Text>\n      </div>\n\n      <div\n        style={{\n          display: 'flex',\n          padding: '13px 12px',\n          justifyContent: 'space-between',\n          alignItems: 'center',\n        }}\n      >\n        <Text>\n          Quality of execution:\n          <br />\n          (Max {maxScore} points for criteria)\n        </Text>\n        <div style={{ width: '60%', display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>\n          <Slider\n            style={{ width: '70%' }}\n            min={0}\n            max={maxScore}\n            onChange={num => updateSubtaskData({ point: num })}\n            value={criteriaScore ?? 0}\n          />\n          <InputNumber\n            min={0}\n            max={maxScore}\n            value={criteriaScore ?? 0}\n            onChange={num => updateSubtaskData({ point: num })}\n          />\n        </div>\n      </div>\n      <div style={{ padding: '0 12px' }}>\n        <TextArea\n          style={{ border: statusCommentRequired ? '1px red solid' : '' }}\n          value={subtaskData.textComment}\n          rows={2}\n          onInput={event => updateSubtaskData({ textComment: (event.target as HTMLInputElement).value })}\n        />\n        <div style={{ height: '20px' }}>\n          {statusCommentRequired && <Text type=\"danger\">Please leave a detailed comment</Text>}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/components/criteria/TitleCriteria.tsx",
    "content": "import { theme, Typography } from 'antd';\nimport { CrossCheckCriteriaDataDto } from '@client/api';\n\ninterface TitleCriteriaProps {\n  titleData: CrossCheckCriteriaDataDto;\n}\nconst { Text } = Typography;\n\nexport function TitleCriteria({ titleData }: TitleCriteriaProps) {\n  const { token } = theme.useToken();\n  return (\n    <div\n      style={{\n        display: 'block',\n        fontSize: '14px',\n        marginTop: '10px',\n        background: token.blue2,\n        border: '1px solid #91D5FF',\n        borderRadius: '2px',\n        padding: '9px',\n      }}\n    >\n      <Text key={titleData.key}>{titleData.text}</Text>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/constants.ts",
    "content": "import { CrossCheckMessageDtoRoleEnum } from '@client/api';\n\nexport enum LocalStorageKey {\n  AreContactsVisible = 'crossCheckAreContactsVisible',\n}\n\nexport interface SolutionReviewSettings {\n  areContactsVisible: boolean;\n  setAreContactsVisible?: (value: boolean) => void;\n}\n\nexport const AVATAR_ICON_PATH = {\n  expert: '/static/svg/sloths/Expert.svg',\n  thanks: '/static/svg/sloths/Thanks.svg',\n};\n\nexport const ROLE_TAG_COLOR = {\n  [CrossCheckMessageDtoRoleEnum.Reviewer]: 'processing',\n  [CrossCheckMessageDtoRoleEnum.Student]: 'success',\n};\n\nexport enum EditableTableColumnsDataIndex {\n  Type = 'type',\n  Max = 'max',\n  Text = 'text',\n  Actions = 'actions',\n}\n\nexport enum TaskType {\n  Title = 'title',\n  Subtask = 'subtask',\n  Penalty = 'penalty',\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/hooks/index.ts",
    "content": "export { useSolutionReviewSettings } from './useSolutionReviewSettings';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/hooks/useSolutionReviewSettings.ts",
    "content": "import { useLocalStorage } from 'react-use';\nimport { LocalStorageKey, SolutionReviewSettings } from '../constants';\n\nexport function useSolutionReviewSettings(): SolutionReviewSettings {\n  const [areContactsVisible = true, setAreContactsVisible] = useLocalStorage<boolean>(\n    LocalStorageKey.AreContactsVisible,\n  );\n\n  return { areContactsVisible, setAreContactsVisible };\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/index.tsx",
    "content": "export { addKeyAndIndex } from '@client/modules/CrossCheck/utils/addKeyAndIndex';\nexport { EditableTable } from '@client/modules/CrossCheck/EditableTableForCrossCheck';\nexport { UploadCriteriaJSON } from '@client/modules/CrossCheck/UploadCriteriaJSON';\nexport { AddCriteriaForCrossCheck } from '@client/modules/CrossCheck/AddCriteriaForCrossCheck';\nexport { ExportJSONButton } from '@client/modules/CrossCheck/ExportJSONButton';\nexport { getCriteriaStatusColor } from '@client/modules/CrossCheck/utils/getCriteriaStatusColor';\n"
  },
  {
    "path": "client/src/modules/CrossCheck/utils/addKeyAndIndex.tsx",
    "content": "import { CriteriaDto } from '@client/api';\n\nexport const addKeyAndIndex = (array: CriteriaDto[]): CriteriaDto[] => {\n  return array.map((item, index) => ({\n    ...item,\n    key: index.toString(),\n    index,\n  }));\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheck/utils/arrayMoveImmutable.tsx",
    "content": "import { CriteriaDto } from '@client/api';\n\nexport function arrayMoveMutable(array: CriteriaDto[], fromIndex: number, toIndex: number) {\n  const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex;\n\n  if (startIndex >= 0 && startIndex < array.length) {\n    const endIndex = toIndex < 0 ? array.length + toIndex : toIndex;\n\n    const [item] = array.splice(fromIndex, 1);\n    if (item) {\n      array.splice(endIndex, 0, item);\n    }\n  }\n}\n\nexport function arrayMoveImmutable(array: CriteriaDto[], fromIndex: number, toIndex: number) {\n  const arrayCopy = [...array];\n  arrayMoveMutable(arrayCopy, fromIndex, toIndex);\n  return arrayCopy;\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheck/utils/getCriteriaStatusColor.ts",
    "content": "const colors = ['colorBgContainer', 'red1', 'yellow1', 'green1'] as const;\n\nexport function getCriteriaStatusColor(score: number, maxScore?: number) {\n  const [transparent, red, yellow, green] = colors;\n\n  if (!maxScore) {\n    return transparent;\n  }\n\n  if (score === 0) {\n    return red;\n  }\n\n  if (score < maxScore) {\n    return yellow;\n  }\n\n  if (score === maxScore) {\n    return green;\n  }\n\n  return transparent;\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheckPairs/components/BadReview/BadReviewControllers.tsx",
    "content": "import { Button, Col, Modal, Row, Select, Spin } from 'antd';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport { CheckService } from '@client/services/check';\nimport { CourseTaskDetails } from '@client/services/course';\nimport { BadReviewTable } from './BadReviewTable';\n\ninterface IBadReviewControllersProps {\n  courseTasks: CourseTaskDetails[];\n  courseId: number;\n}\n\nexport interface IBadReview {\n  checkerScore: number;\n  comment?: string;\n  taskName: string;\n  checkerGithubId: string;\n  studentGithubId: string;\n  studentAvgScore?: number;\n}\n\nexport type checkType = 'Bad comment' | 'Did not check' | 'No type';\n\nexport function BadReviewControllers({ courseTasks, courseId }: IBadReviewControllersProps) {\n  const { Option } = Select;\n  const [taskId, setTaskId] = useState<number>();\n  const [data, setData] = useState<IBadReview[]>();\n  const [isLoading, setIsLoading] = useState<boolean>(true);\n  const [checkType, setCheckType] = useState<checkType>();\n  const [isModalVisible, setIsModalVisible] = useState<boolean>(false);\n  const checkService = useMemo(() => new CheckService(), []);\n\n  const getData = useCallback(async (): Promise<void> => {\n    if (taskId && checkType) {\n      setIsLoading(true);\n      const dataFromService = await checkService.getData(taskId, checkType, courseId);\n      setData(dataFromService);\n      setIsLoading(false);\n    }\n  }, [taskId, checkType, checkService, courseId]);\n\n  const showModal = () => {\n    setIsModalVisible(true);\n  };\n\n  const handleCancel = () => {\n    setIsModalVisible(false);\n  };\n\n  const buttonHandler = (type: checkType) => {\n    setCheckType(type);\n    showModal();\n  };\n\n  useEffect(() => setCheckType('No type'), [taskId]);\n\n  useEffect(() => {\n    getData();\n  }, [checkType]);\n\n  return (\n    <>\n      <Row gutter={16} justify=\"start\" style={{ marginBottom: '10px' }}>\n        <Col>\n          <Select\n            placeholder=\"Select task\"\n            style={{ width: 200 }}\n            onChange={(value: number) => setTaskId(value)}\n            showSearch\n            optionFilterProp=\"label\"\n          >\n            {courseTasks.map(task => (\n              <Option key={task.id} value={task.id} label={task.name}>\n                {task.name}\n              </Option>\n            ))}\n          </Select>\n        </Col>\n        <Col>\n          <Button type=\"primary\" href={`/api/v2/courses/${courseId}/cross-checks/${taskId}/csv`} disabled={!taskId}>\n            Download solutions urls\n          </Button>\n        </Col>\n        <Col>\n          <Button type=\"primary\" danger onClick={() => buttonHandler('Bad comment')} disabled={!taskId}>\n            Bad comment\n          </Button>\n        </Col>\n        <Col>\n          <Button type=\"primary\" danger onClick={() => buttonHandler('Did not check')} disabled={!taskId}>\n            Didn't check\n          </Button>\n        </Col>\n      </Row>\n      <Modal\n        title={`Bad checkers in ${checkType}`}\n        open={isModalVisible}\n        width={1250}\n        style={{ top: 20 }}\n        onCancel={handleCancel}\n        footer={[\n          <Button key=\"cancel\" type=\"primary\" onClick={handleCancel}>\n            Cancel\n          </Button>,\n        ]}\n      >\n        {isLoading || !data ? <Spin /> : <BadReviewTable data={data} type={checkType!} />}\n      </Modal>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheckPairs/components/BadReview/BadReviewTable.tsx",
    "content": "import { message, Table, Typography } from 'antd';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { checkType, IBadReview } from './BadReviewControllers';\n\ninterface IBadReviewTableProps {\n  data: IBadReview[];\n  type: checkType;\n}\n\nexport const BadReviewTable = ({ data, type }: IBadReviewTableProps) => {\n  const { Text } = Typography;\n\n  const columns = [\n    {\n      title: 'Checker',\n      dataIndex: 'checkerGithubId',\n      key: 'checkerGithubId',\n      render: (id: string) => <GithubUserLink value={id} />,\n    },\n    {\n      title: 'Task',\n      dataIndex: 'taskName',\n      key: 'taskName',\n    },\n    {\n      title: 'Student',\n      dataIndex: 'studentGithubId',\n      key: 'studentGithubId',\n      render: (id: string) => <GithubUserLink value={id} />,\n    },\n    {\n      title: \"Checker's score\",\n      dataIndex: 'checkerScore',\n      key: 'checkerScore',\n    },\n    {\n      title: 'Average student score',\n      dataIndex: 'studentAvgScore',\n      key: 'studentAvgScore',\n    },\n    {\n      title: \"Checker's comment\",\n      dataIndex: 'comment',\n      key: 'comment',\n    },\n  ];\n\n  let columnsType;\n\n  switch (type) {\n    case 'Bad comment':\n      columnsType = columns.filter(c => c.dataIndex !== 'studentAvgScore');\n      break;\n    case 'Did not check':\n      columnsType = columns.filter(c => c.dataIndex !== 'comment');\n      break;\n    default:\n      message.error('Something went wrong');\n  }\n\n  return (\n    <>{data.length ? <Table columns={columnsType} dataSource={data} scroll={{ x: true }} /> : <Text>No data</Text>}</>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheckPairs/components/CrossCheckPairsTable/CrossCheckPairsTable.module.css",
    "content": ".tableScore :global(td),\n.tableScore :global(th),\n.tableRow :global(td) {\n  padding: 0 5px !important;\n  font-size: 11px;\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheckPairs/components/CrossCheckPairsTable/CrossCheckPairsTable.tsx",
    "content": "import { Table, TablePaginationConfig } from 'antd';\nimport { CrossCheckPairDto } from '@client/api';\nimport { FilterValue, SorterResult } from 'antd/lib/table/interface';\nimport {\n  CustomColumnType,\n  fields,\n  getCrossCheckPairsColumns,\n} from '@client/modules/CrossCheckPairs/data/getCrossCheckPairsColumns';\n\nimport styles from './CrossCheckPairsTable.module.css';\n\nexport type Filters = Omit<typeof fields, 'score' | 'submittedDate' | 'reviewedDate'>;\n\ninterface CustomSorterResult<RecordType> extends SorterResult<RecordType> {\n  column?: CustomColumnType<RecordType>;\n}\n\nexport type Sorter<RecordType> = CustomSorterResult<RecordType> | CustomSorterResult<RecordType>[];\n\ntype CrossCheckTableProps = {\n  loaded: boolean;\n  crossCheckPairs: CrossCheckPairDto[];\n  pagination: TablePaginationConfig;\n  onChange: (\n    pagination: TablePaginationConfig,\n    filters: Record<keyof Filters, FilterValue | null>,\n    sorter: Sorter<CrossCheckPairDto>,\n  ) => void;\n  viewComment: (value: CrossCheckPairDto) => void;\n};\n\nexport const CrossCheckPairsTable = ({\n  loaded,\n  crossCheckPairs,\n  pagination,\n  onChange,\n  viewComment,\n}: CrossCheckTableProps) => {\n  if (!loaded) return null;\n\n  // where 800 is approximate sum of basic columns (GitHub, Name, etc.)\n  const tableWidth = 800;\n  return (\n    <>\n      <Table<CrossCheckPairDto>\n        className={styles.tableScore}\n        showHeader\n        scroll={{ x: tableWidth, y: 'calc(100vh - 250px)' }}\n        pagination={pagination}\n        dataSource={crossCheckPairs}\n        size=\"small\"\n        rowClassName={styles.tableRow}\n        onChange={onChange}\n        key=\"id\"\n        columns={getCrossCheckPairsColumns(viewComment)}\n      />\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/CrossCheckPairs/data/getCrossCheckPairsColumns.tsx",
    "content": "import { CrossCheckPairDto } from '@client/api';\nimport { GithubFilled } from '@ant-design/icons';\nimport { ColumnType } from 'antd/lib/table/interface';\nimport { omit } from 'lodash';\nimport { dateTimeRenderer, getColumnSearchProps } from '@client/shared/components/Table';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { Button, Flex } from 'antd';\n\nexport const fields = {\n  task: 'task',\n  checker: 'checker',\n  student: 'student',\n  url: 'url',\n  score: 'score',\n  submittedDate: 'submittedDate',\n  reviewedDate: 'reviewedDate',\n};\n\nexport interface CustomColumnType<RecordType> extends ColumnType<RecordType> {\n  sorterField?: string;\n}\n\nconst renderGithubLink = (value: string) =>\n  value ? (\n    <div>\n      <GithubAvatar githubId={value} size={24} />\n      &nbsp;\n      <a target=\"_blank\" rel=\"noopener noreferrer\" href={`https://github.com/${value}`}>\n        {value}\n      </a>\n    </div>\n  ) : null;\n\nconst renderPrivateRepositoryLink = (value?: string) =>\n  value ? (\n    <Flex>\n      <a target=\"_blank\" href={value}>\n        <GithubFilled /> Private Repository\n      </a>\n    </Flex>\n  ) : null;\n\nexport const getCrossCheckPairsColumns = (\n  viewComment: (value: CrossCheckPairDto) => void,\n): CustomColumnType<CrossCheckPairDto>[] => [\n  {\n    title: 'Task',\n    fixed: 'left',\n    dataIndex: ['task', 'name'],\n    key: fields.task,\n    width: 100,\n    sorter: true,\n    sorterField: 'task',\n    ...omit(getColumnSearchProps(['task', 'name']), 'onFilter'),\n  },\n  {\n    title: 'Checker',\n    fixed: 'left',\n    key: fields.checker,\n    dataIndex: ['checker', 'githubId'],\n    sorter: true,\n    sorterField: 'checker',\n    width: 150,\n    render: renderGithubLink,\n    ...omit(getColumnSearchProps(['checkerStudent', 'githubId']), 'onFilter'),\n  },\n  {\n    title: 'Student',\n    key: fields.student,\n    dataIndex: ['student', 'githubId'],\n    sorter: true,\n    sorterField: 'student',\n    width: 150,\n    render: renderGithubLink,\n    ...omit(getColumnSearchProps(['student', 'githubId']), 'onFilter'),\n  },\n  {\n    title: 'Url',\n    dataIndex: 'url',\n    key: fields.url,\n    width: 150,\n    sorter: true,\n    sorterField: 'url',\n    ...getColumnSearchProps('url'),\n  },\n  {\n    title: 'Private Repository',\n    dataIndex: 'privateRepository',\n    key: 'privateRepository',\n    width: 150,\n    render: value => renderPrivateRepositoryLink(value),\n  },\n  {\n    title: 'Score',\n    dataIndex: 'score',\n    key: fields.score,\n    width: 80,\n    sorter: true,\n    sorterField: 'score',\n    render: value => <>{value ?? '(Empty)'}</>,\n  },\n  {\n    title: 'Submitted Date',\n    dataIndex: 'submittedDate',\n    key: fields.submittedDate,\n    width: 80,\n    sorter: true,\n    sorterField: 'submittedDate',\n    render: dateTimeRenderer,\n  },\n  {\n    title: 'Reviewed Date',\n    dataIndex: 'reviewedDate',\n    key: fields.reviewedDate,\n    width: 80,\n    sorter: true,\n    sorterField: 'reviewedDate',\n    render: (_, record) => dateTimeRenderer(record.reviewedDate),\n  },\n  {\n    title: 'Comment',\n    dataIndex: 'comment',\n    key: 'comment',\n    width: 60,\n    render: (_, record) => (\n      <Button disabled={!record.historicalScores} onClick={() => viewComment(record)} type=\"link\" size=\"small\">\n        Show\n      </Button>\n    ),\n  },\n];\n"
  },
  {
    "path": "client/src/modules/CrossCheckPairs/pages/CrossCheckPairs/CrossCheckPairs.tsx",
    "content": "import { Collapse, Modal, Space, TablePaginationConfig } from 'antd';\nimport { Comment } from '@client/components/Comment';\nimport { FilterValue } from 'antd/lib/table/interface';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { dateTimeRenderer } from '@client/shared/components/Table';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport { CourseService, CourseTaskDetails } from '@client/services/course';\nimport { CoursesTasksApi, CrossCheckMessageDtoRoleEnum, CrossCheckPairDto } from '@client/api';\nimport PreparedComment from '@client/shared/components/Forms/PreparedComment';\nimport { Message } from '@client/modules/CrossCheck/components/SolutionReview/Message';\nimport { CrossCheckCriteria } from '@client/modules/CrossCheck/components/criteria/CrossCheckCriteria';\nimport { BadReviewControllers } from '@client/modules/CrossCheckPairs/components/BadReview/BadReviewControllers';\nimport {\n  Filters,\n  Sorter,\n  CrossCheckPairsTable,\n} from '@client/modules/CrossCheckPairs/components/CrossCheckPairsTable/CrossCheckPairsTable';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\nenum OrderDirection {\n  ASC = 'ASC',\n  DESC = 'DESC',\n}\n\nconst DEFAULT_ORDER_BY = 'task';\nconst DEFAULT_ORDER_DIRECTION = OrderDirection.ASC;\n\nconst api = new CoursesTasksApi();\n\nexport default function Page() {\n  const { course, courses } = useActiveCourseContext();\n  const [modal, contextHolder] = Modal.useModal();\n  const courseId = course.id;\n  const courseService = useMemo(() => new CourseService(courseId), [course]);\n\n  const [loading, setLoading] = useState(false);\n  const [courseTasks, setCourseTasks] = useState<CourseTaskDetails[]>([]);\n  const [crossCheckList, setCrossCheckList] = useState({\n    content: [] as CrossCheckPairDto[],\n    pagination: { current: 1, pageSize: 50 } as IPaginationInfo,\n    orderBy: { field: DEFAULT_ORDER_BY, order: DEFAULT_ORDER_DIRECTION },\n  });\n  const [loaded, setLoaded] = useState(false);\n\n  const loadInitialData = useCallback(async () => {\n    try {\n      setLoading(true);\n      const [{ data: crossCheckData }, tasksFromReq] = await Promise.all([\n        api.getCrossCheckPairs(\n          courseId,\n          crossCheckList.pagination.pageSize,\n          crossCheckList.pagination.current,\n          crossCheckList.orderBy.field,\n          crossCheckList.orderBy.order,\n        ),\n        courseService.getCourseTasksDetails(),\n      ]);\n      setCourseTasks(tasksFromReq.filter(task => task.pairsCount));\n      setCrossCheckList({\n        content: crossCheckData.items,\n        pagination: crossCheckData.pagination,\n        orderBy: crossCheckList.orderBy,\n      });\n      setLoaded(true);\n    } finally {\n      setLoading(false);\n    }\n  }, []);\n\n  const getCourseScore = useCallback(\n    async (\n      pagination: TablePaginationConfig,\n      filters: Record<keyof Filters, FilterValue | null>,\n      sorter: Sorter<CrossCheckPairDto>,\n    ) => {\n      if (Array.isArray(sorter)) {\n        return;\n      }\n\n      const orderBy = {\n        field: sorter.column?.sorterField ?? DEFAULT_ORDER_BY,\n        order: sorter.order === 'descend' ? OrderDirection.DESC : OrderDirection.ASC,\n      };\n\n      setLoading(true);\n      try {\n        const { data } = await api.getCrossCheckPairs(\n          courseId,\n          pagination.pageSize as IPaginationInfo['pageSize'],\n          pagination.current as IPaginationInfo['current'],\n          orderBy.field,\n          orderBy.order,\n          filters.checker?.toString(),\n          filters.student?.toString(),\n          filters.url?.toString(),\n          filters.task?.toString(),\n        );\n        setCrossCheckList({\n          content: data.items,\n          pagination: data.pagination,\n          orderBy: {\n            field: orderBy.field,\n            order: orderBy.order,\n          },\n        });\n      } finally {\n        setLoading(false);\n      }\n    },\n    [crossCheckList.content],\n  );\n\n  useEffect(() => {\n    loadInitialData();\n  }, []);\n\n  const handleViewComment = ({ historicalScores, checker, messages }: CrossCheckPairDto) => {\n    modal.info({\n      width: 1020,\n      maskClosable: true,\n      title: `Comment from ${checker.githubId}`,\n      content: historicalScores.map((historicalScore, index) => (\n        <Space direction=\"vertical\" key={historicalScore.dateTime} style={{ width: '100%' }}>\n          <Comment\n            content={\n              <>\n                {dateTimeRenderer(historicalScore.dateTime)}\n                <PreparedComment text={historicalScore.comment}></PreparedComment>\n                {index === 0 &&\n                  messages.length > 0 &&\n                  messages.map(message => (\n                    <Message\n                      key={message.timestamp}\n                      message={message}\n                      currentRole={CrossCheckMessageDtoRoleEnum.Student}\n                      settings={{ areContactsVisible: true }}\n                    />\n                  ))}\n              </>\n            }\n          />\n          {historicalScore.criteria?.length ? (\n            <Collapse>\n              <Collapse.Panel key={historicalScore.dateTime} header=\"Detailed feedback\">\n                <CrossCheckCriteria criteria={historicalScore.criteria} />\n              </Collapse.Panel>\n            </Collapse>\n          ) : null}\n        </Space>\n      )),\n    });\n  };\n\n  return (\n    <AdminPageLayout loading={loading} title=\"Cross-Check\" showCourseName courses={courses}>\n      {contextHolder}\n      <BadReviewControllers courseTasks={courseTasks} courseId={course?.id} />\n      <CrossCheckPairsTable\n        loaded={loaded}\n        crossCheckPairs={crossCheckList.content}\n        pagination={crossCheckList.pagination}\n        onChange={getCourseScore}\n        viewComment={handleViewComment}\n      />\n    </AdminPageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/CrossCheckPairs/pages/CrossCheckPairs/index.ts",
    "content": "export { default as CrossCheckPairs } from './CrossCheckPairs';\n"
  },
  {
    "path": "client/src/modules/Discipline/components/DisciplineModal.tsx",
    "content": "import { Form, Input, message, Modal } from 'antd';\nimport { useEffect } from 'react';\nimport { DisciplineDto, DisciplinesApi } from '@client/api';\n\ninterface IDisciplineModal {\n  isModalVisible: boolean;\n  onCancel: () => void;\n  loadDisciplines: () => Promise<void>;\n  discipline: DisciplineDto | null;\n}\nconst disciplineService = new DisciplinesApi();\n\nexport function DisciplineModal({ isModalVisible, onCancel, loadDisciplines, discipline }: IDisciplineModal) {\n  const [form] = Form.useForm();\n\n  useEffect(() => form.resetFields, [isModalVisible]);\n\n  const initialValues = {\n    name: discipline?.name,\n  };\n\n  const submitForm = async () => {\n    try {\n      const value = await form.validateFields();\n\n      if (discipline) {\n        await disciplineService.updateDiscipline(discipline.id, value);\n      } else {\n        await disciplineService.createDiscipline(value);\n      }\n\n      await loadDisciplines();\n      onCancel();\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    }\n  };\n\n  return (\n    <Modal\n      title={discipline ? 'Edit discipline' : 'Add discipline'}\n      open={isModalVisible}\n      onCancel={onCancel}\n      onOk={submitForm}\n    >\n      <Form layout=\"vertical\" form={form} initialValues={initialValues}>\n        <Form.Item key=\"name\" name=\"name\" label=\"Discipline\" rules={[{ required: true }]}>\n          <Input />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Discipline/components/DisciplineTable.tsx",
    "content": "import { Button, Modal, Space, Table } from 'antd';\nimport { DisciplineDto } from '@client/api';\nimport { DeleteOutlined, EditOutlined, ExclamationCircleOutlined } from '@ant-design/icons';\n\nconst { Column } = Table;\nconst { confirm } = Modal;\ninterface IDisciplineTable {\n  disciplines: DisciplineDto[];\n  handleUpdate: (record: DisciplineDto) => void;\n  handleDelete: (record: DisciplineDto) => Promise<void>;\n}\n\nexport const DisciplineTable = ({ disciplines, handleDelete, handleUpdate }: IDisciplineTable) => {\n  const deleteDisciplineHandler = (record: DisciplineDto) => {\n    confirm({\n      title: 'Do you want to delete this discipline?',\n      icon: <ExclamationCircleOutlined />,\n      content: 'Some descriptions',\n      async onOk() {\n        await handleDelete(record);\n      },\n    });\n  };\n\n  const updateDisciplineHandler = (record: DisciplineDto) => {\n    handleUpdate(record);\n  };\n\n  return (\n    <>\n      <Table dataSource={disciplines} rowKey={'name'}>\n        <Column title=\"Discipline\" dataIndex=\"name\" key=\"name\" />\n        <Column\n          title=\"Actions\"\n          key=\"action\"\n          width={100}\n          render={record => (\n            <Space size=\"middle\">\n              <Button key={'edit'} onClick={() => updateDisciplineHandler(record)} size=\"small\">\n                <EditOutlined size={8} />\n              </Button>\n              <Button key={'delete'} onClick={() => deleteDisciplineHandler(record)} size=\"small\" danger>\n                <DeleteOutlined size={8} />\n              </Button>\n            </Space>\n          )}\n        />\n      </Table>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Discipline/pages/DisciplinePage.tsx",
    "content": "import { DisciplineDto, DisciplinesApi } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { DisciplineModal } from '../components/DisciplineModal';\nimport { useCallback, useState } from 'react';\nimport { Button, Layout, message } from 'antd';\nimport { DisciplineTable } from '../components/DisciplineTable';\nimport { useAsync } from 'react-use';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\nconst disciplinesApi = new DisciplinesApi();\n\nexport const DisciplinePage = () => {\n  const { courses } = useActiveCourseContext();\n  const [disciplines, setDisciplines] = useState([] as DisciplineDto[]);\n  const [discipline, setDiscipline] = useState<DisciplineDto | null>(null);\n  const [loading, setLoading] = useState(false);\n  const [isModalVisible, setIsModalVisible] = useState(false);\n\n  const loadDisciplines = useCallback(async () => {\n    try {\n      setLoading(true);\n      const { data: disciplines } = await disciplinesApi.getDisciplines();\n      setDisciplines(disciplines);\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    } finally {\n      setLoading(false);\n    }\n  }, []);\n\n  const handleDelete = async (record: DisciplineDto) => {\n    await disciplinesApi.deleteDiscipline(record.id);\n    await loadDisciplines();\n  };\n\n  const handleModalCancel = () => {\n    setIsModalVisible(false);\n    setDiscipline(null);\n  };\n\n  const handleModalShow = () => {\n    setIsModalVisible(true);\n  };\n\n  const handleModalShowUpdate = (discipline: DisciplineDto) => {\n    setDiscipline(discipline);\n    setIsModalVisible(true);\n  };\n\n  useAsync(loadDisciplines, []);\n\n  return (\n    <AdminPageLayout title=\"Manage Disciplines\" loading={loading} courses={courses}>\n      <Layout.Content style={{ margin: 8 }}>\n        <Button type=\"primary\" onClick={handleModalShow} style={{ marginBottom: '25px' }}>\n          Add Disciplines\n        </Button>\n        <DisciplineTable\n          disciplines={disciplines || []}\n          handleUpdate={handleModalShowUpdate}\n          handleDelete={handleDelete}\n        />\n      </Layout.Content>\n      <DisciplineModal\n        isModalVisible={isModalVisible}\n        onCancel={handleModalCancel}\n        loadDisciplines={loadDisciplines}\n        discipline={discipline}\n      />\n    </AdminPageLayout>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/DiscordAdmin/components/DiscordServersModal.tsx",
    "content": "import { Col, Form, Input, Row } from 'antd';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { DiscordServerDto, UpdateDiscordServerDto } from '@client/api';\n\ntype Props = {\n  data: Partial<DiscordServerDto> | null;\n  title: string;\n  submit: (values: UpdateDiscordServerDto) => Promise<void>;\n  cancel: () => void;\n  getInitialValues: (data: Partial<DiscordServerDto>) => any;\n  loading: boolean;\n};\n\nexport function DiscordServersModal({ data, title, submit, cancel, getInitialValues, loading }: Props) {\n  return data ? (\n    <ModalForm\n      data={data}\n      title={title}\n      submit={submit}\n      cancel={cancel}\n      getInitialValues={getInitialValues}\n      loading={loading}\n    >\n      <Row gutter={24}>\n        <Col span={24}>\n          <Form.Item name=\"name\" label=\"Name\" rules={[{ required: true, message: 'Please enter server name' }]}>\n            <Input />\n          </Form.Item>\n        </Col>\n        <Col span={24}>\n          <Form.Item\n            name=\"gratitudeUrl\"\n            label=\"Gratitude URL\"\n            rules={[{ required: true, message: 'Please enter gratitude URL' }]}\n          >\n            <Input />\n          </Form.Item>\n        </Col>\n        <Col span={24}>\n          <Form.Item\n            name=\"mentorsChatUrl\"\n            label=\"Mentors chat URL\"\n            rules={[{ required: true, message: 'Please enter mentors chat URL' }]}\n          >\n            <Input />\n          </Form.Item>\n        </Col>\n      </Row>\n    </ModalForm>\n  ) : null;\n}\n"
  },
  {
    "path": "client/src/modules/DiscordAdmin/components/DiscordServersTable.tsx",
    "content": "import { Table, Typography } from 'antd';\nimport { stringSorter } from '@client/shared/components/Table';\nimport { DiscordServerDto } from '@client/api';\nimport { CustomPopconfirm } from '@client/components/common/CustomPopconfirm';\n\ntype Props = {\n  data: DiscordServerDto[];\n  onEdit: (record: DiscordServerDto) => void;\n  onDelete: (id: number) => void;\n};\n\nexport function DiscordServersTable({ data, onEdit, onDelete }: Props) {\n  return (\n    <Table\n      size=\"small\"\n      style={{ marginTop: 8 }}\n      dataSource={data}\n      pagination={{ pageSize: 100 }}\n      rowKey=\"id\"\n      columns={getColumns(onEdit, onDelete)}\n    />\n  );\n}\n\nfunction getColumns(handleEditItem: (record: DiscordServerDto) => void, handleDeleteItem: (id: number) => void) {\n  return [\n    {\n      title: 'Id',\n      dataIndex: 'id',\n    },\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      sorter: stringSorter<DiscordServerDto>('name'),\n    },\n    {\n      title: 'Gratitude URL',\n      dataIndex: 'gratitudeUrl',\n      sorter: stringSorter<DiscordServerDto>('gratitudeUrl'),\n    },\n    {\n      title: 'Mentors chat URL',\n      dataIndex: 'mentorsChatUrl',\n      sorter: stringSorter<DiscordServerDto>('mentorsChatUrl'),\n    },\n    {\n      title: 'Actions',\n      dataIndex: 'actions',\n      render: (_: any, record: DiscordServerDto) => (\n        <>\n          <span>\n            <Typography.Link onClick={() => handleEditItem(record)}>Edit</Typography.Link>{' '}\n          </span>\n          <span style={{ marginLeft: 8 }}>\n            <CustomPopconfirm\n              onConfirm={() => handleDeleteItem(record.id)}\n              title=\"Are you sure you want to delete this item?\"\n            >\n              <Typography.Link>Delete</Typography.Link>\n            </CustomPopconfirm>\n          </span>\n        </>\n      ),\n    },\n  ];\n}\n"
  },
  {
    "path": "client/src/modules/DiscordAdmin/hooks/index.ts",
    "content": "export * from './useDiscordServers';\n"
  },
  {
    "path": "client/src/modules/DiscordAdmin/hooks/useDiscordServers.ts",
    "content": "import { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { DiscordServersApi, UpdateDiscordServerDto, DiscordServerDto } from '@client/api';\n\nconst discordServersService = new DiscordServersApi();\n\nexport function useDiscordServers() {\n  const [data, setData] = useState<DiscordServerDto[]>([]);\n\n  const loadData = async () => {\n    const { data } = await discordServersService.getDiscordServers();\n    setData(data);\n  };\n\n  const { loading } = useAsync(loadData, []);\n\n  const createServer = async (values: UpdateDiscordServerDto) => {\n    await discordServersService.createDiscordServer(values);\n    await loadData();\n  };\n\n  const updateServer = async (id: number, values: UpdateDiscordServerDto) => {\n    await discordServersService.updateDiscordServer(id, values);\n    await loadData();\n  };\n\n  const deleteServer = async (id: number) => {\n    await discordServersService.deleteDiscordServer(id);\n    await loadData();\n  };\n\n  return { data, loading, createServer, updateServer, deleteServer };\n}\n"
  },
  {
    "path": "client/src/modules/DiscordAdmin/index.ts",
    "content": "export { DiscordAdminPage } from './pages/DiscordAdminPage';\nexport { useDiscordServers } from './hooks/useDiscordServers';\nexport { DiscordServersTable } from './components/DiscordServersTable';\nexport { DiscordServersModal } from './components/DiscordServersModal';\n"
  },
  {
    "path": "client/src/modules/DiscordAdmin/pages/DiscordAdminPage/DiscordAdminPage.tsx",
    "content": "import { Button, Layout, message } from 'antd';\nimport { useCallback, useState } from 'react';\nimport { DiscordServerDto, UpdateDiscordServerDto } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { DiscordServersTable } from '../../components/DiscordServersTable';\nimport { DiscordServersModal } from '../../components/DiscordServersModal';\nimport { useDiscordServers } from '../../hooks/useDiscordServers';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\nconst { Content } = Layout;\n\nenum ModalAction {\n  update = 'update',\n  create = 'create',\n}\n\nexport function DiscordAdminPage() {\n  const { courses } = useActiveCourseContext();\n  const { data, loading, createServer, updateServer, deleteServer } = useDiscordServers();\n  const [modalData, setModalData] = useState<Partial<DiscordServerDto> | null>(null);\n  const [modalAction, setModalAction] = useState(ModalAction.update);\n  const [modalLoading, setModalLoading] = useState(false);\n\n  const handleAddItem = () => {\n    setModalData({});\n    setModalAction(ModalAction.create);\n  };\n\n  const handleEditItem = (record: DiscordServerDto) => {\n    setModalData(record);\n    setModalAction(ModalAction.update);\n  };\n\n  const handleDeleteItem = async (id: number) => {\n    try {\n      await deleteServer(id);\n    } catch {\n      message.error('Failed to delete discord/telegram channel. Please try later.');\n    }\n  };\n\n  const handleModalSubmit = useCallback(\n    async (values: UpdateDiscordServerDto) => {\n      try {\n        if (modalLoading) {\n          return;\n        }\n        setModalLoading(true);\n        if (modalAction === ModalAction.update) {\n          await updateServer(modalData!.id!, values);\n        } else {\n          await createServer(values);\n        }\n        setModalData(null);\n      } catch {\n        message.error('An error occurred. Cannot save discord/telegram channel.');\n      } finally {\n        setModalLoading(false);\n      }\n    },\n    [modalAction, modalData, modalLoading, createServer, updateServer],\n  );\n\n  function getInitialValues(modalData: Partial<DiscordServerDto>) {\n    return modalData;\n  }\n\n  return (\n    <AdminPageLayout title=\"Manage Discord/Telegram\" loading={loading} courses={courses}>\n      <Content style={{ margin: 8 }}>\n        <Button type=\"primary\" onClick={handleAddItem}>\n          Add Discord/Telegram channel\n        </Button>\n        <DiscordServersTable data={data} onEdit={handleEditItem} onDelete={handleDeleteItem} />\n      </Content>\n      <DiscordServersModal\n        data={modalData}\n        title=\"Discord/Telegram channel\"\n        submit={handleModalSubmit}\n        cancel={() => setModalData(null)}\n        getInitialValues={getInitialValues}\n        loading={modalLoading}\n      />\n    </AdminPageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/DiscordAdmin/pages/DiscordAdminPage/index.ts",
    "content": "export { DiscordAdminPage } from './DiscordAdminPage';\n"
  },
  {
    "path": "client/src/modules/EventsAdmin/components/EventsModal.tsx",
    "content": "import { Form, Input, Select } from 'antd';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { EventDto, DisciplineDto } from '@client/api';\nimport { EVENT_TYPES } from '@client/data/eventTypes';\n\ntype Props = {\n  data: Partial<EventDto> | null;\n  title: string;\n  submit: (values: any) => Promise<void>;\n  cancel: () => void;\n  getInitialValues: (data: Partial<EventDto>) => any;\n  disciplines: DisciplineDto[];\n};\n\nexport function EventsModal({ data, title, submit, cancel, getInitialValues, disciplines }: Props) {\n  return data ? (\n    <ModalForm data={data} title={title} submit={submit} cancel={cancel} getInitialValues={getInitialValues}>\n      <Form.Item name=\"name\" label=\"Name\" rules={[{ required: true, message: 'Please enter event name' }]}>\n        <Input />\n      </Form.Item>\n      <Form.Item name=\"type\" label=\"Event Type\" rules={[{ required: true, message: 'Please select a type' }]}>\n        <Select>\n          {EVENT_TYPES.map(({ name, id }) => (\n            <Select.Option key={id} value={id}>\n              {name}\n            </Select.Option>\n          ))}\n        </Select>\n      </Form.Item>\n      <Form.Item\n        required\n        name=\"disciplineId\"\n        label=\"Discipline\"\n        rules={[{ required: true, message: 'Please select a discipline' }]}\n      >\n        <Select>\n          {disciplines.map(({ id, name }) => (\n            <Select.Option key={id} value={id}>\n              {name}\n            </Select.Option>\n          ))}\n        </Select>\n      </Form.Item>\n      <Form.Item name=\"descriptionUrl\" label=\"Description URL\">\n        <Input />\n      </Form.Item>\n      <Form.Item name=\"description\" label=\"Description\">\n        <Input.TextArea />\n      </Form.Item>\n    </ModalForm>\n  ) : null;\n}\n"
  },
  {
    "path": "client/src/modules/EventsAdmin/components/EventsTable.tsx",
    "content": "import { Table, Typography } from 'antd';\nimport { stringSorter, stringTrimRenderer, getColumnSearchProps } from '@client/shared/components/Table';\nimport { EventDto } from '@client/api';\nimport { CustomPopconfirm } from '@client/components/common/CustomPopconfirm';\nimport { ColumnsType } from 'antd/lib/table';\n\ntype Props = {\n  data: EventDto[];\n  onEdit: (record: EventDto) => void;\n  onDelete: (id: number) => void;\n};\n\nexport function EventsTable({ data, onEdit, onDelete }: Props) {\n  return (\n    <Table\n      size=\"small\"\n      style={{ marginTop: 8 }}\n      dataSource={data}\n      pagination={{ pageSize: 100 }}\n      rowKey=\"id\"\n      columns={getColumns(onEdit, onDelete) as ColumnsType<EventDto>}\n    />\n  );\n}\n\nfunction getColumns(handleEditItem: (record: EventDto) => void, handleDeleteItem: (id: number) => void) {\n  return [\n    {\n      title: 'Id',\n      dataIndex: 'id',\n    },\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      sorter: stringSorter<EventDto>('name'),\n      ...getColumnSearchProps('name'),\n    },\n    {\n      title: 'Discipline',\n      dataIndex: ['discipline', 'name'],\n    },\n    {\n      title: 'Description URL',\n      dataIndex: 'descriptionUrl',\n    },\n    {\n      title: 'Description',\n      dataIndex: 'description',\n      render: stringTrimRenderer,\n    },\n    {\n      title: 'Type',\n      dataIndex: 'type',\n    },\n    {\n      title: 'Actions',\n      dataIndex: 'actions',\n      width: 100,\n      render: (_: any, record: EventDto) => (\n        <>\n          <span>\n            <Typography.Link onClick={() => handleEditItem(record)}>Edit</Typography.Link>{' '}\n          </span>\n          <span style={{ marginLeft: 8 }}>\n            <CustomPopconfirm\n              onConfirm={() => handleDeleteItem(record.id)}\n              title=\"Are you sure you want to delete this item?\"\n            >\n              <Typography.Link>Delete</Typography.Link>\n            </CustomPopconfirm>\n          </span>\n        </>\n      ),\n    },\n  ];\n}\n"
  },
  {
    "path": "client/src/modules/EventsAdmin/hooks/index.ts",
    "content": "export * from './useEvents';\n"
  },
  {
    "path": "client/src/modules/EventsAdmin/hooks/useEvents.ts",
    "content": "import { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { EventsApi, DisciplinesApi, EventDto, DisciplineDto } from '@client/api';\n\nconst eventsApi = new EventsApi();\nconst disciplinesApi = new DisciplinesApi();\n\nexport function useEvents() {\n  const [data, setData] = useState<EventDto[]>([]);\n  const [disciplines, setDisciplines] = useState<DisciplineDto[]>([]);\n\n  const loadData = async () => {\n    const [{ data: events }, { data: disciplines }] = await Promise.all([\n      eventsApi.getEvents(),\n      disciplinesApi.getDisciplines(),\n    ]);\n    setData(events);\n    setDisciplines(disciplines || []);\n  };\n\n  const { loading } = useAsync(loadData, []);\n\n  const createEvent = async (values: any) => {\n    await eventsApi.createEvent(values);\n    await loadData();\n  };\n\n  const updateEvent = async (id: number, values: any) => {\n    await eventsApi.updateEvent(id, values);\n    await loadData();\n  };\n\n  const deleteEvent = async (id: number) => {\n    await eventsApi.deleteEvent(id);\n    await loadData();\n  };\n\n  return { data, disciplines, loading, createEvent, updateEvent, deleteEvent };\n}\n"
  },
  {
    "path": "client/src/modules/EventsAdmin/index.ts",
    "content": "export { EventsAdminPage } from './pages/EventsAdminPage';\nexport { useEvents } from './hooks/useEvents';\nexport { EventsTable } from './components/EventsTable';\nexport { EventsModal } from './components/EventsModal';\n"
  },
  {
    "path": "client/src/modules/EventsAdmin/pages/EventsAdminPage/EventsAdminPage.tsx",
    "content": "import { Button, Layout, message } from 'antd';\nimport { useCallback, useState } from 'react';\nimport { EventDto } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { EventsTable } from '../../components/EventsTable';\nimport { EventsModal } from '../../components/EventsModal';\nimport { useEvents } from '../../hooks/useEvents';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CreateEventDto } from '@client/api';\n\nconst { Content } = Layout;\n\nexport function EventsAdminPage() {\n  const { courses } = useActiveCourseContext();\n  const { data, disciplines, loading, createEvent, updateEvent, deleteEvent } = useEvents();\n  const [modalData, setModalData] = useState<Partial<EventDto> | null>(null);\n  const [modalAction, setModalAction] = useState('update');\n\n  const handleAddItem = () => {\n    setModalData({});\n    setModalAction('create');\n  };\n\n  const handleEditItem = (record: EventDto) => {\n    setModalData(record);\n    setModalAction('update');\n  };\n\n  const handleDeleteItem = async (id: number) => {\n    try {\n      await deleteEvent(id);\n    } catch {\n      message.error('Failed to delete item. Please try later.');\n    }\n  };\n\n  const handleModalSubmit = useCallback(\n    async (values: any) => {\n      try {\n        const record: CreateEventDto = {\n          name: values.name,\n          description: values.description,\n          descriptionUrl: values.descriptionUrl,\n          type: values.type,\n          disciplineId: values.disciplineId,\n        };\n        if (modalAction === 'update') {\n          await updateEvent(modalData!.id!, record);\n        } else {\n          await createEvent(record);\n        }\n        setModalData(null);\n      } catch (e) {\n        console.error(e);\n        message.error('An error occurred. Cannot save event.');\n      }\n    },\n    [modalAction, modalData, createEvent, updateEvent],\n  );\n\n  function getInitialValues(modalData: Partial<EventDto>) {\n    return { ...modalData, disciplineId: modalData.discipline?.id };\n  }\n\n  return (\n    <AdminPageLayout title=\"Manage Events\" loading={loading} courses={courses}>\n      <Content style={{ margin: 8 }}>\n        <Button type=\"primary\" onClick={handleAddItem}>\n          Add Event\n        </Button>\n        <EventsTable data={data} onEdit={handleEditItem} onDelete={handleDeleteItem} />\n      </Content>\n      <EventsModal\n        data={modalData}\n        title=\"Event\"\n        submit={handleModalSubmit}\n        cancel={() => setModalData(null)}\n        getInitialValues={getInitialValues}\n        disciplines={disciplines}\n      />\n    </AdminPageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/EventsAdmin/pages/EventsAdminPage/index.ts",
    "content": "export { EventsAdminPage } from './EventsAdminPage';\n"
  },
  {
    "path": "client/src/modules/Feedback/components/FeedbackForm.tsx",
    "content": "import { Alert, Button, Col, Form, Input, message, Radio, Rate, Row, Typography } from 'antd';\nimport {\n  CreateStudentFeedbackDto,\n  CreateStudentFeedbackDtoEnglishLevelEnum as EnglishLevelEnum,\n  MentorStudentDto,\n  CreateStudentFeedbackDtoRecommendationEnum as RecommendationEnum,\n  SoftSkillEntryIdEnum,\n} from '@client/api';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { useEffect } from 'react';\nimport { convertSoftSkillValueToEnum, softSkills, softSkillValues } from '../data/softSkills';\nimport { useRouter } from 'next/router';\n\ntype FormValues = Record<SoftSkillEntryIdEnum, number> & {\n  studentId: number;\n  suggestions: string;\n  recommendation: RecommendationEnum;\n  recommendationComment: string;\n  englishLevel: EnglishLevelEnum;\n};\n\nconst englishLevels = [\n  EnglishLevelEnum.A1,\n  EnglishLevelEnum.A2,\n  EnglishLevelEnum.B1,\n  EnglishLevelEnum.B2,\n  EnglishLevelEnum.C1,\n  EnglishLevelEnum.C2,\n];\n\ntype FeedbackFormProps = {\n  studentId: number;\n  students?: MentorStudentDto[];\n  onSubmit: (studentId: number, payload: CreateStudentFeedbackDto, existingFeedbackId?: number) => Promise<void>;\n};\n\nconst { TextArea } = Input;\n\nconst getInitialValues = (studentId: number, students: MentorStudentDto[] | undefined): FormValues | undefined => {\n  const selectedStudent = students?.find(student => student.id === studentId);\n  if (selectedStudent) {\n    const feedback = selectedStudent.feedbacks[0];\n    if (feedback) {\n      return {\n        studentId,\n        suggestions: feedback.content.suggestions,\n        recommendation: feedback.recommendation,\n        recommendationComment: feedback.content.recommendationComment,\n        englishLevel: feedback.englishLevel,\n        ...feedback.content.softSkills.reduce(\n          (acc, { id, value }) => {\n            acc[id as SoftSkillEntryIdEnum] = softSkillValues[value];\n            return acc;\n          },\n          {} as Record<SoftSkillEntryIdEnum, number>,\n        ),\n      };\n    }\n  }\n  return { studentId } as FormValues;\n};\n\nexport const FeedbackForm = ({ studentId, onSubmit, students }: FeedbackFormProps) => {\n  const router = useRouter();\n  const [form] = Form.useForm<FormValues>();\n\n  useEffect(() => {\n    const initialValues = getInitialValues(studentId, students);\n    if (initialValues) {\n      form.setFieldsValue(initialValues);\n    }\n  }, [studentId, students, form]);\n\n  const handleStudentChange = (value: string) => {\n    const newStudentId = Number(value);\n    const initialValues = getInitialValues(newStudentId, students);\n    if (initialValues) {\n      form.resetFields();\n      form.setFieldsValue(initialValues);\n    } else {\n      form.resetFields();\n      form.setFieldsValue({ studentId: newStudentId });\n    }\n    router.push(\n      {\n        pathname: router.pathname,\n        query: { ...router.query, studentId: newStudentId },\n      },\n      undefined,\n      { shallow: true },\n    );\n  };\n\n  const handleSubmit = async (values: FormValues) => {\n    try {\n      const { studentId, ...rest } = values;\n\n      const payload: CreateStudentFeedbackDto = {\n        recommendation: rest.recommendation,\n        content: {\n          suggestions: rest.suggestions ?? '',\n          recommendationComment: rest.recommendationComment,\n          softSkills: softSkills.map(({ id }) => ({ id, value: convertSoftSkillValueToEnum(rest[id]) })),\n        },\n        englishLevel: rest.englishLevel ? rest.englishLevel : EnglishLevelEnum.Unknown,\n      };\n\n      const selectedStudent = students?.find(student => student.id === studentId);\n      const existingFeedback = selectedStudent?.feedbacks[0];\n\n      await onSubmit(studentId, payload, existingFeedback ? existingFeedback.id : undefined);\n    } catch {\n      message.error('Error occurred while creating feedback. Please try later.');\n    }\n  };\n\n  return (\n    <Form\n      style={{ margin: '24px 0' }}\n      onFinish={handleSubmit}\n      form={form}\n      layout=\"vertical\"\n      initialValues={{ studentId }}\n    >\n      <Alert\n        showIcon\n        type=\"info\"\n        message={\n          <>\n            <div>This feedback is very important for RS School process.</div>\n            <div>Please spend 5 minutes to complete it. Thank you!</div>\n          </>\n        }\n      />\n      <Alert\n        style={{ marginTop: 8 }}\n        showIcon\n        type=\"warning\"\n        message={\n          <div>If you recommend to \"Hire\", we will attach the feedback to student's CV and it will be public.</div>\n        }\n      />\n      <Form.Item name=\"studentId\" label=\"Student\">\n        <UserSearch allowClear={false} defaultValues={students} keyField=\"id\" onChange={handleStudentChange} />\n      </Form.Item>\n      <Typography.Title level={5}>Recommended To</Typography.Title>\n      <Form.Item name=\"recommendation\" rules={[{ required: true, message: 'Required' }]}>\n        <Radio.Group>\n          <Radio.Button value={RecommendationEnum.Hire}>Hire</Radio.Button>\n          <Radio.Button value={RecommendationEnum.NotHire}>Not Hire</Radio.Button>\n        </Radio.Group>\n      </Form.Item>\n      <Form.Item name=\"recommendationComment\" rules={[{ required: true, message: 'Required' }]} label=\"What was good\">\n        <TextArea rows={7} allowClear />\n      </Form.Item>\n      <Form.Item name=\"suggestions\" label=\"What could be improved\">\n        <TextArea rows={3} allowClear />\n      </Form.Item>\n      <Typography.Title level={5}>English</Typography.Title>\n      <Form.Item label=\"Approximate English level\" name=\"englishLevel\">\n        <Radio.Group>\n          {englishLevels.map(level => (\n            <Radio.Button key={level} value={level}>\n              {level.toUpperCase()}\n            </Radio.Button>\n          ))}\n        </Radio.Group>\n      </Form.Item>\n\n      <Typography.Title level={5}>Soft Skills</Typography.Title>\n      <Row wrap={true}>\n        {softSkills.map(({ id, name }) => (\n          <Col key={id} span={12}>\n            <Form.Item key={id} label={name} name={id}>\n              <Rate />\n            </Form.Item>\n          </Col>\n        ))}\n      </Row>\n      <Button htmlType=\"submit\" type=\"primary\">\n        Submit\n      </Button>\n    </Form>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Feedback/data/softSkills.ts",
    "content": "import { SoftSkillEntryIdEnum, SoftSkillEntryValueEnum } from '@client/api';\n\nexport const softSkills: { id: SoftSkillEntryIdEnum; name: string }[] = [\n  {\n    id: SoftSkillEntryIdEnum.Responsible,\n    name: 'Responsible',\n  },\n  {\n    id: SoftSkillEntryIdEnum.TeamPlayer,\n    name: 'Good team player',\n  },\n  {\n    id: SoftSkillEntryIdEnum.Communicable,\n    name: 'Communicable',\n  },\n];\n\nexport const convertSoftSkillValueToEnum = (value: number | null) => {\n  switch (value) {\n    case 1:\n      return SoftSkillEntryValueEnum.Poor;\n    case 2:\n      return SoftSkillEntryValueEnum.Fair;\n    case 3:\n      return SoftSkillEntryValueEnum.Good;\n    case 4:\n      return SoftSkillEntryValueEnum.Great;\n    case 5:\n      return SoftSkillEntryValueEnum.Excellent;\n    default:\n      return SoftSkillEntryValueEnum.None;\n  }\n};\n\nexport const softSkillValues: Record<SoftSkillEntryValueEnum, number> = {\n  [SoftSkillEntryValueEnum.None]: 0,\n  [SoftSkillEntryValueEnum.Poor]: 1,\n  [SoftSkillEntryValueEnum.Fair]: 2,\n  [SoftSkillEntryValueEnum.Good]: 3,\n  [SoftSkillEntryValueEnum.Great]: 4,\n  [SoftSkillEntryValueEnum.Excellent]: 5,\n};\n"
  },
  {
    "path": "client/src/modules/Home/components/CourseLinks/CourseLinks.tsx",
    "content": "import { List } from 'antd';\nimport Link from 'next/link';\nimport { LinkRenderData } from '@client/modules/Home/data/links';\n\ntype CourseLinksProps = {\n  courseLinks: LinkRenderData[];\n};\n\nexport default function CourseLinks({ courseLinks }: CourseLinksProps) {\n  if (!courseLinks.length) {\n    return null;\n  }\n\n  return (\n    <List\n      size=\"small\"\n      bordered\n      dataSource={courseLinks}\n      renderItem={linkInfo => (\n        <List.Item key={linkInfo.url}>\n          <Link prefetch={false} href={linkInfo.url}>\n            {linkInfo.icon} {linkInfo.name}\n          </Link>\n        </List.Item>\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Home/components/CourseLinks/index.tsx",
    "content": "import { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\nexport const DynamiCourseLinks = dynamicWithSkeleton(() => import('./CourseLinks'));\n\nexport default DynamiCourseLinks;\n"
  },
  {
    "path": "client/src/modules/Home/components/CourseSelector/index.tsx",
    "content": "import { Select } from 'antd';\nimport { CSSProperties } from 'react';\nimport { Course } from '@client/services/models';\nimport { CourseIcon } from '@client/shared/components/Icons/CourseIcon';\n\ntype Props = {\n  course: Course | null;\n  onChangeCourse: (courseId: number) => void;\n  courses: Course[];\n};\n\nexport function CourseSelector(props: Props) {\n  const { course, courses, onChangeCourse } = props;\n\n  if (!course) {\n    return null;\n  }\n  return (\n    <Select\n      showSearch\n      optionFilterProp=\"children\"\n      style={{ width: 300, marginBottom: 16 }}\n      defaultValue={course.id}\n      onChange={onChangeCourse}\n    >\n      {[...courses]\n        .sort((a, b) => Number(a.completed) - Number(b.completed))\n        .map(course => (\n          <Select.Option style={getStatusCss(course)} key={course.id} value={course.id}>\n            <CourseIcon course={course} /> {course.name} {getStatus(course)}\n          </Select.Option>\n        ))}\n    </Select>\n  );\n}\n\nconst getStatus = (course: Course) => {\n  if (course.completed) {\n    return `(Archived)`;\n  }\n  return '';\n};\n\nconst getStatusCss = (course: Course): CSSProperties => {\n  if (course.completed) {\n    return {\n      color: '#999',\n    };\n  }\n  return {};\n};\n"
  },
  {
    "path": "client/src/modules/Home/components/HomeSummary/HomeSummary.tsx",
    "content": "import { Card, Col, Row, Statistic, theme, Typography } from 'antd';\nimport { StudentSummaryDto } from '@client/api';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\n\ntype HomeSummaryProps = {\n  summary: StudentSummaryDto | null;\n  courseTasks: { id: number }[];\n};\n\nexport default function HomeSummary({ summary, courseTasks }: HomeSummaryProps) {\n  const { token } = theme.useToken();\n\n  if (!summary) {\n    return null;\n  }\n\n  const {\n    name,\n    githubId,\n    contactsEmail,\n    contactsPhone,\n    contactsSkype,\n    contactsTelegram,\n    contactsNotes,\n    contactsWhatsApp,\n  } = summary.mentor ?? {};\n  const tasksCount = summary.results.filter(r => Number(r.score) > 0).length;\n  const totalTaskCount = courseTasks.length;\n\n  const contacts = [\n    { label: 'Email', value: contactsEmail },\n    { label: 'Phone', value: contactsPhone },\n    { label: 'Skype', value: contactsSkype },\n    { label: 'Telegram', value: contactsTelegram },\n    { label: 'Notes', value: contactsNotes },\n    { label: 'WhatsApp', value: contactsWhatsApp },\n  ];\n\n  return (\n    <Row gutter={24}>\n      <Col xs={24} sm={24} md={24} lg={12}>\n        <Card style={{ marginBottom: 16 }} size=\"small\" title=\"Your stats\">\n          <Row>\n            <Col span={12}>\n              <Statistic title=\"Score Points\" value={summary.totalScore} />\n            </Col>\n            <Col span={12}>\n              <Statistic title=\"Completed Tasks\" value={`${tasksCount}/${totalTaskCount}`} />\n            </Col>\n            <Col span={24} style={{ marginTop: 16 }}>\n              <Statistic\n                title=\"Status\"\n                valueStyle={{ color: summary.isActive ? token.green6 : token.red6 }}\n                value={summary.isActive ? 'Active' : 'Inactive'}\n              />\n            </Col>\n          </Row>\n        </Card>\n      </Col>\n      {summary.mentor && (\n        <Col xs={24} sm={24} md={24} lg={12}>\n          <Card size=\"small\" title=\"Your mentor\">\n            <div>\n              <div>{name}</div>\n              <div>\n                <GithubUserLink value={githubId!} />\n              </div>\n            </div>\n            {contacts.map(({ label, value }, index) =>\n              value ? (\n                <Typography.Paragraph key={index}>\n                  <Typography.Text type=\"secondary\">{label}:</Typography.Text> {value}\n                </Typography.Paragraph>\n              ) : null,\n            )}\n          </Card>\n        </Col>\n      )}\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Home/components/HomeSummary/index.tsx",
    "content": "import { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\n\nexport const DynamicHomeSummary = dynamicWithSkeleton(() => import('./HomeSummary'));\n\nexport default DynamicHomeSummary;\n"
  },
  {
    "path": "client/src/modules/Home/components/NoCourse/index.tsx",
    "content": "import { CheckSquareOutlined, QuestionCircleTwoTone, StarOutlined, UserOutlined } from '@ant-design/icons';\nimport { Button, Result, Row } from 'antd';\nimport { Course } from '@client/services/models';\n\ntype Props = {\n  courses: Course[];\n  preselectedCourses: Course[];\n};\n\nexport function NoCourse({ courses, preselectedCourses }: Props) {\n  {\n    const hasPlanned = courses.some(course => course.planned && !course.completed);\n    return (\n      <Result\n        icon={<QuestionCircleTwoTone twoToneColor=\"#52c41a\" />}\n        title=\"You are not student or mentor in any active course\"\n        subTitle={\n          <div>\n            <span>\n              {hasPlanned\n                ? 'You can register to the upcoming course.'\n                : 'Unfortunately, there are no any planned courses for students but you can always register as mentor'}\n              <Button target=\"_blank\" size=\"small\" type=\"link\" href=\"https://docs.rs.school/#/rs-school-mentor\">\n                More info\n              </Button>\n            </span>\n          </div>\n        }\n        extra={\n          <>\n            <Row justify=\"center\">\n              <Button size=\"large\" icon={<StarOutlined />} type=\"default\" href=\"/registry/mentor\">\n                Register as Mentor\n              </Button>\n              {hasPlanned && (\n                <Button\n                  style={{ marginLeft: 16 }}\n                  size=\"large\"\n                  icon={<UserOutlined />}\n                  href=\"/registry/student\"\n                  type=\"default\"\n                >\n                  Register as Student\n                </Button>\n              )}\n            </Row>\n            <Row justify=\"center\" style={{ marginTop: 16 }}>\n              {preselectedCourses.map(c => (\n                <Button\n                  key={c.id}\n                  size=\"large\"\n                  icon={<CheckSquareOutlined />}\n                  type=\"primary\"\n                  href={`/course/mentor/confirm?course=${c.alias}`}\n                >\n                  Confirm {c.name}\n                </Button>\n              ))}\n            </Row>\n          </>\n        }\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Home/components/RegistryBanner/index.tsx",
    "content": "import { Alert, Button, AlertProps } from 'antd';\n\nexport function RegistryBanner(props: Partial<AlertProps>) {\n  return (\n    <Alert\n      type=\"info\"\n      showIcon\n      message=\"New RS School Course is starting and we are looking for mentors!\"\n      description={\n        <Button type=\"primary\" href=\"/registry/mentor\">\n          Register as Mentor\n        </Button>\n      }\n      {...props}\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Home/components/SystemAlerts/index.tsx",
    "content": "import { Alert } from 'antd';\nimport type { AlertDto } from '@client/api';\n\ntype Props = {\n  alerts: AlertDto[];\n};\n\nexport function SystemAlerts({ alerts }: Props) {\n  return (\n    <>\n      {alerts.map(({ text, type }) => {\n        const alertType = type === 'warn' ? 'warning' : type;\n        return <Alert key={text} style={{ margin: '8px 0' }} type={alertType as any} showIcon message={text} />;\n      })}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Home/data/links.tsx",
    "content": "import {\n  AppstoreOutlined,\n  UsergroupAddOutlined,\n  FireOutlined,\n  DashboardOutlined,\n  GoldOutlined,\n  CodeOutlined,\n  CheckCircleOutlined,\n  AudioOutlined,\n  PlayCircleOutlined,\n  StopOutlined,\n} from '@ant-design/icons';\nimport { Session } from '@client/components/withSession';\nimport { Course } from '@client/services/models';\nimport { isStudent, isAdmin, isMentor, isCourseManager, isActiveStudent, isDementor } from '@client/domain/user';\nimport { getAutoTestRoute } from '@client/services/routes';\nimport { MenuProps } from 'antd';\nimport Router from 'next/router';\nimport CalendarOutlined from '@ant-design/icons/CalendarOutlined';\n\nconst anyAccess = () => true;\nconst isCourseNotCompleted = (_: Session, course: Course) => !course.completed;\n\nconst every =\n  (...checks: ((session: Session, courseId: number) => boolean)[]) =>\n  (session: Session, courseId: number) =>\n    checks.every(check => check(session, courseId));\n\nconst everyCourse =\n  (...checks: ((session: Session, course: Course) => boolean)[]) =>\n  (session: Session, course: Course) =>\n    checks.every(check => check(session, course));\n\nconst some =\n  (...checks: ((session: Session, courseId: number) => boolean)[]) =>\n  (session: Session, courseId: number) =>\n    checks.some(check => check(session, courseId));\n\nexport type LinkData = {\n  name: string;\n  icon?: JSX.Element;\n  getUrl: (course: Course) => string;\n  access: (session: Session, courseId: number) => boolean;\n  courseAccess?: (session: Session, course: Course) => boolean;\n  newTab?: boolean;\n};\n\nexport type LinkRenderData = Pick<LinkData, 'icon' | 'name'> & { url: string };\n\nconst links: LinkData[] = [\n  {\n    name: 'Dashboard',\n    icon: <DashboardOutlined />,\n    getUrl: (course: Course) => `/course/student/dashboard?course=${course.alias}`,\n    access: every(isStudent),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Dashboard',\n    icon: <AppstoreOutlined />,\n    getUrl: (course: Course) => `/course/mentor/dashboard?course=${course.alias}`,\n    access: every(isMentor),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Score',\n    icon: <FireOutlined style={{ color: '#ffa500' }} />,\n    getUrl: (course: Course) => `/course/score?course=${course.alias}`,\n    access: anyAccess,\n  },\n  {\n    name: 'Schedule',\n    icon: <CalendarOutlined style={{ color: '#eb2f96' }} />,\n    getUrl: (course: Course) => `/course/schedule?course=${course.alias}`,\n    access: anyAccess,\n  },\n  {\n    name: 'My Students',\n    icon: <GoldOutlined style={{ color: '#7f00ff' }} />,\n    getUrl: (course: Course) => `/course/mentor/students?course=${course.alias}`,\n    access: every(isMentor),\n  },\n  {\n    name: 'Cross-Check: Submit',\n    icon: <CodeOutlined />,\n    getUrl: (course: Course) => `/course/student/cross-check-submit?course=${course.alias}`,\n    access: every(isActiveStudent),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Cross-Check: Review',\n    icon: <CheckCircleOutlined style={{ color: '#f56161' }} />,\n    getUrl: (course: Course) => `/course/student/cross-check-review?course=${course.alias}`,\n    access: every(isActiveStudent),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Interviews',\n    icon: <AudioOutlined />,\n    getUrl: (course: Course) => `/course/student/interviews?course=${course.alias}`,\n    access: every(isStudent),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Interviews',\n    icon: <AudioOutlined style={{ color: '#ffa500' }} />,\n    getUrl: (course: Course) => `/course/mentor/interviews?course=${course.alias}`,\n    access: every(isMentor),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Auto-Test',\n    icon: <PlayCircleOutlined style={{ color: '#7f00ff' }} />,\n    getUrl: (course: Course) => getAutoTestRoute(course.alias),\n    access: some(isActiveStudent, isCourseManager),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Expel/Unassign Student',\n    icon: <StopOutlined style={{ color: '#ff0000' }} />,\n    getUrl: (course: Course) => `/course/mentor/expel-student?course=${course.alias}`,\n    access: every(isMentor),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Team Distributions',\n    icon: <UsergroupAddOutlined style={{ color: '#7f00ff' }} />,\n    getUrl: (course: Course) => `/course/team-distributions?course=${course.alias}`,\n    access: some(isCourseManager, isActiveStudent, isDementor),\n    courseAccess: everyCourse(isCourseNotCompleted),\n  },\n  {\n    name: 'Course Statistics',\n    icon: <AppstoreOutlined />,\n    getUrl: (course: Course) => `/course/stats?course=${course.alias}`,\n    access: anyAccess,\n  },\n];\n\nexport function getCourseLinks(session: Session, activeCourse: Course | null): LinkRenderData[] {\n  return activeCourse\n    ? links\n        .filter(\n          route =>\n            isAdmin(session) ||\n            (route.access(session, activeCourse.id) && (route.courseAccess?.(session, activeCourse) ?? true)),\n        )\n        .map(({ name, icon, getUrl }) => ({ name, icon, url: getUrl(activeCourse) }))\n    : [];\n}\n\nexport function getNavigationItems(session: Session, activeCourse: Course | null): MenuProps['items'] {\n  if (!activeCourse || !activeCourse.id) return [];\n\n  return activeCourse\n    ? links\n        .filter(\n          route =>\n            isAdmin(session) ||\n            (route.access(session, activeCourse.id) && (route.courseAccess?.(session, activeCourse) ?? true)),\n        )\n        .map(({ name, icon, getUrl }) => ({\n          label: name,\n          icon,\n          key: getUrl(activeCourse),\n          onClick: () => Router.push(getUrl(activeCourse)),\n        }))\n    : [];\n}\n"
  },
  {
    "path": "client/src/modules/Home/data/loadHomeData.ts",
    "content": "import { CoursesTasksApi } from '@client/api';\nimport { CourseService } from '@client/services/course';\n\nexport async function loadHomeData(courseId: number, githubId: string) {\n  const [studentSummary, { data: courseTasks }] = await Promise.all([\n    new CourseService(courseId).getStudentSummary(githubId),\n    new CoursesTasksApi().getCourseTasks(courseId),\n  ]);\n  return {\n    studentSummary,\n    courseTasks: courseTasks.map(t => ({ id: t.id })),\n  };\n}\n"
  },
  {
    "path": "client/src/modules/Home/hooks/useActiveCourse.test.tsx",
    "content": "import { renderHook, act } from '@testing-library/react';\nimport { ProfileCourseDto } from '@client/api';\nimport { useActiveCourse } from './useActiveCourse';\nimport * as ReactUse from 'react-use';\n\ndescribe('useActiveCourse', () => {\n  const courses = [\n    { id: 1, name: 'Course 1' },\n    { id: 2, name: 'Course 2' },\n    { id: 3, name: 'Course 3' },\n  ] as ProfileCourseDto[];\n\n  it('should return the first course as the active course by default', () => {\n    const { result } = renderHook(() => useActiveCourse(courses));\n    expect(result.current[0]).toEqual(courses[0]);\n  });\n\n  it('should return the previously selected course when it is stored in local storage', () => {\n    vi.spyOn(ReactUse, 'useLocalStorage').mockReturnValueOnce(['2', vi.fn(), vi.fn()]);\n    const { result } = renderHook(() => useActiveCourse(courses));\n    expect(result.current[0]).toEqual(courses[1]);\n  });\n\n  it('should return the correct course when setActiveCourse is called', () => {\n    const { result } = renderHook(() => useActiveCourse(courses));\n    act(() => {\n      result.current[1](3);\n    });\n    expect(result.current[0]).toEqual(courses[2]);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Home/hooks/useActiveCourse.tsx",
    "content": "import { useState } from 'react';\nimport { useLocalStorage } from 'react-use';\nimport { Course } from '@client/services/models';\n\nexport function useActiveCourse(courses: Course[]): [Course | null, (courseId: number) => void] {\n  const [courseId, setCourseId] = useState<number>();\n  const [storageValue, setStorageValue] = useLocalStorage('activeCourseId');\n  const activeCourseId = courseId || Number(storageValue);\n  const course = courses.find(course => course.id === activeCourseId) ?? courses[0] ?? null;\n  return [\n    course,\n    (courseId: number) => {\n      setCourseId(courseId);\n      setStorageValue(courseId);\n    },\n  ];\n}\n"
  },
  {
    "path": "client/src/modules/Home/hooks/useStudentSummary.tsx",
    "content": "import { StudentSummaryDto } from '@client/api';\nimport { Session } from '@client/components/withSession';\nimport { isStudent } from '@client/domain/user';\nimport { loadHomeData } from '@client/modules/Home/data/loadHomeData';\nimport { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { Course } from '@client/services/models';\n\nexport function useStudentSummary(session: Session, course: Course | null) {\n  const [studentSummary, setStudentSummary] = useState<StudentSummaryDto | null>(null);\n  const [courseTasks, setCourseTasks] = useState<{ id: number }[]>([]);\n\n  useAsync(async () => {\n    const showData = course && isStudent(session, course.id);\n    const data = showData ? await loadHomeData(course.id, session.githubId) : null;\n    setStudentSummary(data?.studentSummary ?? null);\n    setCourseTasks(data?.courseTasks ?? []);\n  }, [course]);\n\n  return {\n    studentSummary,\n    courseTasks,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/Home/pages/HomePage/index.tsx",
    "content": "import { Alert, Button, Col, Layout, Row, theme } from 'antd';\nimport { AlertDto, AlertsApi } from '@client/api';\nimport { AdminSider } from '@client/shared/components/Sider/AdminSider';\nimport { FooterLayout } from '@client/components/Footer';\nimport { Header } from '@client/shared/components/Header';\nimport { isAdmin, isAnyCourseDementor, isAnyCoursePowerUser, isAnyMentor } from '@client/domain/user';\nimport HomeSummary from '@client/modules/Home/components/HomeSummary';\nimport { NoCourse } from '@client/modules/Home/components/NoCourse';\nimport { CourseSelector } from '@client/modules/Home/components/CourseSelector';\nimport { RegistryBanner } from '@client/modules/Home/components/RegistryBanner';\nimport { SystemAlerts } from '@client/modules/Home/components/SystemAlerts';\nimport { getCourseLinks } from '@client/modules/Home/data/links';\nimport { useStudentSummary } from '@client/modules/Home/hooks/useStudentSummary';\nimport { useContext, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CoursesService } from '@client/services/courses';\nimport { MentorRegistryService } from '@client/services/mentorRegistry';\nimport { Course } from '@client/services/models';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport CourseLinks from '@client/modules/Home/components/CourseLinks';\n\nconst { Content } = Layout;\n\nconst mentorRegistryService = new MentorRegistryService();\nconst alertService = new AlertsApi();\n\nexport function HomePage() {\n  const { courses = [], setCourse, course } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n  const plannedCourses = (courses || []).filter(course => course.planned && !course.inviteOnly);\n  const wasMentor = isAnyMentor(session);\n  const hasRegistryBanner =\n    !isAdmin(session) &&\n    wasMentor &&\n    plannedCourses.length > 0 &&\n    plannedCourses.every(course => session.courses[course.id] == null);\n\n  const isPowerUser = isAnyCoursePowerUser(session) || isAnyCourseDementor(session);\n  const [allCourses, setAllCourses] = useState<Course[]>([]);\n  const [preselectedCourses, setPreselectedCourses] = useState<Course[]>([]);\n  const [alerts, setAlerts] = useState<AlertDto[]>([]);\n\n  const courseLinks = useMemo(() => getCourseLinks(session, course), [course]);\n  const [approvedCourse] = preselectedCourses.filter(course => !session.courses?.[course.id]);\n\n  useAsync(async () => {\n    const { data } = await alertService.getAlerts(true);\n    setAlerts(data);\n  });\n\n  useAsync(async () => {\n    const mentor = await mentorRegistryService.getMentor().catch(() => null);\n    if (mentor == null) {\n      return;\n    }\n    const allCourses = await new CoursesService().getCourses();\n    const preselectedCourses = allCourses.filter(c => mentor?.preselectedCourses.includes(c.id));\n    setAllCourses(allCourses);\n    setPreselectedCourses(preselectedCourses);\n  });\n\n  const handleChangeCourse = (courseId: number) => {\n    const course = courses.find(course => {\n      return course.id === courseId;\n    });\n    if (course) {\n      setCourse(course);\n    }\n  };\n\n  const { courseTasks, studentSummary } = useStudentSummary(session, course);\n\n  const { token } = theme.useToken();\n\n  return (\n    <Layout style={{ minHeight: '100vh' }}>\n      <Header />\n      <Layout style={{ background: token.colorBgContainer }}>\n        {isPowerUser && <AdminSider courses={courses} activeCourse={course} />}\n        <Content style={{ margin: 16, marginBottom: 32 }}>\n          {!course && <NoCourse courses={allCourses} preselectedCourses={preselectedCourses} />}\n\n          {approvedCourse && (\n            <div style={{ margin: '16px 0' }}>\n              <Alert\n                type=\"success\"\n                showIcon\n                message={`You are approved as a mentor to \"${approvedCourse.name}\" course`}\n                description={\n                  <Button type=\"primary\" href={`/course/mentor/confirm?course=${approvedCourse.alias}`}>\n                    Confirm Participation\n                  </Button>\n                }\n              />\n            </div>\n          )}\n\n          <SystemAlerts alerts={alerts} />\n\n          {hasRegistryBanner && <RegistryBanner style={{ margin: '16px 0' }} />}\n\n          <CourseSelector course={course} onChangeCourse={handleChangeCourse} courses={courses} />\n\n          <Row gutter={24}>\n            <Col xs={24} sm={12} md={10} lg={8} style={{ marginBottom: 16 }}>\n              <CourseLinks courseLinks={courseLinks} />\n            </Col>\n            <Col xs={24} sm={12} md={12} lg={16}>\n              <HomeSummary courseTasks={courseTasks} summary={studentSummary} />\n            </Col>\n          </Row>\n        </Content>\n      </Layout>\n      <FooterLayout />\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interview/Student/components/AlertDescription.module.css",
    "content": ".icon {\n  background-image: url('https://cdn.rs.school/sloths/cleaned/lazy.svg');\n  background-position: center;\n  background-size: contain;\n  background-repeat: no-repeat;\n  max-width: 270px;\n  height: 160px;\n  margin: 10px auto;\n}\n"
  },
  {
    "path": "client/src/modules/Interview/Student/components/AlertDescription.tsx",
    "content": "import styles from './AlertDescription.module.css';\n\nexport const AlertDescription = ({ backgroundImage }: { backgroundImage?: string }) => {\n  return <div className={styles.icon} style={{ backgroundImage }} />;\n};\n"
  },
  {
    "path": "client/src/modules/Interview/Student/components/ExtraInfo.tsx",
    "content": "import { CheckCircleOutlined } from '@ant-design/icons';\nimport { Button, Tag } from 'antd';\nimport { formatShortDate } from '@client/services/formatter';\nimport { isRegistrationNotStarted } from '@client/domain/interview';\n\nexport const ExtraInfo = ({\n  id,\n  registrationStart,\n  isRegistered,\n  onRegister,\n}: {\n  id: number;\n  registrationStart: string;\n  isRegistered: boolean;\n  onRegister: (id: string) => void;\n}) => {\n  const registrationNotStarted = isRegistrationNotStarted(registrationStart);\n\n  return registrationNotStarted ? (\n    <Tag color=\"orange\">Registration starts on {formatShortDate(registrationStart)}</Tag>\n  ) : (\n    <Button\n      onClick={() => onRegister(id.toString())}\n      icon={isRegistered ? <CheckCircleOutlined /> : null}\n      disabled={isRegistered}\n      type={isRegistered ? 'default' : 'primary'}\n    >\n      {isRegistered ? 'Registered' : 'Register'}\n    </Button>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Interview/Student/components/InterviewCard.tsx",
    "content": "import { Col, Card, Button, Alert, Typography, Flex } from 'antd';\nimport { CommentOutlined, InfoCircleTwoTone } from '@ant-design/icons';\nimport { InterviewDto } from '@client/api';\nimport {\n  getInterviewCardResult,\n  InterviewDetails,\n  InterviewPeriod,\n  InterviewStatus,\n  isRegistrationNotStarted,\n} from '@client/domain/interview';\nimport { InterviewDescription } from './InterviewDescription';\nimport { getInterviewCardDetails } from '../data/getInterviewCardDetails';\nimport { AlertDescription } from './AlertDescription';\nimport { ExtraInfo } from './ExtraInfo';\nimport { Decision } from '@client/data/interviews/technical-screening';\n\nconst { Meta } = Card;\n\nexport const InterviewCard = ({\n  comment,\n  interview,\n  item,\n  isRegistered,\n  onRegister,\n}: {\n  comment?: string | null;\n  interview: InterviewDto;\n  item: InterviewDetails | null;\n  isRegistered: boolean;\n  onRegister: (id: string) => void;\n}) => {\n  const { id, descriptionUrl, name, startDate, endDate, studentRegistrationStartDate: registrationStart } = interview;\n  const interviewPassed = item?.status === InterviewStatus.Completed;\n  const interviewResult = getInterviewCardResult(item?.result as Decision);\n  const hasInterviewPair = !!item;\n\n  const registrationNotStarted = isRegistrationNotStarted(registrationStart);\n  const { cardMessage, backgroundImage } = getInterviewCardDetails({\n    interviewResult,\n    interviewPassed,\n    isRegistered,\n    registrationNotStarted,\n    registrationStart,\n    hasInterviewPair,\n  });\n\n  return (\n    <Col key={id} xs={24} lg={12}>\n      <Card\n        bodyStyle={{ paddingTop: 0 }}\n        hoverable\n        title={\n          <Button type=\"link\" href={descriptionUrl} target=\"_blank\" style={{ padding: 0, fontWeight: 500 }}>\n            {name}\n          </Button>\n        }\n        extra={<InterviewPeriod startDate={startDate} endDate={endDate} shortDate />}\n      >\n        <Meta\n          style={{ minHeight: 80, alignItems: 'center', textAlign: 'center' }}\n          description={\n            item ? (\n              <InterviewDescription {...item} />\n            ) : (\n              <ExtraInfo\n                id={id}\n                registrationStart={registrationStart}\n                isRegistered={isRegistered}\n                onRegister={onRegister}\n              />\n            )\n          }\n        />\n        <Flex vertical gap=\"small\">\n          <Alert\n            message={<div style={{ minHeight: 50 }}>{cardMessage}</div>}\n            icon={<InfoCircleTwoTone />}\n            showIcon\n            type=\"info\"\n            description={<AlertDescription backgroundImage={backgroundImage} />}\n            style={{ minHeight: 275 }}\n          />\n          {comment && (\n            <Alert\n              message={\n                <Typography.Paragraph ellipsis={{ rows: 1, tooltip: true }} style={{ marginBottom: 0 }}>\n                  {comment}\n                </Typography.Paragraph>\n              }\n              icon={<CommentOutlined />}\n              showIcon\n              type=\"success\"\n            />\n          )}\n        </Flex>\n      </Card>\n    </Col>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Interview/Student/components/InterviewDescription.tsx",
    "content": "import { Descriptions } from 'antd';\nimport { InterviewDetails, getInterviewResult } from '@client/domain/interview';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { Decision } from '@client/data/interviews/technical-screening';\nimport { StatusLabel } from './StatusLabel';\n\nexport const InterviewDescription = ({ interviewer, status, result }: InterviewDetails) => {\n  return (\n    <div style={{ padding: '8px 0' }}>\n      <Descriptions layout=\"vertical\" size=\"small\">\n        <Descriptions.Item label=\"Interviewer\">\n          <GithubUserLink value={interviewer.githubId} />\n        </Descriptions.Item>\n        <Descriptions.Item label=\"Status\">\n          <StatusLabel status={status} />\n        </Descriptions.Item>\n        <Descriptions.Item label=\"Result\">\n          <b>{getInterviewResult(result as Decision) ?? '-'}</b>\n        </Descriptions.Item>\n      </Descriptions>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Interview/Student/components/NoInterviewsAlert.tsx",
    "content": "import { InfoCircleTwoTone } from '@ant-design/icons';\nimport { Row, Col, Alert } from 'antd';\nimport { AlertDescription } from './AlertDescription';\n\nexport const NoInterviewsAlert = () => (\n  <Row justify=\"center\">\n    <Col xs={24} lg={12}>\n      <Alert\n        type=\"info\"\n        showIcon\n        icon={<InfoCircleTwoTone />}\n        message=\"There are no planned interviews.\"\n        description={<AlertDescription />}\n      />\n    </Col>\n  </Row>\n);\n"
  },
  {
    "path": "client/src/modules/Interview/Student/components/StatusLabel.tsx",
    "content": "import { Tag } from 'antd';\nimport { InterviewStatus } from '@client/domain/interview';\n\nexport const StatusLabel = ({ status }: { status: InterviewStatus }) => {\n  const statusMap = {\n    [InterviewStatus.Completed]: { color: 'green', label: 'Completed' },\n    [InterviewStatus.Canceled]: { color: 'red', label: 'Canceled' },\n    [InterviewStatus.NotCompleted]: { color: 'orange', label: 'Not Completed' },\n  };\n  const { color, label } = statusMap[status] || statusMap[InterviewStatus.NotCompleted];\n\n  return <Tag color={color}>{label}</Tag>;\n};\n"
  },
  {
    "path": "client/src/modules/Interview/Student/data/getInterviewCardDetails.tsx",
    "content": "import { InterviewResult } from '@client/domain/interview';\nimport { formatShortDate } from '@client/services/formatter';\n\ninterface StudentInterviewDetails {\n  registrationNotStarted: boolean;\n  isRegistered: boolean;\n  interviewPassed: boolean;\n  registrationStart: string;\n  interviewResult: InterviewResult;\n  hasInterviewPair: boolean;\n}\n\nexport const getInterviewCardDetails = ({\n  interviewResult,\n  interviewPassed,\n  isRegistered,\n  registrationNotStarted,\n  registrationStart,\n  hasInterviewPair,\n}: StudentInterviewDetails) => {\n  if (interviewPassed) {\n    switch (interviewResult) {\n      case InterviewResult.Yes:\n        return {\n          cardMessage: 'You have your interview result. Congratulations!',\n          backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/congratulations.svg)',\n        };\n      case InterviewResult.No:\n        return {\n          cardMessage: 'Your interview result is ready. Mistakes are proof that you are trying. Stay positive!',\n          backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/train.svg)',\n        };\n      case InterviewResult.Draft:\n        return {\n          cardMessage: `Your interview is complete. The mentor hasn't provided feedback yet. Please check back later.`,\n          backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/mentor-new.svg)',\n        };\n    }\n  }\n\n  if (isRegistered) {\n    const message = hasInterviewPair\n      ? 'Contact your interviewer to schedule the interview as soon as possible!'\n      : 'You’re all set! Prepare for your upcoming interview.';\n    const image = hasInterviewPair\n      ? 'url(https://cdn.rs.school/sloths/stickers/interview-with-mentor/image.svg)'\n      : 'url(https://cdn.rs.school/sloths/cleaned/its-a-good-job.svg)';\n    return {\n      cardMessage: message,\n      backgroundImage: image,\n    };\n  }\n\n  if (registrationNotStarted) {\n    return {\n      cardMessage: (\n        <div>\n          Remember to come back and register after{' '}\n          <span style={{ whiteSpace: 'nowrap' }}>{formatShortDate(registrationStart ?? '')}</span>!\n        </div>\n      ),\n      backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/listening.svg)',\n    };\n  }\n\n  return {\n    cardMessage: 'Register and get ready for your exciting interview!',\n    backgroundImage: 'url(https://cdn.rs.school/sloths/cleaned/take-notes.svg)',\n  };\n};\n"
  },
  {
    "path": "client/src/modules/Interview/Student/index.ts",
    "content": "export { NoInterviewsAlert } from './components/NoInterviewsAlert';\nexport { InterviewCard } from './components/InterviewCard';\n"
  },
  {
    "path": "client/src/modules/Interviews/data/getInterviewData.ts",
    "content": "import { CoursesInterviewsApi } from '@client/api';\nimport { templates } from '@client/data/interviews';\nimport { ParsedUrlQuery } from 'querystring';\nimport type { CourseOnlyPageProps } from '@client/services/models';\n\nexport type FeedbackProps = CourseOnlyPageProps & {\n  interviewTaskId: number;\n  type: keyof typeof templates;\n  githubId: string;\n};\n\nconst coursesInterviewsApi = new CoursesInterviewsApi();\n\n/**\n * Gets regular interview data\n */\nexport async function getInterviewData({\n  query,\n  courseId,\n}: {\n  query: ParsedUrlQuery;\n  courseId: number;\n}): Promise<Omit<FeedbackProps, keyof CourseOnlyPageProps>> {\n  const githubId = query.githubId as string;\n  const type = query.type as FeedbackProps['type'];\n\n  const response = await coursesInterviewsApi.getInterviews(courseId, false);\n  const interview =\n    response.data.find(interview => (interview.attributes as { template?: string })?.template === type) ?? null;\n\n  if (interview == null) {\n    throw new Error('Interview not found');\n  }\n\n  return {\n    interviewTaskId: interview.id,\n    type,\n    githubId,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/data/getStageInterviewData.ts",
    "content": "import {\n  CoursesInterviewsApi,\n  CoursesTasksApi,\n  CourseStatsApi,\n  InterviewFeedbackDto,\n  StudentDto,\n  StudentsApi,\n  TaskDtoTypeEnum,\n} from '@client/api';\nimport { getTasksTotalScore } from '@client/domain/course';\nimport { ParsedUrlQuery } from 'querystring';\nimport type { CourseOnlyPageProps } from '@client/services/models';\n\nexport type StageFeedbackProps = CourseOnlyPageProps & {\n  interviewId: number;\n  student: StudentDto;\n  courseSummary: {\n    totalScore: number;\n    studentsCount: number;\n  };\n  interviewFeedback: InterviewFeedbackDto;\n  type: typeof TaskDtoTypeEnum.StageInterview;\n};\n\n/**\n * Gets stage interview data\n */\nexport async function getStageInterviewData({\n  courseId,\n  query,\n}: {\n  query: ParsedUrlQuery;\n  courseId: number;\n}): Promise<Omit<StageFeedbackProps, keyof CourseOnlyPageProps>> {\n  validateQueryParams(query, ['studentId', 'interviewId']);\n\n  const studentId = Number(query.studentId);\n  const interviewId = Number(query.interviewId);\n\n  const [\n    { data: student },\n    { data: tasks },\n    {\n      data: { activeStudentsCount },\n    },\n    { data: interviewFeedback },\n  ] = await Promise.all([\n    new StudentsApi().getStudent(Number(studentId)),\n    new CoursesTasksApi().getCourseTasks(courseId),\n    new CourseStatsApi().getCourseStats(courseId),\n    new CoursesInterviewsApi().getInterviewFeedback(courseId, interviewId, TaskDtoTypeEnum.StageInterview),\n  ]);\n  if (!student) {\n    throw new Error('Student not found');\n  }\n\n  return {\n    interviewId,\n    student,\n    courseSummary: {\n      totalScore: getTasksTotalScore(tasks),\n      studentsCount: activeStudentsCount,\n    },\n    interviewFeedback,\n    type: TaskDtoTypeEnum.StageInterview,\n  };\n}\n\nfunction validateQueryParams(query: ParsedUrlQuery, params: string[]) {\n  for (const param of params) {\n    if (!query[param]) {\n      throw new Error(`Parameter ${param} is not defined`);\n    }\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/data/index.ts",
    "content": "import { FeedbackProps } from './getInterviewData';\nimport { StageFeedbackProps } from './getStageInterviewData';\n\nexport { getInterviewData, type FeedbackProps } from './getInterviewData';\nexport { getStageInterviewData, type StageFeedbackProps } from './getStageInterviewData';\n\nexport type PageProps = FeedbackProps | StageFeedbackProps;\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/InterviewFeedback/index.tsx",
    "content": "import { Button, Checkbox, Form, Input, Space, Typography } from 'antd';\nimport { AxiosError } from 'axios';\nimport { CommentInput } from '@client/shared/components/Forms';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { InputType, templates } from '@client/data/interviews';\nimport { Fragment, useMemo, useState } from 'react';\nimport { CourseService } from '@client/services/course';\nimport { FeedbackProps } from '../../data/getInterviewData';\nimport { ScoreSelector } from '@client/shared/components/ScoreSelector';\nimport { useRouter } from 'next/router';\nimport { useMessage } from '@client/hooks';\n\ntype FormAnswer = {\n  questionId: string;\n  questionText: string;\n  answer: string;\n};\n\nexport function InterviewFeedback({ course, type, interviewTaskId, githubId }: FeedbackProps) {\n  const courseId = course.id;\n\n  const template = templates[type];\n\n  const [form] = Form.useForm();\n  const router = useRouter();\n\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const [loading, setLoading] = useState(false);\n\n  const { message } = useMessage();\n\n  const questions = useMemo(() => template.categories.flatMap(c => c.questions), [type]);\n\n  const handleSubmit = async (values: any) => {\n    if (!githubId || loading) {\n      return;\n    }\n    try {\n      setLoading(true);\n      const formAnswers: FormAnswer[] = questions.map(({ id, name }) => ({\n        answer: values[id],\n        questionId: id.toString(),\n        questionText: name,\n      }));\n      const score = Number(values.score);\n      const body = { formAnswers, score, comment: values.comment || '' };\n      await courseService.postStudentInterviewResult(githubId, interviewTaskId, body);\n      message.success('You interview feedback has been submitted. Thank you.');\n      form.resetFields();\n    } catch (e) {\n      const error = e as AxiosError<any>;\n      const response = error.response;\n      const errorMessage = response?.data?.data?.message ?? 'An error occurred. Please try later.';\n      message.error(errorMessage);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <PageLayoutSimple loading={loading} title={`${template.name}: Interview Feedback`} showCourseName>\n      <Typography style={{ marginBottom: 24 }}>\n        <h4>Process</h4>\n        <div>\n          <a target=\"_blank\" href={template.examplesUrl}>\n            Sample interview questions ({template.examplesUrl})\n          </a>\n        </div>\n        <div>1) Ask a question </div>\n        <div>2) Listen for the answer </div>\n        <div>3) Complete or correct the answer if needed</div>\n        <div>4) Ask the next question</div>\n      </Typography>\n\n      <Typography>\n        <div style={{ marginBottom: 8 }} dangerouslySetInnerHTML={{ __html: template.descriptionHtml ?? '' }} />\n      </Typography>\n\n      <Form\n        form={form}\n        layout=\"vertical\"\n        onFinish={handleSubmit}\n        onFinishFailed={({ errorFields: [errorField] }) => errorField && form.scrollToField(errorField.name)}\n      >\n        <Space align=\"baseline\">\n          <Typography.Title level={4}>Student: </Typography.Title>{' '}\n          <GithubAvatar githubId={githubId ?? undefined} size={24} />\n          <Typography.Link target=\"_blank\" href={`/profile?githubId=${githubId}`}>\n            <Typography.Title level={4}>\n              <Typography.Link>{githubId}</Typography.Link>\n            </Typography.Title>\n          </Typography.Link>\n        </Space>\n\n        {template.categories.map(category => (\n          <Fragment key={category.id}>\n            <Typography.Title level={4}>\n              {category.name}\n              {category.description ? <Typography.Title level={5}>{category.description}</Typography.Title> : null}\n            </Typography.Title>\n            {category.questions.map(question => {\n              switch (question.type) {\n                case InputType.Input:\n                  return (\n                    <Form.Item label={question.name} name={question.id} key={question.id}>\n                      <Input.TextArea rows={4} />\n                    </Form.Item>\n                  );\n                default:\n                  return (\n                    <Form.Item\n                      style={{ marginBottom: 16 }}\n                      name={question.id}\n                      key={question.id}\n                      valuePropName=\"checked\"\n                    >\n                      <Checkbox>{question.name}</Checkbox>\n                    </Form.Item>\n                  );\n              }\n            })}\n          </Fragment>\n        ))}\n        <Typography.Title level={4}>Total Score</Typography.Title>\n        <Form.Item name=\"score\" label=\"Score\" rules={[{ required: true, message: 'Please select a Score' }]}>\n          <ScoreSelector />\n        </Form.Item>\n        <Typography.Title level={4}>Comment</Typography.Title>\n        <CommentInput />\n        <Button size=\"large\" type=\"primary\" htmlType=\"submit\">\n          Submit\n        </Button>\n        <Button\n          type=\"default\"\n          size=\"large\"\n          onClick={() => router.back()}\n          style={{ float: 'right', marginBottom: '20px' }}\n        >\n          Back\n        </Button>\n      </Form>\n    </PageLayoutSimple>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/CustomQuestion.tsx",
    "content": "import { useRef } from 'react';\nimport { Button, Card, Col, Input, InputRef, Row, Space } from 'antd';\n\ntype Props = {\n  cancel: () => void;\n  save(question: string): void;\n};\n\nexport function CustomQuestion({ cancel, save }: Props) {\n  const addRef = useRef<InputRef>(null);\n\n  function saveQuestion() {\n    const value = addRef.current?.input?.value.trim();\n    if (!value) {\n      return;\n    }\n\n    save(value);\n  }\n\n  return (\n    <Card bodyStyle={{ padding: '12px 24px' }} style={{ marginBottom: 16 }}>\n      <Input\n        placeholder=\"Enter your question\"\n        ref={addRef}\n        autoFocus\n        onPressEnter={e => {\n          // prevent form submit\n          e.preventDefault();\n          saveQuestion();\n        }}\n      />\n      <Row style={{ marginTop: 15 }}>\n        <Col flex={1} />\n        <Space>\n          <Button onClick={cancel}>Cancel</Button>\n          <Button type=\"primary\" onClick={saveQuestion}>\n            Save\n          </Button>\n        </Space>\n      </Row>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/FormItem.tsx",
    "content": "import { Form, FormInstance, Input, Radio, Space, Typography, Checkbox } from 'antd';\nimport { StepFormItem, RadioOption, FeedbackStepId } from '@client/data/interviews/technical-screening';\nimport { NestedRadio } from './NestedRadio';\nimport { InputType } from '@client/data/interviews';\nimport { Fragment } from 'react';\nimport { QuestionList } from './QuestionList';\n\nconst { Item } = Form;\nconst { Group } = Radio;\nconst { Text } = Typography;\n\nexport function FormItem({ item, form, stepId }: { item: StepFormItem; form: FormInstance; stepId: FeedbackStepId }) {\n  switch (item.type) {\n    case InputType.Radio:\n      return (\n        <Item name={item.id} rules={[{ required: item.required, message: 'Required' }]}>\n          <Group>\n            <Space direction=\"vertical\">\n              {item.options.map((option: RadioOption) => {\n                return (\n                  <Fragment key={option.id}>\n                    <Radio key={option.id} value={option.id}>\n                      {option.title}\n                    </Radio>\n                    <NestedRadio form={form} option={option} parentId={item.id} stepId={stepId} />\n                  </Fragment>\n                );\n              })}\n            </Space>\n          </Group>\n        </Item>\n      );\n    case InputType.RadioButton:\n      return (\n        <Item name={item.id} rules={[{ required: item.required, message: 'Required' }]}>\n          <Group buttonStyle=\"solid\">\n            {item.description && (\n              <Text type=\"secondary\">\n                <div>{item.description}</div>\n              </Text>\n            )}\n            {item.options.map((option: RadioOption) => {\n              return (\n                <Radio.Button key={option.id} value={option.id}>\n                  {option.title}\n                </Radio.Button>\n              );\n            })}\n          </Group>\n        </Item>\n      );\n    case InputType.Checkbox:\n      return (\n        <Item name={item.id} rules={[{ required: item.required, message: 'Required' }]}>\n          <Checkbox.Group\n            options={item.options.map((option: RadioOption) => ({ label: option.title, value: option.id }))}\n          />\n        </Item>\n      );\n    case InputType.Input:\n      return (\n        <Item name={item.id} rules={[{ required: item.required, message: 'Required' }]}>\n          <Input\n            placeholder={item.placeholder}\n            type={item.inputType}\n            min={item.min}\n            max={item.max}\n            style={{ width: item.inputType === 'number' ? 100 : undefined }}\n          />\n        </Item>\n      );\n    case InputType.TextArea:\n      return (\n        <Item name={item.id} rules={[{ required: item.required, message: 'Required' }]}>\n          <Input.TextArea placeholder={item.placeholder} />\n        </Item>\n      );\n    case InputType.Rating: {\n      return <QuestionList form={form} question={item} stepId={stepId} />;\n    }\n    default:\n      return null;\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/NestedRadio.tsx",
    "content": "import { Radio, Space, FormInstance, Form } from 'antd';\nimport { RadioOption } from '@client/data/interviews/technical-screening';\nimport { useEffect } from 'react';\n\nconst { Item, useWatch } = Form;\nconst { Group } = Radio;\n\n/**\n * handles dynamic display if parent is selected and automatic cleanup\n */\nexport function NestedRadio({\n  form,\n  option,\n  parentId,\n}: {\n  form: FormInstance;\n  option: RadioOption;\n  parentId: string;\n  stepId: string;\n}) {\n  const parentValue = useWatch(parentId);\n\n  useEffect(() => {\n    //reset current value in form, if parent value changes\n    if (parentValue && parentValue !== option.id) {\n      form.resetFields([option.id]);\n    }\n  }, [parentValue, option.id]);\n\n  if (!option.options) {\n    return null;\n  }\n\n  return (\n    <Item shouldUpdate noStyle>\n      {() =>\n        form.getFieldValue(parentId) === option.id && (\n          <Item name={option.id} rules={[{ required: true, message: 'Required' }]}>\n            <Group>\n              <Space direction=\"vertical\" style={{ marginLeft: '24px' }}>\n                {option.options?.map((subOption: any) => (\n                  <Radio key={subOption.id} value={subOption.id}>\n                    {subOption.title}\n                  </Radio>\n                ))}\n              </Space>\n            </Group>\n          </Item>\n        )\n      }\n    </Item>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/QuestionCard.tsx",
    "content": "import { Card, Col, Form, Rate, Row } from 'antd';\nimport { ReactNode } from 'react';\n\ntype Props = {\n  content: ReactNode;\n  fieldName: string[];\n  required?: boolean;\n  tooltips?: string[];\n};\n\n/**\n * Question requiring a rate answer\n */\nexport function QuestionCard({ content, fieldName, required, tooltips }: Props) {\n  return (\n    <Card bodyStyle={{ padding: '12px 24px' }} style={{ flex: 1 }}>\n      <Row align=\"middle\" wrap={false}>\n        <Col flex={1} style={{ flexWrap: 'wrap' }}>\n          {content}\n        </Col>\n        <Col style={{ flexShrink: 0, marginLeft: '10px' }}>\n          <Form.Item name={fieldName} rules={[{ required, message: 'Required' }]}>\n            <Rate tooltips={tooltips} />\n          </Form.Item>\n        </Col>\n      </Row>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/QuestionList.tsx",
    "content": "import { Form, Space, Button, Row, Col, Typography } from 'antd';\nimport DeleteOutlined from '@ant-design/icons/DeleteOutlined';\nimport PlusOutlined from '@ant-design/icons/PlusOutlined';\nimport { QuestionCard } from './QuestionCard';\nimport { QuestionsPicker } from './QuestionsPicker';\nimport { useMemo, useState } from 'react';\nimport { QuestionItem, FeedbackStepId } from '@client/data/interviews/technical-screening';\nimport { FormInstance } from 'antd/lib';\nimport { CustomQuestion } from './CustomQuestion';\nimport { InterviewQuestion } from '@common/models';\n\nconst { Text } = Typography;\n\ntype Props = {\n  question: QuestionItem;\n  form: FormInstance;\n  stepId: FeedbackStepId;\n};\n\nexport function QuestionList({ form, question, stepId }: Props) {\n  const { examples = [], id, required, tooltips } = question;\n  const [addModeActive, setAddModeActive] = useState<null | 'custom' | 'prepared'>(null);\n\n  const formQuestions = Form.useWatch<InterviewQuestion[] | undefined>(question.id, { form });\n\n  // filter out already added questions\n  const pickerQuestions = useMemo(() => {\n    return examples.filter(\n      (question: InterviewQuestion) => formQuestions?.some(({ id }) => id === question.id) !== true,\n    );\n  }, [formQuestions]);\n\n  return (\n    <>\n      <Form.List name={question.id}>\n        {(fields, { add, remove }) => (\n          <>\n            {fields.map(({ name }) => {\n              const question: InterviewQuestion = form.getFieldValue(id)[name];\n\n              return (\n                <Row key={question.id} align={'middle'} style={{ marginBottom: 16 }}>\n                  <QuestionCard\n                    fieldName={[`${name}`, 'value']}\n                    required={required}\n                    tooltips={tooltips}\n                    content={\n                      <>\n                        {question.topic && (\n                          <Row>\n                            <Text type=\"secondary\">{question.topic}</Text>\n                          </Row>\n                        )}\n                        <Text>{question.title}</Text>\n                      </>\n                    }\n                  />\n                  {fields.length > 1 && (\n                    <Col style={{ paddingLeft: 20 }}>\n                      <DeleteOutlined onClick={() => remove(name)} />\n                    </Col>\n                  )}\n                </Row>\n              );\n            })}\n\n            {addModeActive === 'prepared' && (\n              <QuestionsPicker\n                questions={pickerQuestions}\n                onSave={questions => onAddQuestion(questions, add)}\n                onCancel={() => setAddModeActive(null)}\n              />\n            )}\n\n            {addModeActive === 'custom' && (\n              <CustomQuestion cancel={() => setAddModeActive(null)} save={question => onAddQuestion([question], add)} />\n            )}\n\n            <Space style={{ margin: '30px 0' }}>\n              {pickerQuestions.length > 0 && (\n                <Button ghost type=\"primary\" onClick={() => setAddModeActive('prepared')} icon={<PlusOutlined />}>\n                  Add from list\n                </Button>\n              )}\n              <Button ghost type=\"primary\" onClick={() => setAddModeActive('custom')} icon={<PlusOutlined />}>\n                Custom {stepId === FeedbackStepId.Practice ? 'task' : 'question'}\n              </Button>\n            </Space>\n          </>\n        )}\n      </Form.List>\n    </>\n  );\n\n  function onAddQuestion(toAdd: string[], add: (question: InterviewQuestion) => void) {\n    if (addModeActive === 'prepared') {\n      examples.filter(({ id }) => toAdd.includes(id)).forEach(question => add(question));\n    } else {\n      toAdd.forEach(question => add({ id: generateId(), title: question }));\n    }\n    setAddModeActive(null);\n  }\n}\n\nfunction generateId() {\n  const randomNum = Math.random();\n  const id = randomNum.toString(36).substring(2, 11); // Generate a string of 9 characters\n  return id;\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/QuestionsPicker.tsx",
    "content": "import { Checkbox, Form, Modal, Row } from 'antd';\nimport { InterviewQuestion } from '@common/models';\n\nconst { Item } = Form;\n\nexport function QuestionsPicker({\n  questions,\n  onCancel,\n  onSave,\n}: {\n  questions: InterviewQuestion[];\n  onCancel: () => void;\n  onSave: (questions: string[]) => void;\n}) {\n  const [form] = Form.useForm();\n\n  function onFinish(values: { questions: string[] }) {\n    onSave(values.questions);\n  }\n\n  return (\n    <Modal\n      open\n      title=\"Add question\"\n      okButtonProps={{ htmlType: 'submit' }}\n      onOk={form.submit}\n      okText=\"Add\"\n      onCancel={onCancel}\n    >\n      <Form form={form} style={{ padding: 40 }} onFinish={onFinish}>\n        <Item\n          name={'questions'}\n          rules={[{ required: true, message: 'Please select questions to add to the feedback' }]}\n        >\n          <Checkbox.Group style={{ flexDirection: 'column' }}>\n            {questions.map(question => (\n              <Row key={question.id}>\n                <Checkbox value={question.id}>{question.title}</Checkbox>\n              </Row>\n            ))}\n          </Checkbox.Group>\n        </Item>\n      </Form>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/StageInterviewFeedback.tsx",
    "content": "import { Divider, Layout } from 'antd';\nimport dynamic from 'next/dynamic';\nimport { Header } from '@client/shared/components/Header';\n\nimport { Steps } from './Steps';\nimport { StudentInfo } from './StudentInfo';\nimport { SubHeader } from './SubHeader';\nimport { StepContextProvider } from './StepContext';\nimport { StepsContent } from './StepsContent';\nimport { featureToggles } from '@client/services/features';\nimport { StageFeedbackProps } from '../../data';\n\nconst LegacyTechScreening = dynamic(() => import('@client/pages/course/mentor/interview-technical-screening'), {\n  loading: () => <p>Loading...</p>,\n});\n\nexport function StageInterviewFeedback(props: StageFeedbackProps) {\n  const { student, courseSummary, interviewFeedback, course, interviewId, type } = props;\n\n  const shouldFallbackToLegacy = !featureToggles.feedback || interviewFeedback.version === 0;\n\n  // if the feedback exists and doesn't have a version, it means it was created before the feedback feature was released\n  // fallback to previous form. Once we migrate old data to new format(Artsiom A.), we may remove this fallback\n  if (shouldFallbackToLegacy) {\n    return <LegacyTechScreening />;\n  }\n\n  return (\n    <Layout style={{ background: 'transparent', minHeight: '100vh' }}>\n      <Header title=\"Technical screening\" showCourseName />\n      <SubHeader isCompleted={interviewFeedback.isCompleted ?? false} />\n      <StepContextProvider\n        interviewFeedback={interviewFeedback}\n        course={course}\n        interviewId={interviewId}\n        type={type}\n        interviewMaxScore={interviewFeedback.maxScore}\n      >\n        <Layout style={{ background: 'transparent' }}>\n          <Layout.Content>\n            <StepsContent />\n          </Layout.Content>\n          <Layout.Sider\n            reverseArrow\n            theme=\"light\"\n            width={400}\n            style={{ borderLeft: '1px solid rgba(240, 242, 245)' }}\n            breakpoint=\"md\"\n            collapsedWidth={0}\n          >\n            <StudentInfo student={student} courseSummary={courseSummary} />\n            <Divider style={{ margin: 0 }} />\n            <Steps />\n          </Layout.Sider>\n        </Layout>\n      </StepContextProvider>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/StepContext.tsx",
    "content": "import { CoursesInterviewsApi, InterviewFeedbackDto, ProfileCourseDto } from '@client/api';\nimport { FeedbackStep, Feedback } from '@client/data/interviews/technical-screening';\nimport { createContext, PropsWithChildren, useCallback, useMemo, useState } from 'react';\nimport { useLoading } from '@client/components/useLoading';\nimport { message } from 'antd';\nimport { useRouter } from 'next/router';\nimport {\n  getDefaultStep,\n  getFeedbackFromTemplate,\n  getUpdatedFeedback,\n  isInterviewCanceled,\n} from './feedbackTemplateHandler';\nimport { InterviewFeedbackValues } from '@common/models';\n\ntype ContextProps = {\n  course: ProfileCourseDto;\n  interviewId: number;\n  interviewFeedback: InterviewFeedbackDto;\n  type: string;\n  interviewMaxScore: number;\n};\n\ntype StepApi = {\n  activeStepIndex: number;\n  steps: FeedbackStep[];\n  next: (values: InterviewFeedbackValues) => void;\n  prev: () => void;\n  onValuesChange(_: Partial<InterviewFeedbackValues>, values: InterviewFeedbackValues): void;\n  loading: boolean;\n  isFinalStep: boolean;\n};\n\nexport const StepContext = createContext<StepApi>({} as StepApi);\n\nexport function StepContextProvider(props: PropsWithChildren<ContextProps>) {\n  const { interviewFeedback, children, course, interviewId, type, interviewMaxScore } = props;\n  const router = useRouter();\n  const [loading, withLoading] = useLoading(false, error => {\n    message.error('An unexpected error occurred. Please try later.');\n    throw error;\n  });\n\n  const [feedback, setFeedback] = useState<Feedback>(() =>\n    getFeedbackFromTemplate(interviewFeedback, interviewMaxScore),\n  );\n  const [activeStepIndex, setActiveIndex] = useState(() => getDefaultStep(feedback));\n  const activeStep = feedback.steps[activeStepIndex];\n\n  const [isFinished, setIsFinished] = useState(() =>\n    activeStep ? isInterviewCanceled(activeStep.id, activeStep.values) : false,\n  );\n  const isFinalStep = activeStepIndex === feedback.steps.length - 1 || isFinished;\n\n  const saveFeedback = withLoading(async (values: InterviewFeedbackValues) => {\n    const { feedbackValues, steps, isCompleted, score, decision, isGoodCandidate } = getUpdatedFeedback({\n      feedback,\n      newValues: values,\n      activeStepIndex,\n      interviewMaxScore,\n    });\n    await new CoursesInterviewsApi().createInterviewFeedback(course.id, interviewId, type, {\n      isCompleted,\n      score,\n      decision,\n      isGoodCandidate,\n      json: feedbackValues,\n      version: feedback.version,\n    });\n\n    setFeedback({\n      isCompleted,\n      steps,\n      version: feedback.version,\n    });\n  });\n\n  const onValuesChange = useCallback(\n    (_: Partial<InterviewFeedbackValues>, values: InterviewFeedbackValues) => {\n      if (activeStep) {\n        setIsFinished(isInterviewCanceled(activeStep.id, values));\n      }\n    },\n    [activeStep?.id],\n  );\n\n  const next = useCallback(\n    async (values: InterviewFeedbackValues) => {\n      try {\n        await saveFeedback(values);\n      } catch {\n        return;\n      }\n      if (isFinalStep) {\n        router.push(`/course/mentor/interviews?course=${course.alias}`);\n        return;\n      }\n\n      setActiveIndex(index => {\n        if (index === feedback.steps.length - 1) {\n          return index;\n        }\n\n        return index + 1;\n      });\n    },\n    [feedback.steps, isFinalStep, activeStepIndex],\n  );\n\n  const prev = useCallback(() => {\n    setActiveIndex(index => {\n      if (index === 0) {\n        return index;\n      }\n\n      return index - 1;\n    });\n  }, []);\n\n  const api = useMemo(\n    () => ({\n      activeStepIndex,\n      steps: feedback.steps,\n      next,\n      prev,\n      onValuesChange,\n      loading,\n      isFinalStep,\n    }),\n    [activeStepIndex, feedback.steps, isFinalStep, loading, onValuesChange],\n  );\n\n  return <StepContext.Provider value={api}>{children}</StepContext.Provider>;\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/StepForm.tsx",
    "content": "import { Form, Space, Typography, Button } from 'antd';\nimport { FeedbackStep, StepFormItem } from '@client/data/interviews/technical-screening';\nimport { FormItem } from './FormItem';\nimport { InputType } from '@client/data/interviews';\nimport { InterviewFeedbackValues } from '@common/models';\n\nconst { Title, Text } = Typography;\n\ntype Values = Record<string, string>;\n\ntype Props = {\n  step: FeedbackStep;\n  back: () => void;\n  next: (values: Values) => void;\n  onValuesChange: (changedValues: Partial<Values>, values: Values) => void;\n  isLast: boolean;\n  isFirst: boolean;\n};\n\nexport function StepForm({ step, next, back, isFirst, isLast, onValuesChange }: Props) {\n  const [form] = Form.useForm();\n\n  return (\n    <Form\n      form={form}\n      style={{ padding: 40 }}\n      onFinish={next}\n      onValuesChange={onValuesChange}\n      initialValues={getInitialQuestions(step)}\n      onFinishFailed={({ errorFields: [errorField] }) => errorField && form.scrollToField(errorField.name)}\n    >\n      <Space direction=\"vertical\" style={{ width: '100%' }}>\n        <Form.Item>\n          <Title level={3}>{step.title}</Title>\n          <Text type=\"secondary\">{step.description}</Text>\n        </Form.Item>\n        {step.items.map((item: StepFormItem) => (\n          <div key={item.id}>\n            <Title level={5}>{item.title}</Title>\n            <FormItem form={form} item={item} stepId={step.id} />\n          </div>\n        ))}\n\n        <Space style={{ justifyContent: 'space-between', display: 'flex' }}>\n          {!isFirst ? <Button onClick={back}>Back</Button> : <div />}\n          {<Button htmlType=\"submit\">{isLast ? 'Submit' : 'Next'}</Button>}\n        </Space>\n      </Space>\n    </Form>\n  );\n}\n\nfunction getInitialQuestions(step: FeedbackStep) {\n  const { items, values } = step;\n\n  if (values) {\n    return values;\n  }\n\n  // if values are not yet defined(ie feedback is not yet submitted), initialize dynamic questions with default structure\n  return items.reduce((acc: InterviewFeedbackValues, item) => {\n    if (item.type === InputType.Rating) {\n      acc[item.id] = item.questions;\n    }\n    if (item.type === InputType.Input && item.defaultValue) {\n      acc[item.id] = item.defaultValue;\n    }\n    return acc;\n  }, {});\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/Steps.tsx",
    "content": "import { Steps as Stepper } from 'antd';\nimport { useContext } from 'react';\nimport { StepContext } from './StepContext';\n\nexport function Steps() {\n  const { activeStepIndex, steps } = useContext(StepContext);\n\n  return (\n    <Stepper\n      direction=\"vertical\"\n      current={activeStepIndex}\n      size={'small'}\n      style={{ padding: 24 }}\n      items={steps.map((step, index) => ({\n        title: step.title,\n        description: step.stepperDescription,\n        status: getStatus(index),\n      }))}\n    />\n  );\n\n  function getStatus(index: number) {\n    if (index === activeStepIndex) {\n      return 'process';\n    }\n    return steps[index]?.isCompleted ? 'finish' : 'wait';\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/StepsContent.tsx",
    "content": "import { useContext } from 'react';\nimport { StepContext } from './StepContext';\nimport { StepForm } from './StepForm';\nimport { Spin } from 'antd';\n\nexport function StepsContent() {\n  const { activeStepIndex, steps, next, prev, onValuesChange, loading, isFinalStep } = useContext(StepContext);\n  const step = steps[activeStepIndex];\n\n  if (!step) {\n    return <div>Step not found</div>;\n  }\n\n  return (\n    <Spin spinning={loading}>\n      <StepForm\n        step={step}\n        back={prev}\n        isFirst={activeStepIndex === 0}\n        isLast={isFinalStep}\n        next={next}\n        onValuesChange={onValuesChange}\n        key={step.id}\n      />\n    </Spin>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/StudentInfo.tsx",
    "content": "import { Col, Row, Typography } from 'antd';\nimport GithubFilled from '@ant-design/icons/GithubFilled';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { StudentDto } from '@client/api';\n\ntype Props = {\n  student: StudentDto;\n  courseSummary: {\n    totalScore: number;\n    studentsCount: number;\n  };\n};\n\nconst { Text } = Typography;\n\nexport function StudentInfo(props: Props) {\n  const { student, courseSummary } = props;\n  const { githubId, name, rank, totalScore } = student;\n  const hasName = name && name !== '(Empty)';\n  const location = [student.cityName, student.countryName].filter(Boolean).join(', ');\n\n  return (\n    <Col style={{ padding: 24 }}>\n      <Row align=\"middle\" gutter={24}>\n        <Col>\n          <GithubAvatar githubId={githubId} size={48} />\n        </Col>\n        <Col>\n          {hasName && (\n            <Row>\n              <Typography.Title level={5}>{name}</Typography.Title>\n            </Row>\n          )}\n          <Row>\n            <Typography.Link target=\"_blank\" href={`https://github.com/${githubId}`}>\n              <GithubFilled /> {githubId}\n            </Typography.Link>\n          </Row>\n        </Col>\n      </Row>\n      <Row justify=\"space-between\" style={{ marginTop: 32 }}>\n        <Col>\n          <Row>\n            <Text type=\"secondary\">Position</Text>\n          </Row>\n          <Row>\n            <Text>{`${rank}/${courseSummary.studentsCount}`}</Text>\n          </Row>\n        </Col>\n        <Col>\n          <Row>\n            <Text type=\"secondary\">Total Score</Text>\n          </Row>\n          <Row>\n            {' '}\n            <Text>{`${totalScore}/${courseSummary.totalScore}`}</Text>\n          </Row>\n        </Col>\n        <Col>\n          <Row>\n            <Text type=\"secondary\">Location</Text>\n          </Row>\n          <Row>\n            <Text>{location}</Text>\n          </Row>\n        </Col>\n      </Row>\n    </Col>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/SubHeader.module.css",
    "content": ".container :global(div) {\n  border-bottom: 1px solid rgba(240, 242, 245, 1);\n  padding: 24px;\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/SubHeader.tsx",
    "content": "import { Col, Space, Tag, Typography } from 'antd';\nimport ArrowLeftOutlined from '@ant-design/icons/ArrowLeftOutlined';\nimport { useRouter } from 'next/router';\nimport styles from './SubHeader.module.css';\n\ntype Props = {\n  isCompleted: boolean;\n};\n\nexport function SubHeader(props: Props) {\n  const { isCompleted } = props;\n  const router = useRouter();\n\n  return (\n    <Space align=\"center\" size=\"middle\" className={styles.container}>\n      <Col />\n      <ArrowLeftOutlined onClick={router.back} />\n      <Typography.Text strong>Feedback form</Typography.Text>\n      <Tag color={isCompleted ? 'green' : undefined}>{isCompleted ? 'Completed' : 'Uncompleted'}</Tag>\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/feedbackTemplateHandler.test.ts",
    "content": "import { InterviewFeedbackValues } from '@common/models/interview';\nimport {\n  getFeedbackFromTemplate,\n  getDefaultStep,\n  isInterviewCanceled,\n  getUpdatedFeedback,\n} from './feedbackTemplateHandler';\nimport { Decision, Feedback, FeedbackStepId, feedbackTemplate } from '@client/data/interviews/technical-screening';\n\ndescribe('getFeedbackFromTemplate', () => {\n  test('should return default template when no feedback exists', () => {\n    const interviewFeedback = { json: undefined, isCompleted: false, maxScore: 100 };\n\n    const feedback = getFeedbackFromTemplate(interviewFeedback, 100);\n\n    expect(feedback.steps.length).toBe(feedbackTemplate.steps.length);\n    expect(feedback.isCompleted).toBe(false);\n    expect(feedback.version).toBe(feedbackTemplate.version);\n  });\n\n  test('should merge feedback data with template', () => {\n    const interviewFeedback = {\n      json: {\n        steps: {\n          [FeedbackStepId.Introduction]: {\n            isCompleted: true,\n            values: { interviewResult: 'completed' },\n          },\n          [FeedbackStepId.Theory]: {\n            isCompleted: true,\n            values: { questions: [{ value: 3 }] },\n          },\n        },\n      },\n      version: 1,\n      isCompleted: true,\n      maxScore: 100,\n    };\n\n    const feedback = getFeedbackFromTemplate(interviewFeedback, 100);\n\n    expect(feedback.steps.length).toBe(feedbackTemplate.steps.length);\n    expect(feedback.isCompleted).toBe(true);\n    expect(feedback.version).toBe(1);\n\n    expect(feedback.steps[0]?.isCompleted).toBe(true);\n    expect(feedback.steps[0]?.values?.interviewResult).toBe('completed');\n    expect(feedback.steps[1]?.isCompleted).toBe(true);\n    expect(feedback.steps[1]?.values).toEqual({ questions: [{ value: 3 }], score: 30 });\n\n    expect(feedback.steps[2]?.isCompleted).toBe(undefined);\n  });\n});\n\ndescribe('getDefaultStep', () => {\n  test('should return the first incomplete step', () => {\n    const feedback = {\n      steps: [\n        { id: FeedbackStepId.Introduction, isCompleted: true },\n        { id: FeedbackStepId.Theory, isCompleted: false },\n        { id: FeedbackStepId.Practice, isCompleted: false },\n        { id: FeedbackStepId.Decision, isCompleted: false },\n      ],\n      version: 1,\n      isCompleted: false,\n    };\n    const defaultStep = getDefaultStep(feedback as Feedback);\n    expect(defaultStep).toBe(1);\n  });\n\n  test('should return the final step if all steps are completed', () => {\n    const feedback = {\n      steps: [\n        { id: FeedbackStepId.Introduction, isCompleted: true },\n        { id: FeedbackStepId.Theory, isCompleted: true },\n        { id: FeedbackStepId.Practice, isCompleted: true },\n        { id: FeedbackStepId.Decision, isCompleted: true },\n      ],\n      isCompleted: true,\n    };\n    const defaultStep = getDefaultStep(feedback as Feedback);\n    expect(defaultStep).toBe(3);\n  });\n\n  test('should return intro step if interview is not conducted', () => {\n    const feedback = {\n      steps: [\n        { id: FeedbackStepId.Introduction, isCompleted: true, values: { interviewResult: 'missed' } },\n        { id: FeedbackStepId.Theory, isCompleted: false },\n        { id: FeedbackStepId.Practice, isCompleted: false },\n        { id: FeedbackStepId.Decision, isCompleted: false },\n      ],\n      isCompleted: false,\n    };\n    const defaultStep = getDefaultStep(feedback as Feedback);\n    expect(defaultStep).toBe(0);\n  });\n});\n\ndescribe('isInterviewCanceled', () => {\n  test('should return true if interview is rejected on the intro step', () => {\n    const stepValues = { interviewResult: 'missed' };\n    const isRejected = isInterviewCanceled(FeedbackStepId.Introduction, stepValues);\n\n    expect(isRejected).toBe(true);\n  });\n\n  test('should return false if not rejected on intro step', () => {\n    const stepValues = { interviewResult: 'completed' };\n    const isRejected = isInterviewCanceled(FeedbackStepId.Introduction, stepValues);\n\n    expect(isRejected).toBe(false);\n  });\n\n  test('should return false if not a intro step', () => {\n    const stepValues = {};\n    const isRejected = isInterviewCanceled(FeedbackStepId.Theory, stepValues);\n\n    expect(isRejected).toBe(false);\n  });\n});\n\ndescribe('getUpdatedFeedback', () => {\n  test('should mark active index as completed and return new feedback', () => {\n    const feedback = {\n      version: 1,\n      isCompleted: false,\n      steps: [\n        { id: FeedbackStepId.Introduction, isCompleted: true, items: [], values: { interviewResult: 'completed' } },\n        { id: FeedbackStepId.Theory, isCompleted: false, items: [] },\n        { id: FeedbackStepId.Practice, isCompleted: false, items: [] },\n        { id: FeedbackStepId.Decision, isCompleted: false, items: [] },\n      ],\n    } as unknown as Feedback;\n    const newValues: InterviewFeedbackValues = { questions: [{ id: '1', title: 'test', value: 3 }] };\n    const activeStepIndex = 1;\n\n    const updatedFeedback = getUpdatedFeedback({ feedback, newValues, activeStepIndex, interviewMaxScore: 100 });\n\n    expect(updatedFeedback.steps[0]?.isCompleted).toBe(true);\n    expect(updatedFeedback.steps[1]?.isCompleted).toBe(true);\n    expect(updatedFeedback.steps[1]?.values?.questions).toEqual([{ id: '1', title: 'test', value: 3 }]);\n    expect(updatedFeedback.feedbackValues).toEqual({\n      steps: {\n        decision: {\n          isCompleted: false,\n          values: undefined,\n        },\n        intro: {\n          isCompleted: true,\n          values: { interviewResult: 'completed' },\n        },\n        practice: {\n          isCompleted: false,\n          values: undefined,\n        },\n        theory: {\n          isCompleted: true,\n          values: {\n            questions: [\n              {\n                id: '1',\n                title: 'test',\n                value: 3,\n              },\n            ],\n            score: 30,\n          },\n        },\n      },\n    });\n    expect(updatedFeedback.steps[2]?.isCompleted).toBe(false);\n    expect(updatedFeedback.steps[2]?.values).toBeUndefined();\n    expect(updatedFeedback.steps[3]?.isCompleted).toBe(false);\n    expect(updatedFeedback.steps[3]?.values).toBeUndefined();\n    expect(updatedFeedback.isCompleted).toBe(false);\n    expect(updatedFeedback.score).toBeUndefined();\n    expect(updatedFeedback.decision).toBeUndefined();\n    expect(updatedFeedback.isGoodCandidate).toBeUndefined();\n  });\n\n  test('should mark all steps as completed if interview is completed', () => {\n    const feedback = {\n      version: '1.0',\n      isCompleted: false,\n      steps: [\n        { id: FeedbackStepId.Introduction, isCompleted: true, items: [], values: { interviewResult: 'completed' } },\n        { id: FeedbackStepId.Theory, isCompleted: true, items: [], values: { questions: [{ value: 3 }] } },\n        { id: FeedbackStepId.Practice, isCompleted: true, items: [], values: { questions: [{ value: 4 }] } },\n        { id: FeedbackStepId.Decision, isCompleted: false, items: [] },\n      ],\n    } as unknown as Feedback;\n    const newValues = { decision: Decision.Yes, isGoodCandidate: ['true'], finalScore: 8 };\n    const activeStepIndex = 3;\n    const interviewMaxScore = 10;\n    const updatedFeedback = getUpdatedFeedback({ feedback, newValues, activeStepIndex, interviewMaxScore });\n\n    expect(updatedFeedback.steps.every(step => step.isCompleted)).toBe(true);\n    expect(updatedFeedback.decision).toBe(Decision.Yes);\n    expect(updatedFeedback.isGoodCandidate).toBeTruthy();\n    expect(updatedFeedback.isCompleted).toBeTruthy();\n    expect(updatedFeedback.score).toBe(8);\n    expect(updatedFeedback.feedbackValues).toEqual({\n      steps: {\n        decision: {\n          isCompleted: true,\n          values: {\n            decision: Decision.Yes,\n            isGoodCandidate: ['true'],\n            finalScore: 8,\n          },\n        },\n        intro: {\n          isCompleted: true,\n          values: { interviewResult: 'completed' },\n        },\n        practice: {\n          isCompleted: true,\n          values: {\n            questions: [{ value: 4 }],\n            score: 4,\n          },\n        },\n        theory: {\n          isCompleted: true,\n          values: {\n            questions: [{ value: 3 }],\n            score: 3,\n          },\n        },\n      },\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/feedbackTemplateHandler.ts",
    "content": "import { InterviewFeedbackDto } from '@client/api';\nimport { Feedback, FeedbackStep, FeedbackStepId, feedbackTemplate } from '@client/data/interviews/technical-screening';\nimport { InterviewFeedbackStepData, InterviewFeedbackValues, InterviewQuestion } from '@common/models';\n\ntype FeedbackData = {\n  steps: Record<FeedbackStepId, InterviewFeedbackStepData>;\n};\n\n/**\n * Based on existing feedback data returns default template or merges data with its version template\n */\nexport function getFeedbackFromTemplate(interviewFeedback: InterviewFeedbackDto, interviewMaxScore: number): Feedback {\n  // no feedback yet, return all steps based on latest version\n  if (!interviewFeedback.json) {\n    return {\n      steps: feedbackTemplate.steps.map(step => ({ ...step, isCompleted: false })),\n      isCompleted: false,\n      version: feedbackTemplate.version,\n    };\n  }\n\n  const { isCompleted, json, version } = interviewFeedback;\n\n  return mergeFeedbackValuesToTemplate(\n    {\n      version,\n      isCompleted,\n      steps: feedbackTemplate.steps,\n    } as Feedback,\n    json as FeedbackData,\n    interviewMaxScore,\n  );\n}\n\n/**\n * Looks for either first incomplete step or the final one\n */\nexport function getDefaultStep(feedback: Feedback) {\n  for (let i = 0; i < feedback.steps.length; i++) {\n    const step = feedback.steps[i];\n    if (!step) continue;\n\n    const { isCompleted, id, values } = step;\n\n    if (!isCompleted || isInterviewCanceled(id, values) || i === feedback.steps.length - 1) {\n      return i;\n    }\n  }\n\n  return 0;\n}\n\n/**\n * checks whether the step contains rejection value\n */\nexport function isInterviewCanceled(stepId: FeedbackStepId, stepValues: InterviewFeedbackValues = {}) {\n  return stepId === FeedbackStepId.Introduction && stepValues.interviewResult === 'missed';\n}\n\n/**\n * Merges save feedback data with template\n */\nfunction mergeFeedbackValuesToTemplate(feedback: Feedback, data: FeedbackData, interviewMaxScore: number): Feedback {\n  const { steps } = data;\n\n  const mergedFeedback = {\n    ...feedback,\n    steps: feedback.steps.map(step => {\n      const stepData = steps[step.id];\n      const result = {\n        ...step,\n        ...stepData,\n      };\n      const ratedSteps = [FeedbackStepId.Theory, FeedbackStepId.Practice];\n\n      if (ratedSteps.includes(result.id) && result.values) {\n        result.values.score = calculateStepScore(result, interviewMaxScore / ratedSteps.length);\n      }\n\n      return result;\n    }),\n  };\n\n  return applyDefaultFinalScore(mergedFeedback, interviewMaxScore);\n}\n\nfunction applyDefaultFinalScore(mergedFeedback: Feedback, interviewMaxScore: number) {\n  return {\n    ...mergedFeedback,\n    steps: mergedFeedback.steps.map(step => {\n      if (step.id !== FeedbackStepId.Decision) {\n        return step;\n      }\n      return {\n        ...step,\n        items: step.items.map(item => {\n          if (item.id === 'finalScore') {\n            return {\n              ...item,\n              defaultValue: calculateFinalScore(mergedFeedback.steps),\n              max: interviewMaxScore,\n            };\n          }\n          return item;\n        }),\n      };\n    }),\n  };\n}\n\nexport function getUpdatedFeedback({\n  activeStepIndex,\n  feedback,\n  interviewMaxScore,\n  newValues,\n}: {\n  feedback: Feedback;\n  newValues: InterviewFeedbackValues;\n  activeStepIndex: number;\n  interviewMaxScore: number;\n}) {\n  const { steps } = feedback;\n  const activeStep = steps[activeStepIndex];\n  if (!activeStep) {\n    throw new Error('Active step not found');\n  }\n  const isCanceled = isInterviewCanceled(activeStep.id, newValues);\n\n  const feedbackValues = {\n    steps: generateFeedbackValues(steps, activeStepIndex, newValues, isCanceled),\n  };\n  const newFeedback = mergeFeedbackValuesToTemplate(feedback, feedbackValues, interviewMaxScore);\n\n  return {\n    steps: newFeedback.steps,\n    feedbackValues,\n    isCompleted: isInterviewCompleted(newFeedback),\n    ...getInterviewSummary(newFeedback),\n  };\n}\n\nfunction generateFeedbackValues(\n  steps: FeedbackStep[],\n  activeStepIndex: number,\n  newValues: InterviewFeedbackValues,\n  isCanceled: boolean,\n): Record<FeedbackStepId, InterviewFeedbackStepData> {\n  return steps.reduce(\n    (stepMap, step, index) => {\n      if (index === activeStepIndex) {\n        stepMap[step.id] = {\n          isCompleted: true,\n          values: newValues,\n        };\n        return stepMap;\n      }\n\n      // if is canceled, all steps after the current one should be marked as not completed and values should be removed\n      stepMap[step.id] = {\n        values: isCanceled ? undefined : step.values,\n        isCompleted: isCanceled ? false : step.isCompleted,\n      };\n      return stepMap;\n    },\n    {} as Record<FeedbackStepId, InterviewFeedbackStepData>,\n  );\n}\n\n/**\n * Calculates rating/decision & isGoodCandidate using latest feedback state\n */\nfunction getInterviewSummary(feedback: Feedback) {\n  const { steps } = feedback;\n  const decision = steps.find(step => step.id === FeedbackStepId.Decision);\n  const introduction = steps.find(step => step.id === FeedbackStepId.Introduction);\n  const isInterviewConducted = !isInterviewCanceled(FeedbackStepId.Introduction, introduction?.values);\n\n  return {\n    score: isInterviewConducted ? (decision?.values?.finalScore as number) : 0,\n    decision: getDecision(),\n    isGoodCandidate: getIsGoodCandidate(),\n  };\n\n  function getIsGoodCandidate() {\n    if (!isInterviewConducted) {\n      return false;\n    }\n    if (decision?.values?.isGoodCandidate == undefined) {\n      return;\n    }\n\n    return (decision.values.isGoodCandidate as string[]).includes('true');\n  }\n\n  function getDecision() {\n    if (!isInterviewConducted) {\n      // if the interview was missed, return the reason\n      return introduction?.values?.['missed'] as string;\n    }\n\n    return decision?.values?.decision as string;\n  }\n}\n\nfunction isInterviewCompleted(feedback: Feedback) {\n  const { steps } = feedback;\n  const introduction = feedback.steps.find(step => step.id === FeedbackStepId.Introduction);\n\n  return (\n    (introduction && isInterviewCanceled(introduction.id, introduction.values)) || steps.every(step => step.isCompleted)\n  );\n}\n\nfunction calculateFinalScore(steps: Feedback['steps']) {\n  const theory = (steps.find(step => step.id === FeedbackStepId.Theory)?.values?.score as number | undefined) ?? 0;\n  const practice = (steps.find(step => step.id === FeedbackStepId.Practice)?.values?.score as number | undefined) ?? 0;\n\n  return theory + practice;\n}\n\n/**\n * Calculates current step score based on questions values\n */\nfunction calculateStepScore(step: FeedbackStep, interviewMaxScore: number) {\n  const { values = {} } = step;\n  const questions = values.questions as unknown as InterviewQuestion[] | undefined;\n\n  if (!questions) {\n    return 0;\n  }\n  const scorePerQuestion = interviewMaxScore / questions.length;\n\n  return Math.round(\n    questions.reduce((score, question) => {\n      if (!question.value) {\n        return score;\n      }\n      const maxQuestionRating = 5;\n      const proportion = question.value / maxQuestionRating;\n      const questionScore = proportion * scorePerQuestion;\n      return score + questionScore;\n    }, 0),\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Interviews/pages/StageInterviewFeedback/index.ts",
    "content": "export { StageInterviewFeedback } from './StageInterviewFeedback';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/Instructions/Instructions.tsx",
    "content": "import { useState } from 'react';\nimport { Badge, Card, Col, Row, Space, Typography } from 'antd';\nimport { INSTRUCTIONS_TEXT, renderDescription, renderSocialLinks } from '.';\nimport { DiscordServersApi } from '@client/api';\nimport { useAsyncEffect } from 'ahooks';\n\ninterface InstructionsProps {\n  courseId: number;\n  discordServerId: number;\n}\n\nconst { Meta, Grid } = Card;\nconst { Text } = Typography;\n\nconst discordServer = new DiscordServersApi();\n\nfunction Instructions({ courseId, discordServerId }: InstructionsProps) {\n  const { title, description } = INSTRUCTIONS_TEXT;\n  const [steps, setSteps] = useState(INSTRUCTIONS_TEXT.steps);\n\n  useAsyncEffect(async () => {\n    if (!discordServerId) return;\n\n    const response = await discordServer.getInviteLinkByDiscordServerId(courseId, discordServerId);\n    const telegramInviteURL = response.data;\n\n    const updatedSteps = steps.map(step => {\n      if (!step.links) return step;\n\n      const updatedLinks = step.links.map(link =>\n        link.title === 'telegram' ? { ...link, url: telegramInviteURL } : link,\n      );\n\n      return { ...step, links: updatedLinks };\n    });\n\n    setSteps(updatedSteps);\n  }, []);\n\n  return (\n    <Card bordered={false}>\n      <Grid style={{ width: '100%' }} hoverable={false}>\n        <Meta title={title} description={<Text>{description}</Text>} />\n      </Grid>\n      <Row>\n        {steps.map((s, idx) => (\n          <Col key={s.title} xs={24} sm={24} md={8}>\n            <Grid hoverable={false} style={{ width: '100%', height: '100%' }}>\n              <Meta\n                title={\n                  <Space>\n                    <Badge count={idx + 1} style={{ backgroundColor: '#1677ff' }} />\n                    {s.title}\n                  </Space>\n                }\n                description={\n                  <Space direction=\"vertical\" size=\"middle\">\n                    {renderDescription(s.html)}\n                    {s.links ? renderSocialLinks(s.links) : null}\n                  </Space>\n                }\n              />\n            </Grid>\n          </Col>\n        ))}\n      </Row>\n    </Card>\n  );\n}\n\nexport default Instructions;\n"
  },
  {
    "path": "client/src/modules/Mentor/components/Instructions/constants.ts",
    "content": "export const INSTRUCTIONS_TEXT = {\n  title: \"What's next?\",\n  description: \"Don't panic! Information about students' tasks for review will appear here.\",\n  steps: [\n    {\n      title: 'Wait for the distribution of students',\n      html: \"We haven't forgotten about you, don't worry. Make sure notifications are turned on. While you can read <a href='https://docs.rs.school/#/mentoring-kick-off' target='_blank'>this</a> information and subscribe to our communities:\",\n      links: [\n        { title: 'github', url: 'https://github.com/rolling-scopes/rsschool-app' },\n        { title: 'discord', url: 'https://discord.com/invite/PRADsJB' },\n        { title: 'linkedin', url: 'https://www.linkedin.com/company/the-rolling-scopes-school/' },\n        { title: 'telegram', url: '' },\n      ],\n    },\n    {\n      title: 'Interview with students',\n      html: 'Wait for the distribution of students for the Screenings interview. You will receive an notification when students are appointed to the interview with you. From among them, choose for those whom you want to mentor. More details about the procedure can be found <a href=\"https://docs.rs.school/#/mentoring-first-interview\" target=\"_blank\">here</a>.',\n    },\n    {\n      title: \"Check your students' tasks\",\n      html: 'Check tasks and set score for them. You can help students in every possible way in the process or check the final tasks only. It all depends on how you feel comfortable building the process. More details about the procedure can be found <a href=\"https://docs.app.rs.school/#/platform/pull-request-review-process\" target=\"_blank\">here</a>.',\n    },\n  ],\n};\n"
  },
  {
    "path": "client/src/modules/Mentor/components/Instructions/index.tsx",
    "content": "export { default as Instructions } from './Instructions';\nexport * from './constants';\nexport * from './renderers';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/Instructions/renderers.tsx",
    "content": "import { Space, theme, Typography } from 'antd';\nimport { DiscordFilled, LinkedInIcon, TelegramIcon } from '@client/shared/components/Icons';\nimport { GithubOutlined } from '@ant-design/icons';\n\nconst { Link, Text } = Typography;\n\nconst getSocialLinkIcon = (title: string, color?: string) => {\n  switch (title) {\n    case 'discord':\n      return <DiscordFilled style={{ fontSize: '24px', color: '#5865F2' }} />;\n    case 'linkedin':\n      return <LinkedInIcon />;\n    case 'telegram':\n      return <TelegramIcon />;\n    case 'github':\n      return <GithubOutlined style={{ fontSize: '24px', color: color }} />;\n    default:\n      break;\n  }\n};\n\nexport const renderSocialLinks = (links: Record<'title' | 'url', string>[]) => {\n  const { token } = theme.useToken();\n  return (\n    <Space size=\"middle\" style={{ width: '100%', justifyContent: 'center' }}>\n      {links.map(link =>\n        link.url ? (\n          <Link key={link.title} href={link.url} target=\"_blank\">\n            {getSocialLinkIcon(link.title, token.colorTextBase)}\n          </Link>\n        ) : (\n          <span key={link.title} style={{ cursor: 'not-allowed', opacity: 0.5 }}>\n            {getSocialLinkIcon(link.title, token.colorTextBase)}\n          </span>\n        ),\n      )}\n    </Space>\n  );\n};\n\nexport const renderDescription = (text: string) => {\n  return (\n    <Text>\n      <div\n        dangerouslySetInnerHTML={{\n          __html: text,\n        }}\n      />\n    </Text>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { CourseInfo, Session } from '@client/components/withSession';\nimport { SessionContext } from '@client/modules/Course/contexts';\nimport { INSTRUCTIONS_TEXT } from '../Instructions';\nimport MentorDashboard from './MentorDashboard';\nimport { useMentorDashboard } from '@client/modules/Mentor/hooks/useMentorDashboard';\n\nvi.mock('@client/modules/Mentor/hooks/useMentorDashboard');\n\nvi.mock('next/router', () => ({\n  useRouter: vi.fn().mockImplementation(() => ({ asPath: '/course/mentor/' })),\n}));\n\ndescribe('MentorDashboard', () => {\n  it('should render instructions when mentor has no students for this course', async () => {\n    vi.mocked(useMentorDashboard).mockReturnValue([[], false, vi.fn()]);\n\n    render(\n      <SessionContext.Provider\n        value={\n          {\n            id: 1,\n            isActivist: false,\n            isAdmin: true,\n            isHirer: false,\n            githubId: 'github-id',\n            courses: {\n              '400': {\n                mentorId: 1,\n                roles: ['mentor'],\n              } as CourseInfo,\n            },\n          } as Session\n        }\n      >\n        <MentorDashboard />\n      </SessionContext.Provider>,\n    );\n\n    const instructionsTitle = await screen.findByText(INSTRUCTIONS_TEXT.title);\n\n    expect(instructionsTitle).toBeInTheDocument();\n  });\n\n  it('should render table when mentor has students for this course', async () => {\n    const mockData = [\n      {\n        courseTaskId: 1,\n        endDate: new Date('2025-01-01').toISOString(),\n        maxScore: 100,\n        studentName: 'John Doe',\n        taskName: 'Task 1',\n        studentGithubId: '',\n        taskDescriptionUrl: '',\n        resultScore: null,\n        solutionUrl: '',\n        status: 'in-review',\n      } as const,\n    ];\n\n    vi.mocked(useMentorDashboard).mockReturnValue([mockData, false, vi.fn()]);\n\n    render(\n      <SessionContext.Provider\n        value={\n          {\n            id: 1,\n            isActivist: false,\n            isAdmin: true,\n            isHirer: false,\n            githubId: 'github-id',\n            courses: {\n              '400': {\n                mentorId: 1,\n                roles: ['mentor'],\n              } as CourseInfo,\n            },\n          } as Session\n        }\n      >\n        <MentorDashboard />\n      </SessionContext.Provider>,\n    );\n\n    const emptyTable = await screen.findByText(/John Doe/i);\n\n    expect(emptyTable).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Mentor/components/MentorDashboard/MentorDashboard.tsx",
    "content": "import { PageLayout } from '@client/shared/components/PageLayout';\nimport { Instructions, Notification, TaskSolutionsTable } from '..';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useContext } from 'react';\nimport { useMentorDashboard } from '@client/modules/Mentor/hooks/useMentorDashboard';\n\nfunction MentorDashboard() {\n  const { courses } = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const { id: courseId, discordServerId } = course;\n  const mentorId = courses?.[courseId]?.mentorId as number;\n\n  const [data, loading, run] = useMentorDashboard(mentorId, courseId);\n\n  return (\n    <PageLayout loading={loading} title=\"Mentor's Dashboard\" showCourseName>\n      <Notification />\n      {data?.length ? (\n        <TaskSolutionsTable data={data} loading={loading} onChange={run} mentorId={mentorId} courseId={courseId} />\n      ) : (\n        <Instructions courseId={courseId} discordServerId={discordServerId} />\n      )}\n    </PageLayout>\n  );\n}\n\nexport default MentorDashboard;\n"
  },
  {
    "path": "client/src/modules/Mentor/components/MentorDashboard/index.tsx",
    "content": "export { default as MentorDashboard } from './MentorDashboard';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/Notification/Notification.test.tsx",
    "content": "import { screen, render, fireEvent } from '@testing-library/react';\nimport { Notification } from '.';\nimport { INFO_MESSAGE } from '@client/modules/Mentor/constants';\nimport * as ReactUse from 'react-use';\n\ndescribe('Notification', () => {\n  it('should render alert', () => {\n    render(<Notification />);\n\n    expect(screen.getByText(INFO_MESSAGE)).toBeInTheDocument();\n  });\n\n  it('should not render alert when it was hide previously', () => {\n    vi.spyOn(ReactUse, 'useLocalStorage').mockReturnValueOnce([false, vi.fn(), vi.fn()]);\n    render(<Notification />);\n\n    expect(screen.queryByText(INFO_MESSAGE)).not.toBeInTheDocument();\n  });\n\n  it('should hide alert when close icon was clicked', () => {\n    const setIsShowMock = vi.fn();\n    vi.spyOn(ReactUse, 'useLocalStorage').mockReturnValueOnce([true, setIsShowMock, vi.fn()]);\n    render(<Notification />);\n\n    const closeIcon = screen.getByRole('img', { name: 'close' });\n    fireEvent.click(closeIcon);\n\n    expect(setIsShowMock).toHaveBeenCalledWith(false);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Mentor/components/Notification/Notification.tsx",
    "content": "import { Alert } from 'antd';\nimport { useLocalStorage } from 'react-use';\nimport { INFO_MESSAGE } from '@client/modules/Mentor/constants';\n\nfunction Notification() {\n  const [isShown, setIsShown] = useLocalStorage('isPersonalInformationNotificationShown', true);\n\n  const handleClose = () => {\n    setIsShown(false);\n  };\n\n  return isShown ? (\n    <Alert message={INFO_MESSAGE} type=\"info\" showIcon closable onClose={handleClose} style={{ marginBottom: 24 }} />\n  ) : null;\n}\n\nexport default Notification;\n"
  },
  {
    "path": "client/src/modules/Mentor/components/Notification/index.tsx",
    "content": "export { default as Notification } from './Notification';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/ReviewRandomTask/ReviewRandomTask.tsx",
    "content": "import { Button, message } from 'antd';\nimport { useLoading } from '@client/components/useLoading';\nimport { MentorsApi } from '@client/api';\nimport { AxiosError } from 'axios';\nimport { EyeOutlined } from '@ant-design/icons';\n\ninterface Props {\n  mentorId: number;\n  courseId: number;\n  onClick: () => void;\n}\n\nfunction ReviewRandomTask({ mentorId, courseId, onClick }: Props) {\n  const [loading, withLoading] = useLoading(false, e => {\n    const error = e as AxiosError;\n\n    if (error.response?.status === 404) {\n      message.info('Task for review was not found. Please try later.');\n    }\n  });\n\n  const handleClick = withLoading(async () => {\n    const service = new MentorsApi();\n    await service.getRandomTask(mentorId, courseId);\n    onClick();\n  });\n\n  return (\n    <Button type=\"primary\" icon={<EyeOutlined />} loading={loading} disabled={loading} onClick={handleClick}>\n      Review random task\n    </Button>\n  );\n}\n\nexport default ReviewRandomTask;\n"
  },
  {
    "path": "client/src/modules/Mentor/components/ReviewRandomTask/index.tsx",
    "content": "export { default as ReviewRandomTask } from './ReviewRandomTask';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/SubmitReviewModal/SubmitReviewModal.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { MentorDashboardDto } from '@client/api';\nimport { MODAL_TITLE, SubmitReviewModal, SubmitReviewModalProps } from '.';\nimport { SUCCESS_MESSAGE } from './SubmitReviewModal';\n\nconst { mockAxios } = vi.hoisted(() => {\n  const post = vi.fn();\n  const instance = {\n    get: vi.fn(),\n    post,\n    put: vi.fn(),\n    patch: vi.fn(),\n    delete: vi.fn(),\n    interceptors: {\n      request: { use: vi.fn(), eject: vi.fn() },\n      response: { use: vi.fn(), eject: vi.fn() },\n    },\n  };\n  return {\n    mockAxios: {\n      ...instance,\n      create: vi.fn(() => instance),\n      defaults: { headers: { common: {} } },\n      reset() {\n        post.mockReset();\n      },\n    },\n  };\n});\n\nvi.mock('axios', () => ({ default: mockAxios }));\n\nconst MODAL_DATA_MOCK = {\n  courseTaskId: 1,\n  maxScore: 100,\n  resultScore: 20,\n  solutionUrl: `solution-url`,\n  studentGithubId: `student-github`,\n  studentName: `Student Name`,\n  taskDescriptionUrl: `task-url`,\n  taskName: `Task Name`,\n} as MentorDashboardDto;\n\nconst PROPS_MOCK: SubmitReviewModalProps = {\n  data: MODAL_DATA_MOCK,\n  courseId: 1,\n  onClose: vi.fn(),\n  onSubmit: vi.fn(),\n};\n\ndescribe('SubmitReviewModal', () => {\n  afterEach(() => {\n    mockAxios.reset();\n  });\n\n  it.each`\n    text                           | role\n    ${MODAL_DATA_MOCK.taskName}    | ${'link'}\n    ${MODAL_DATA_MOCK.solutionUrl} | ${'link'}\n    ${'Submit'}                    | ${'button'}\n    ${'Cancel'}                    | ${'button'}\n  `('should render $role \"$text\"', ({ text, role }: { text: string; role: string }) => {\n    render(<SubmitReviewModal {...PROPS_MOCK} />);\n\n    const element = screen.getByRole(role, { name: new RegExp(text) });\n\n    expect(element).toBeInTheDocument();\n  });\n\n  it.each`\n    text\n    ${MODAL_DATA_MOCK.maxScore}\n    ${MODAL_DATA_MOCK.studentName}\n  `('should render field \"$text\"', ({ text }: { text: string }) => {\n    render(<SubmitReviewModal {...PROPS_MOCK} />);\n\n    const element = screen.getByText(new RegExp(text));\n\n    expect(element).toBeInTheDocument();\n  });\n\n  it('should not render fields when data was not provided', () => {\n    render(<SubmitReviewModal {...PROPS_MOCK} data={null} />);\n\n    const modal = screen.queryByText(new RegExp(MODAL_TITLE));\n\n    expect(modal).not.toBeInTheDocument();\n  });\n\n  it('should render success message on submit', async () => {\n    mockAxios.post.mockResolvedValueOnce({ data: true });\n    render(<SubmitReviewModal {...PROPS_MOCK} />);\n    const scoreInput = screen.getByRole('spinbutton', { name: /score \\(max 100 points\\)/i });\n    fireEvent.change(scoreInput, { target: { value: 10 } });\n\n    const submitBtn = screen.getByRole('button', { name: 'Submit' });\n    fireEvent.click(submitBtn);\n\n    const message = await screen.findByText(SUCCESS_MESSAGE);\n    expect(message).toBeInTheDocument();\n  });\n\n  it('should render error message when error has occurred on submit', async () => {\n    const errorMessage = 'Network error';\n    mockAxios.post.mockRejectedValueOnce(new Error(errorMessage));\n    render(<SubmitReviewModal {...PROPS_MOCK} />);\n    const scoreInput = screen.getByRole('spinbutton', { name: /score \\(max 100 points\\)/i });\n    fireEvent.change(scoreInput, { target: { value: 10 } });\n\n    const submitBtn = screen.getByRole('button', { name: 'Submit' });\n    fireEvent.click(submitBtn);\n\n    const message = await screen.findByText(errorMessage);\n    expect(message).toBeInTheDocument();\n  });\n\n  it('should call onClose when \"Cancel\" button was clicked', async () => {\n    render(<SubmitReviewModal {...PROPS_MOCK} />);\n    const cancelBtn = screen.getByRole('button', { name: 'Cancel' });\n\n    fireEvent.click(cancelBtn);\n\n    expect(PROPS_MOCK.onClose).toHaveBeenCalledWith(null);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Mentor/components/SubmitReviewModal/SubmitReviewModal.tsx",
    "content": "import { Col, Form, Row, Typography } from 'antd';\nimport { useMemo, useState } from 'react';\nimport { ModalSubmitForm } from '@client/shared/components/Forms';\nimport { ScoreInput } from '@client/shared/components/Forms';\nimport { MentorDashboardDto } from '@client/api';\nimport { CourseService } from '@client/services/course';\nimport isEmpty from 'lodash/isEmpty';\nimport { AxiosError } from 'axios';\n\nexport interface SubmitReviewModalProps {\n  data: MentorDashboardDto | null;\n  courseId: number;\n  onClose: (d: MentorDashboardDto | null) => void;\n  onSubmit: () => void;\n}\n\nconst { Link } = Typography;\n\nexport const MODAL_TITLE = 'Submit Score for';\nexport const SUCCESS_MESSAGE = 'Your review has been successfully submitted';\n\nfunction SubmitReviewModal({ data, courseId, onClose, onSubmit }: SubmitReviewModalProps) {\n  const { studentGithubId, courseTaskId, solutionUrl, studentName, taskDescriptionUrl, taskName, maxScore } =\n    data || {};\n\n  const [loading, setLoading] = useState(false);\n  const [submitted, setSubmitted] = useState(false);\n  const [errorText, setErrorText] = useState('');\n\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n\n  const handleSubmit = async (values: { score: number }) => {\n    setLoading(true);\n\n    try {\n      if (studentGithubId && courseTaskId) {\n        await courseService.postStudentScore(studentGithubId, courseTaskId, {\n          score: values.score,\n          githubPrUrl: solutionUrl,\n        });\n        setSubmitted(true);\n        onSubmit();\n      }\n    } catch (e) {\n      const error = (e as AxiosError<{ message: string }>).response?.data?.message ?? (e as Error).message;\n      setErrorText(error);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleClose = () => {\n    setErrorText('');\n    setLoading(false);\n    setSubmitted(false);\n    onClose(null);\n  };\n\n  return (\n    <ModalSubmitForm\n      title={`${MODAL_TITLE} ${studentName}`}\n      data={data}\n      submit={handleSubmit}\n      close={handleClose}\n      errorText={errorText}\n      loading={loading}\n      submitted={submitted}\n      successText={SUCCESS_MESSAGE}\n      open={!isEmpty(data)}\n    >\n      <Row>\n        <Col span={18} offset={3}>\n          <Form.Item label=\"Task\" name=\"task\">\n            <Link href={taskDescriptionUrl} target=\"_blank\">\n              {taskName}\n            </Link>\n          </Form.Item>\n          <Form.Item label=\"GitHub Pull Request\" name=\"prUrl\">\n            <Link href={solutionUrl} target=\"_blank\">\n              {solutionUrl}\n            </Link>\n          </Form.Item>\n          <ScoreInput maxScore={maxScore} style={{ width: '100%' }} />\n        </Col>\n      </Row>\n    </ModalSubmitForm>\n  );\n}\n\nexport default SubmitReviewModal;\n"
  },
  {
    "path": "client/src/modules/Mentor/components/SubmitReviewModal/index.tsx",
    "content": "export { default as SubmitReviewModal, type SubmitReviewModalProps, MODAL_TITLE } from './SubmitReviewModal';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskSolutionsTable/TaskSolutionsTable.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { TaskSolutionsTable, TaskSolutionsTableProps } from '.';\nimport { MentorDashboardDto } from '@client/api';\nimport { SolutionItemStatus, TaskSolutionsTableColumnName } from '../../constants';\n\nvi.mock('@client/modules/Mentor/hooks/useMentorDashboard');\n\nfunction generateData(count = 3): MentorDashboardDto[] {\n  return new Array(count).fill({}).map((_, idx) => ({\n    courseTaskId: idx + 1,\n    maxScore: idx + 100,\n    resultScore: idx + 20,\n    solutionUrl: `solution-url-${idx}`,\n    studentGithubId: `student-github-${idx}`,\n    studentName: `Student ${idx}`,\n    taskDescriptionUrl: `task-url-${idx}`,\n    taskName: `Task ${idx}`,\n    status: SolutionItemStatus.InReview,\n    endDate: new Date(`1970-02-0${idx + 1}T00:00:00`).toISOString(),\n  }));\n}\n\nconst mockProps: TaskSolutionsTableProps = {\n  mentorId: 1,\n  courseId: 400,\n  data: generateData(),\n  loading: false,\n  onChange: vi.fn(),\n};\n\ndescribe('TaskSolutionsTable', () => {\n  describe('when full data was provided', () => {\n    it.each`\n      label\n      ${TaskSolutionsTableColumnName.Student}\n      ${TaskSolutionsTableColumnName.Task}\n      ${TaskSolutionsTableColumnName.SolutionUrl}\n      ${TaskSolutionsTableColumnName.Score}\n      ${TaskSolutionsTableColumnName.SubmitScores}\n      ${TaskSolutionsTableColumnName.DesiredDeadline}\n    `('should render column name \"$label\"', ({ label }: { label: string }) => {\n      render(<TaskSolutionsTable {...mockProps} />);\n\n      const name = screen.getByText(label);\n\n      expect(name).toBeInTheDocument();\n    });\n\n    it.each`\n      value\n      ${'Student 0'}\n      ${'Task 0'}\n      ${'solution-url-0'}\n      ${'20 / 100'}\n      ${'1970-02-01 00:00'}\n    `('should render column data \"$value\"', ({ value }: { value: string }) => {\n      render(<TaskSolutionsTable {...mockProps} />);\n\n      expect(screen.getByText(value)).toBeInTheDocument();\n    });\n\n    it('should render \"Review random task\" button when \"Random task\" tab is selected', async () => {\n      render(<TaskSolutionsTable {...mockProps} />);\n      const randomTaskTab = screen.getByRole('tab', { name: /random task/i });\n\n      fireEvent.click(randomTaskTab);\n\n      const reviewBtn = await screen.findByText(/review random task/i);\n      expect(reviewBtn).toBeInTheDocument();\n    });\n\n    it('should not render \"Review random task\" button when \"Random task\" tab is not selected', () => {\n      render(<TaskSolutionsTable {...mockProps} />);\n\n      const reviewBtn = screen.queryByText(/review random task/i);\n      expect(reviewBtn).not.toBeInTheDocument();\n    });\n  });\n\n  describe('when result score was not provided', () => {\n    describe('and when deadline passed', () => {\n      it('should render date as warning', () => {\n        const data = [\n          {\n            ...(generateData()[0] as MentorDashboardDto),\n            resultScore: null,\n            endDate: new Date('1970-05-05T00:00:00').toISOString(),\n          },\n        ];\n        render(<TaskSolutionsTable {...mockProps} data={data} />);\n\n        const date = screen.getByText('1970-05-05 00:00');\n        expect(date).toHaveClass('ant-typography-warning');\n      });\n    });\n\n    it('should render \"-\" instead of result score', () => {\n      const data = [\n        {\n          ...(generateData()[0] as MentorDashboardDto),\n          resultScore: null,\n          endDate: new Date('1970-05-05T00:00:00').toISOString(),\n        },\n      ];\n      render(<TaskSolutionsTable {...mockProps} data={data} />);\n\n      const score = screen.getByText('- / 100');\n      expect(score).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskSolutionsTable/TaskSolutionsTable.tsx",
    "content": "import { Col, Row, Table } from 'antd';\nimport { MentorDashboardDto } from '@client/api';\nimport { useMemo, useState } from 'react';\nimport { getColumns } from './renderers';\nimport { SolutionItemStatus } from '../../constants';\nimport { ReviewRandomTask } from '../ReviewRandomTask';\nimport { SubmitReviewModal } from '../SubmitReviewModal';\nimport { TaskStatusTabs } from '../TaskStatusTabs';\n\nexport interface TaskSolutionsTableProps {\n  mentorId: number;\n  courseId: number;\n  onChange: () => void;\n  data: MentorDashboardDto[];\n  loading: boolean;\n}\n\nconst getUniqueKey = (record: MentorDashboardDto) => Object.values(record).filter(Boolean).join('|');\n\nfunction TaskSolutionsTable({ mentorId, onChange, data, loading, courseId }: TaskSolutionsTableProps) {\n  const [modalData, setModalData] = useState<MentorDashboardDto | null>(null);\n  const [activeTab, setActiveTab] = useState(SolutionItemStatus.InReview);\n\n  const statuses = useMemo(() => data?.map(({ status }) => status as SolutionItemStatus), [data]);\n  const isReviewRandomTaskVisible = useMemo(() => {\n    const hasRandomTask = data && data?.filter(({ status }) => status === SolutionItemStatus.RandomTask)?.length > 0;\n    return activeTab === SolutionItemStatus.RandomTask && !hasRandomTask;\n  }, [data, activeTab]);\n\n  const filteredData = data?.filter(item => item.status === activeTab);\n\n  const handleSubmitButtonClick = (data: MentorDashboardDto) => {\n    setModalData(data);\n  };\n\n  return (\n    <>\n      <Row>\n        <Col span={24}>\n          <TaskStatusTabs statuses={statuses} onTabChange={setActiveTab} activeTab={activeTab} />\n        </Col>\n      </Row>\n      <Row style={{ padding: '0 24px' }}>\n        <Col span={24}>\n          <Table\n            locale={{\n              // disable default tooltips on sortable columns\n              triggerDesc: undefined,\n              triggerAsc: undefined,\n              cancelSort: undefined,\n            }}\n            pagination={false}\n            columns={getColumns(handleSubmitButtonClick)}\n            dataSource={filteredData}\n            size=\"middle\"\n            rowKey={getUniqueKey}\n            loading={loading}\n          />\n        </Col>\n      </Row>\n      {isReviewRandomTaskVisible && (\n        <Row style={{ padding: '24px 0' }} justify=\"center\">\n          <Col>\n            <ReviewRandomTask mentorId={mentorId} courseId={courseId} onClick={onChange} />\n          </Col>\n        </Row>\n      )}\n      <SubmitReviewModal courseId={courseId} data={modalData} onClose={setModalData} onSubmit={onChange} />\n    </>\n  );\n}\n\nexport default TaskSolutionsTable;\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskSolutionsTable/index.tsx",
    "content": "export { default as TaskSolutionsTable, type TaskSolutionsTableProps } from './TaskSolutionsTable';\nexport * from './renderers';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskSolutionsTable/renderers.tsx",
    "content": "import { ColumnsType, ColumnType } from 'antd/lib/table';\nimport { TaskSolutionsTableColumnKey, TaskSolutionsTableColumnName } from '@client/modules/Mentor/constants';\nimport { dateSorter, dateWithTimeZoneRenderer, getColumnSearchProps } from '@client/shared/components/Table';\nimport { Button, Space, Typography } from 'antd';\nimport { MentorDashboardDto } from '@client/api';\nimport dayjs from 'dayjs';\n\nconst { Text, Link } = Typography;\n\ntype Breakpoint = ColumnType<MentorDashboardDto>['responsive'];\n\nconst FORMAT = 'YYYY-MM-DD HH:mm';\nconst TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;\nconst DISPLAY_TABLE_BREAKPOINTS: Breakpoint = ['sm'];\nconst DISPLAY_TABLE_MOBILE_BREAKPOINT: Breakpoint = ['xs'];\n\nexport const getColumns = (handleSubmitClick: (data: MentorDashboardDto) => void): ColumnsType<MentorDashboardDto> => [\n  {\n    key: TaskSolutionsTableColumnKey.Number,\n    title: TaskSolutionsTableColumnName.Number,\n    align: 'center',\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n    render: (_v, _r, idx) => idx + 1,\n  },\n  {\n    key: TaskSolutionsTableColumnKey.Student,\n    title: TaskSolutionsTableColumnName.Student,\n    dataIndex: 'studentName',\n    render: renderName,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n    ...getColumnSearchProps('studentName'),\n  },\n  {\n    key: TaskSolutionsTableColumnKey.Task,\n    title: TaskSolutionsTableColumnName.Task,\n    dataIndex: 'taskName',\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n    render: renderTask,\n  },\n  {\n    key: TaskSolutionsTableColumnKey.SolutionUrl,\n    title: TaskSolutionsTableColumnName.SolutionUrl,\n    dataIndex: 'solutionUrl',\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n    render: renderSolutionUrl,\n  },\n  {\n    key: TaskSolutionsTableColumnKey.DesiredDeadline,\n    title: TaskSolutionsTableColumnName.DesiredDeadline,\n    dataIndex: 'endDate',\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n    sortDirections: ['descend', 'ascend'],\n    render: renderDate,\n    sorter: dateSorter('endDate'),\n  },\n  {\n    key: TaskSolutionsTableColumnKey.Score,\n    title: TaskSolutionsTableColumnName.Score,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n    align: 'right',\n    render: renderScore,\n  },\n  {\n    key: TaskSolutionsTableColumnKey.Task,\n    title: TaskSolutionsTableColumnName.Task,\n    responsive: DISPLAY_TABLE_MOBILE_BREAKPOINT,\n    render: renderMobile,\n  },\n  {\n    key: TaskSolutionsTableColumnKey.SubmitScores,\n    title: TaskSolutionsTableColumnName.SubmitScores,\n    align: 'center',\n    render: row => renderSubmitButton(row, handleSubmitClick),\n  },\n];\n\nfunction renderName(value: string, { studentName, studentGithubId }: MentorDashboardDto) {\n  if (!studentName) return value;\n\n  return (\n    <Link target=\"_blank\" href={`/profile?githubId=${studentGithubId}`}>\n      {value}\n    </Link>\n  );\n}\n\nfunction renderTask(value: string, { taskDescriptionUrl }: MentorDashboardDto) {\n  if (!taskDescriptionUrl) return value;\n\n  return (\n    <Link target=\"_blank\" href={taskDescriptionUrl}>\n      {value}\n    </Link>\n  );\n}\n\nfunction renderSolutionUrl(value: string, { solutionUrl }: MentorDashboardDto) {\n  if (!solutionUrl) return value;\n\n  return (\n    <Link target=\"_blank\" href={solutionUrl}>\n      {value}\n    </Link>\n  );\n}\n\nfunction renderScore(_v: string, { maxScore, resultScore }: MentorDashboardDto) {\n  if (!maxScore) return null;\n\n  return (\n    <Text>\n      {resultScore ?? '-'} / {maxScore}\n    </Text>\n  );\n}\n\nfunction renderSubmitButton(row: MentorDashboardDto, handleSubmitClick: (d: MentorDashboardDto) => void) {\n  return (\n    <Button type=\"link\" onClick={() => handleSubmitClick(row)}>\n      Submit\n    </Button>\n  );\n}\n\nfunction renderMobile(row: MentorDashboardDto) {\n  return (\n    <Space direction=\"vertical\">\n      {renderName(row.studentName, row)}\n      {renderTask(row.taskName, row)}\n      {renderSolutionUrl(row.solutionUrl, row)}\n      {renderDate(row.endDate, row)}\n      {renderScore('', row)}\n    </Space>\n  );\n}\n\nfunction renderDate(value: string, { endDate, resultScore }: MentorDashboardDto) {\n  const now = dayjs();\n  const end = dayjs(endDate);\n  const color = end.diff(now, 'hours') < 48 && !resultScore ? 'warning' : undefined;\n  const text = dateWithTimeZoneRenderer(TIMEZONE, FORMAT)(value);\n\n  return <Text type={color}>{text}</Text>;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskStatusTabs/TaskStatusTabs.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport TaskStatusTabs, { Status } from './TaskStatusTabs';\nimport { SolutionItemStatus, TASKS_STATUSES } from '@client/modules/Mentor/constants';\n\nconst PROPS_MOCK = {\n  statuses: [],\n  onTabChange: vi.fn(),\n  activeTab: SolutionItemStatus.InReview,\n};\n\ndescribe('TaskStatusTabs', () => {\n  it('should render status tabs', () => {\n    const statuses = generateStatuses();\n\n    render(<TaskStatusTabs {...PROPS_MOCK} statuses={statuses} />);\n\n    expect(screen.getAllByRole('tab')).toHaveLength(TASKS_STATUSES.length);\n  });\n\n  it('should render status tabs when statuses were not provided', () => {\n    render(<TaskStatusTabs {...PROPS_MOCK} statuses={[]} />);\n\n    expect(screen.getAllByRole('tab')).toHaveLength(TASKS_STATUSES.length);\n  });\n\n  it.each`\n    status                           | count\n    ${SolutionItemStatus.Done}       | ${2}\n    ${SolutionItemStatus.InReview}   | ${3}\n    ${SolutionItemStatus.RandomTask} | ${4}\n  `(\n    'should render badge with count of $count for \"$status\" tab',\n    ({ status, count }: { status: SolutionItemStatus; count: number }) => {\n      const statuses = generateStatuses(count, status);\n\n      render(<TaskStatusTabs {...PROPS_MOCK} statuses={statuses} />);\n\n      expect(screen.getByText(count)).toBeInTheDocument();\n    },\n  );\n\n  describe('when active tab was changed', () => {\n    it('should call onTabChange with tab name \"Done\"', () => {\n      const tabName = SolutionItemStatus.Done;\n      const statuses = generateStatuses();\n      render(<TaskStatusTabs {...PROPS_MOCK} statuses={statuses} />);\n\n      const selectedTab = screen.getByText(new RegExp(tabName, 'i'));\n      fireEvent.click(selectedTab);\n\n      expect(PROPS_MOCK.onTabChange).toHaveBeenCalledWith(tabName);\n    });\n  });\n});\n\nfunction generateStatuses(count = 3, status = SolutionItemStatus.InReview): Status[] {\n  return new Array(count).fill('').map(() => status);\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskStatusTabs/TaskStatusTabs.tsx",
    "content": "import { Tabs } from 'antd';\nimport { FC, useMemo } from 'react';\nimport { tabsRenderer } from './renderers';\nimport { SolutionItemStatus } from '../../constants';\n\nexport type Status = SolutionItemStatus;\n\nexport interface TaskStatusTabsProps {\n  statuses?: Status[];\n  activeTab?: Status;\n  onTabChange: (tab: Status) => void;\n}\n\nconst TaskStatusTabs: FC<TaskStatusTabsProps> = ({ statuses, activeTab, onTabChange }) => {\n  const tabs = useMemo(() => tabsRenderer(statuses, activeTab), [statuses, activeTab]);\n\n  const handleTabChange = (selectedTab: string) => {\n    onTabChange(selectedTab as Status);\n  };\n\n  return <Tabs tabBarStyle={{ padding: '0 24px' }} activeKey={activeTab} items={tabs} onChange={handleTabChange} />;\n};\n\nexport default TaskStatusTabs;\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskStatusTabs/index.ts",
    "content": "export { default as TaskStatusTabs, type Status } from './TaskStatusTabs';\n"
  },
  {
    "path": "client/src/modules/Mentor/components/TaskStatusTabs/renderers.tsx",
    "content": "import { Space } from 'antd';\nimport { ReactNode } from 'react';\nimport { Status } from '.';\nimport { CountBadge } from '@client/components/CountBadge';\nimport { TASKS_STATUSES } from '@client/modules/Mentor/constants';\n\ntype TabItem = {\n  label: ReactNode;\n  key: string;\n};\n\nexport const tabsRenderer = (statuses?: Status[], activeTab?: Status): TabItem[] =>\n  TASKS_STATUSES.reduce((tabs: TabItem[], { label, key }: { label: string; key: string }) => {\n    const count = statuses?.filter(el => el === key).length ?? 0;\n    const badgeStatus = activeTab === key ? 'processing' : 'default';\n    const readableLabel = label.replace(/([A-Z])/g, ' $1');\n\n    const tab = {\n      key,\n      label: (\n        <Space>\n          {readableLabel}\n          <CountBadge showZero count={count} status={badgeStatus} />\n        </Space>\n      ),\n    };\n\n    return [...tabs, tab];\n  }, []);\n"
  },
  {
    "path": "client/src/modules/Mentor/components/index.tsx",
    "content": "export { MentorDashboard } from './MentorDashboard';\nexport { Notification } from './Notification';\nexport { Instructions } from './Instructions';\nexport { TaskSolutionsTable } from './TaskSolutionsTable';\nexport { SubmitReviewModal } from './SubmitReviewModal';\n"
  },
  {
    "path": "client/src/modules/Mentor/constants.ts",
    "content": "export const INFO_MESSAGE =\n  'Please be sure that your personal information is filled on profile, so that assigned students can contact you.';\n\nexport enum TaskSolutionsTableColumnKey {\n  Number = 'number',\n  Student = 'student',\n  Task = 'task',\n  DesiredDeadline = 'desiredDeadline',\n  Score = 'score',\n  SubmitScores = 'submitScores',\n  SolutionUrl = 'solutionUrl',\n}\n\nexport enum TaskSolutionsTableColumnName {\n  Number = '#',\n  Student = 'Student',\n  Task = 'Task',\n  DesiredDeadline = 'Desired deadline',\n  Score = 'Score / Max',\n  SubmitScores = 'Submit scores',\n  SolutionUrl = 'Pull request',\n}\n\nexport enum SolutionItemStatus {\n  InReview = 'in-review',\n  Done = 'done',\n  RandomTask = 'random-task',\n}\n\nexport const TASKS_STATUSES = Object.entries(SolutionItemStatus).map(([key, value]) => ({\n  key: value,\n  label: key,\n}));\n"
  },
  {
    "path": "client/src/modules/Mentor/data/softSkills.ts",
    "content": "import { SoftSkillEntryIdEnum, SoftSkillEntryValueEnum } from '@client/api';\n\nexport const softSkills: { id: SoftSkillEntryIdEnum; name: string }[] = [\n  {\n    id: SoftSkillEntryIdEnum.Responsible,\n    name: 'Responsible',\n  },\n  {\n    id: SoftSkillEntryIdEnum.TeamPlayer,\n    name: 'Good team player',\n  },\n  {\n    id: SoftSkillEntryIdEnum.Communicable,\n    name: 'Communicable',\n  },\n];\n\nexport const convertSoftSkillValueToEnum = (value: number | null) => {\n  switch (value) {\n    case 1:\n      return SoftSkillEntryValueEnum.Poor;\n    case 2:\n      return SoftSkillEntryValueEnum.Fair;\n    case 3:\n      return SoftSkillEntryValueEnum.Good;\n    case 4:\n      return SoftSkillEntryValueEnum.Great;\n    case 5:\n      return SoftSkillEntryValueEnum.Excellent;\n    default:\n      return SoftSkillEntryValueEnum.None;\n  }\n};\n"
  },
  {
    "path": "client/src/modules/Mentor/hooks/useMentorDashboard.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { MentorsApi } from '@client/api';\n\nconst service = new MentorsApi();\n\nexport function useMentorDashboard(mentorId: number | undefined, courseId: number) {\n  const { data, loading, run } = useRequest(async () => {\n    if (!mentorId) {\n      return [];\n    }\n    const { data = [] } = await service.getMentorDashboardData(mentorId, courseId);\n    return data;\n  });\n  return [data, loading, run] as const;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/hooks/useMentorStudents.tsx",
    "content": "import { message } from 'antd';\nimport { MentorsApi, MentorStudentDto } from '@client/api';\nimport { useMemo, useState, useEffect, useCallback } from 'react';\n\nexport function useMentorStudents(mentorId: number | null) {\n  const service = useMemo(() => new MentorsApi(), []);\n\n  const [students, setStudents] = useState<MentorStudentDto[]>([]);\n  const [loading, setLoading] = useState<boolean>(false);\n\n  const fetchStudents = useCallback(async () => {\n    if (mentorId) {\n      setLoading(true);\n      try {\n        const { data = [] } = await service.getMentorStudents(mentorId);\n        setStudents(data);\n      } catch {\n        message.error('Failed to fetch students');\n      } finally {\n        setLoading(false);\n      }\n    }\n  }, [mentorId, service]);\n\n  useEffect(() => {\n    fetchStudents();\n  }, [fetchStudents]);\n\n  return { students, loading, reload: fetchStudents };\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/InterviewWaitingList/index.tsx",
    "content": "import { Button, Table } from 'antd';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport {\n  getColumnSearchProps,\n  numberSorter,\n  stringSorter,\n  boolSorter,\n  boolIconRenderer,\n  PersonCell,\n  dateRenderer,\n} from '@client/shared/components/Table';\nimport { useLoading } from '@client/components/useLoading';\nimport { useMemo, useState, useContext } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { CoursePageProps } from '@client/services/models';\nimport { isCourseManager, isMentor } from '@client/domain/user';\nimport { AvailableStudentDto, CoursesInterviewsApi, InterviewDto, TaskDtoTypeEnum } from '@client/api';\nimport { getRating } from '@client/domain/interview';\nimport { CustomPopconfirm } from '@client/components/common/CustomPopconfirm';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useRequest } from 'ahooks';\nimport { useRouter } from 'next/router';\nimport dayjs from 'dayjs';\nimport { Rating } from '@client/shared/components/Rating';\n\nconst api = new CoursesInterviewsApi();\n\nexport type PageProps = CoursePageProps & { interview: InterviewDto };\n\nexport function InterviewWaitingList() {\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const courseId = course.id;\n  const router = useRouter();\n  const interviewId = Number(router.query.interviewId);\n  const isPowerUser = useMemo(() => isCourseManager(session, courseId), [session, courseId]);\n  const [loading, withLoading] = useLoading(false);\n  const [availableStudents, setAvailableStudents] = useState<AvailableStudentDto[]>([]);\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n\n  const { data: interview } = useRequest(async () => {\n    const { data } = await api.getInterview(interviewId, courseId);\n    const isStage = data.type === TaskDtoTypeEnum.StageInterview;\n    if (!isStage && dayjs(data.startDate).isAfter(dayjs())) {\n      router.push(`/403`);\n    }\n    return data;\n  });\n\n  const isStageInterview = interview?.type === TaskDtoTypeEnum.StageInterview;\n\n  useAsync(\n    withLoading(async () => {\n      const { data } = await api.getAvailableStudents(courseId, interviewId);\n      setAvailableStudents(data);\n    }),\n    [],\n  );\n\n  const inviteStudent = withLoading(async (githubId: string) => {\n    if (isStageInterview) {\n      await courseService.createInterview(githubId, session.githubId);\n    } else {\n      await courseService.addInterviewPair(`${interviewId}`, session.githubId, githubId);\n    }\n    removeStudentFromList(githubId);\n  });\n\n  const assignStudentToMentor = withLoading(async (studentId: string) => {\n    await courseService.updateStudent(studentId, { mentorGithuId: session.githubId });\n    removeStudentFromList(studentId);\n  });\n\n  const removeFromList = withLoading(async (githubId: string) => {\n    await courseService.updateMentoringAvailability(githubId, false);\n    removeStudentFromList(githubId);\n  });\n\n  return (\n    <PageLayout loading={loading} title={`${interview?.name.trim()}: Wait list`} showCourseName>\n      <Table\n        pagination={{ pageSize: 100 }}\n        size=\"small\"\n        dataSource={availableStudents}\n        rowKey=\"id\"\n        columns={[\n          {\n            title: 'GitHub',\n            dataIndex: 'githubId',\n            sorter: stringSorter('githubId'),\n            width: 180,\n            render: (_: string, record) => <PersonCell value={record} />,\n            ...getColumnSearchProps(['githubId', 'name']),\n          },\n          ...(isStageInterview\n            ? [\n                {\n                  title: 'Good Candidate',\n                  dataIndex: 'isGoodCandidate',\n                  width: 180,\n                  sorter: boolSorter('isGoodCandidate'),\n                  render: (value: boolean) => (value ? boolIconRenderer(value) : null),\n                },\n                {\n                  title: 'Interview Rating',\n                  dataIndex: 'rating',\n                  sorter: numberSorter('rating'),\n                  width: 210,\n                  render: (value: number, record: AvailableStudentDto) =>\n                    value != null ? (\n                      <Rating rating={getRating(value, record.maxScore, record.feedbackVersion)} />\n                    ) : null,\n                },\n              ]\n            : []),\n          {\n            title: 'City',\n            dataIndex: 'cityName',\n            sorter: stringSorter('cityName'),\n            width: 180,\n            ...getColumnSearchProps('cityName'),\n          },\n          {\n            title: 'Country',\n            dataIndex: 'countryName',\n            sorter: stringSorter('countryName'),\n            width: 180,\n            ...getColumnSearchProps('countryName'),\n          },\n          {\n            title: 'Score',\n            dataIndex: 'totalScore',\n            sorter: numberSorter('totalScore'),\n          },\n          {\n            title: 'Date registered',\n            width: 140,\n            dataIndex: 'registeredDate',\n            render: (registeredDate: string) => dateRenderer(registeredDate),\n          },\n          {\n            title: 'Actions',\n            dataIndex: 'actions',\n            align: 'right',\n            render: (_, record) => (\n              <>\n                <CustomPopconfirm\n                  title={\n                    <>\n                      Are you sure to interview <b>{record.githubId}</b>?\n                    </>\n                  }\n                  okText=\"Yes\"\n                  onConfirm={() => inviteStudent(record.githubId)}\n                >\n                  <Button type=\"link\">Want to interview</Button>\n                </CustomPopconfirm>\n                {isStageInterview && isMentor(session, courseId) && record.rating ? (\n                  <CustomPopconfirm\n                    title={\n                      <>\n                        Are you sure you want to assign <b>{record.githubId}</b> to yourself without an interview?\n                      </>\n                    }\n                    okText=\"Yes\"\n                    onConfirm={() => assignStudentToMentor(record.githubId)}\n                  >\n                    <Button type=\"link\">Assign student to me</Button>\n                  </CustomPopconfirm>\n                ) : null}\n                {isStageInterview && isPowerUser ? (\n                  <CustomPopconfirm\n                    title={<>Are you sure to remove {record.githubId} from the wait list?</>}\n                    okText=\"Yes\"\n                    onConfirm={() => removeFromList(record.githubId)}\n                  >\n                    <Button type=\"link\">Remove from list</Button>\n                  </CustomPopconfirm>\n                ) : null}\n              </>\n            ),\n          },\n        ]}\n      />\n    </PageLayout>\n  );\n\n  function removeStudentFromList(githubIdToRemove: string) {\n    setAvailableStudents(students => students.filter(({ githubId }) => githubId !== githubIdToRemove));\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/InterviewCard.module.css",
    "content": ".container {\n  margin-bottom: 16px;\n  cursor: default;\n}\n\n.container:last-child {\n  margin-bottom: 0;\n}\n\n.link {\n  padding-left: 0;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/InterviewCard.tsx",
    "content": "import { Button, Card } from 'antd';\nimport { InterviewPeriod } from '@client/domain/interview';\nimport styles from './InterviewCard.module.css';\nimport { InterviewDetails } from './InterviewDetails';\nimport { InterviewDto } from '@client/api';\nimport { Course } from '@client/services/models';\nimport { MentorInterview } from '@client/services/course';\n\nexport function InterviewCard(props: {\n  interviewTask: InterviewDto;\n  course: Course;\n  interviews: MentorInterview[];\n  fetchStudentInterviews: () => Promise<void>;\n}) {\n  const { interviewTask, course, interviews, fetchStudentInterviews } = props;\n  const { name, startDate, endDate, id, description, descriptionUrl } = interviewTask;\n\n  return (\n    <Card\n      hoverable\n      className={styles.container}\n      title={name}\n      extra={<InterviewPeriod startDate={startDate} endDate={endDate} />}\n      key={id}\n    >\n      {description && <p>{description}</p>}\n      <Button type=\"link\" href={descriptionUrl} target=\"_blank\" className={styles.link}>\n        Read more\n      </Button>\n      <InterviewDetails\n        interviewTask={interviewTask}\n        course={course}\n        interviews={interviews}\n        fetchStudentInterviews={fetchStudentInterviews}\n      />\n    </Card>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/InterviewDetails.tsx",
    "content": "import { InterviewDto } from '@client/api';\nimport { isInterviewRegistrationInProgress, isInterviewStarted } from '@client/domain/interview';\nimport { MentorInterview } from '@client/services/course';\nimport { Course } from '@client/services/models';\nimport { RegistrationNoticeAlert } from './RegistrationNoticeAlert';\nimport { InterviewsList } from './InterviewsList';\nimport { WaitListAlert } from './WaitListAlert';\n\nexport function InterviewDetails(props: {\n  interviewTask: InterviewDto;\n  course: Course;\n  interviews: MentorInterview[];\n  fetchStudentInterviews: () => Promise<void>;\n}) {\n  const { interviewTask, course, interviews, fetchStudentInterviews } = props;\n  const { startDate } = interviewTask;\n\n  const isRegistrationInProgress = isInterviewRegistrationInProgress(startDate);\n  const interviewStarted = isInterviewStarted(startDate);\n\n  return (\n    <>\n      {interviewStarted && (\n        <WaitListAlert interviewId={interviewTask.id} startDate={startDate} courseAlias={course.alias} />\n      )}\n      {isRegistrationInProgress && <RegistrationNoticeAlert interview={interviewTask} startDate={startDate} />}\n      {interviewStarted && (\n        <InterviewsList\n          fetchStudentInterviews={fetchStudentInterviews}\n          interviews={interviews}\n          course={course}\n          interviewTask={interviewTask}\n        />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/InterviewsList.module.css",
    "content": ".container {\n  margin: 15px 0;\n}\n\n.items {\n  margin-top: 15px;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/InterviewsList.tsx",
    "content": "import { Alert, Spin } from 'antd';\nimport InfoCircleTwoTone from '@ant-design/icons/InfoCircleTwoTone';\nimport { useState } from 'react';\nimport { MentorInterview } from '@client/services/course';\nimport { StudentInterview } from './StudentInterview';\nimport { InterviewsSummary } from './InterviewsSummary';\nimport { InterviewDto, TaskDtoTypeEnum } from '@client/api';\nimport { Course } from '@client/services/models';\nimport { useLoading } from '@client/components/useLoading';\nimport { useAsyncFn } from 'react-use';\nimport styles from './InterviewsList.module.css';\n\ntype StudentsListProps = {\n  interviews: MentorInterview[] | undefined;\n  course: Course;\n  interviewTask: InterviewDto;\n  fetchStudentInterviews: () => Promise<void>;\n};\n\nexport function InterviewsList(props: StudentsListProps) {\n  const { interviews = [], course, interviewTask, fetchStudentInterviews } = props;\n  const template = interviewTask.attributes?.template;\n  const [isExpanded, setIsExpanded] = useState(false);\n\n  const [loading, withLoading] = useLoading();\n  const [, reloadList] = useAsyncFn(withLoading(fetchStudentInterviews));\n\n  if (!interviews.length) {\n    return (\n      <Alert\n        icon={<InfoCircleTwoTone />}\n        showIcon\n        description=\"You don't have any assigned interviews yet.\"\n        type=\"info\"\n      />\n    );\n  }\n\n  return (\n    <Spin spinning={loading}>\n      <div className={styles.container}>\n        <InterviewsSummary\n          interviewTask={interviewTask}\n          interviews={interviews}\n          courseId={course.id}\n          toggleDetails={() => setIsExpanded(!isExpanded)}\n          reloadList={reloadList}\n          courseAlias={course.alias}\n        />\n        {isExpanded && (\n          <div className={styles.items}>\n            {interviews.map(studentInterview => (\n              <StudentInterview\n                key={studentInterview.student.githubId}\n                interview={studentInterview}\n                interviewTaskType={interviewTask.type as TaskDtoTypeEnum}\n                courseAlias={course.alias}\n                courseId={course.id}\n                template={template}\n              />\n            ))}\n          </div>\n        )}\n      </div>\n    </Spin>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/InterviewsSummary.tsx",
    "content": "import { Button, Row, Space, Typography } from 'antd';\nimport { UserSwitchOutlined, UserAddOutlined } from '@ant-design/icons';\nimport { useMemo, useState } from 'react';\nimport { MentorInterview } from '@client/services/course';\nimport { InterviewDto } from '@client/api';\nimport { getInterviewWaitList, isTechnicalScreening } from '@client/domain/interview';\nimport { SelectMentorModal } from './SelectMentorModal';\nimport { useAsyncFn } from 'react-use';\nimport { CourseService } from '@client/services/course';\n\nexport function InterviewsSummary({\n  interviews,\n  toggleDetails,\n  interviewTask,\n  courseId,\n  reloadList,\n  courseAlias,\n}: {\n  interviewTask: InterviewDto;\n  interviews: MentorInterview[];\n  toggleDetails: () => void;\n  courseId: number;\n  courseAlias: string;\n  reloadList: () => Promise<void>;\n}) {\n  const totalCompleted = useMemo(() => interviews.filter(interview => interview.completed).length, [interviews]);\n  const canTransfer = useMemo(\n    () => interviews.some(studentInterview => !studentInterview.completed && isTechnicalScreening(interviewTask.name)),\n    [interviewTask.name, interviews],\n  );\n  const [showTransfer, setShowTransfer] = useState(false);\n\n  const [, transfer] = useAsyncFn(async (githubId: string, interviewId: number) => {\n    await new CourseService(courseId).updateStageInterview(interviewId, { githubId });\n    await reloadList();\n    setShowTransfer(false);\n  }, []);\n\n  return (\n    <Row justify=\"space-between\">\n      <Row>\n        <Typography.Title level={4}>\n          Interviewed students {totalCompleted}({interviews.length})\n        </Typography.Title>{' '}\n        <Button type=\"link\" onClick={toggleDetails}>\n          Show details\n        </Button>\n      </Row>\n      <Space size={'small'}>\n        <Button icon={<UserAddOutlined />} href={getInterviewWaitList(courseAlias, interviewTask.id)}>\n          Add student\n        </Button>\n        {canTransfer && (\n          <Button icon={<UserSwitchOutlined />} onClick={() => setShowTransfer(true)}>\n            Transfer student\n          </Button>\n        )}\n      </Space>\n      {showTransfer && (\n        <SelectMentorModal\n          courseId={courseId}\n          interviews={interviews.filter(interview => !interview.completed)}\n          onCancel={() => setShowTransfer(false)}\n          onOk={transfer}\n        />\n      )}\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/MentorPreferencesModal.tsx",
    "content": "import { Form, Modal, Spin, Typography } from 'antd';\nimport { MentorOptions, Options } from '@client/components/MentorOptions';\nimport React, { createContext, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { MentorDetailsDtoStudentsPreferenceEnum, MentorsApi } from '@client/api';\nimport { getMentorId } from '@client/domain/user';\nimport { Session } from '@client/components/withSession';\n\ntype Props = {\n  course: { id: number; name: string };\n  session: Session;\n};\n\ntype MentorOptionsContextApi = {\n  showMentorOptions: () => void;\n};\n\nexport const MentorOptionsContext = createContext<MentorOptionsContextApi>({} as MentorOptionsContextApi);\n\nexport function MentorOptionsProvider({ children, course, session }: React.PropsWithChildren<Props>) {\n  const [showModal, setShowModal] = useState(false);\n  return (\n    <MentorOptionsContext.Provider\n      value={{\n        showMentorOptions: () => setShowModal(true),\n      }}\n    >\n      {children}\n      {showModal && <MentorOptionsModal session={session} course={course} close={() => setShowModal(false)} />}\n    </MentorOptionsContext.Provider>\n  );\n}\n\nfunction MentorOptionsModal({ course, close, session }: Props & { close: () => void }) {\n  const [options, setOptions] = useState<Options | null>(null);\n  const [form] = Form.useForm();\n\n  const mentorId = getMentorId(session, course.id);\n  const { loading } = useAsync(async () => {\n    if (!mentorId) {\n      return;\n    }\n    const { data: mentor } = await new MentorsApi().getMentorOptions(mentorId, course.id);\n    const { students, maxStudentsLimit, preferedStudentsLocation } = mentor;\n    setOptions({\n      maxStudentsLimit,\n      preferedStudentsLocation: preferedStudentsLocation as MentorDetailsDtoStudentsPreferenceEnum,\n      students: students.map(s => ({ value: s.id })),\n      preselectedStudents: students,\n    });\n  });\n\n  return (\n    <Spin spinning={loading}>\n      {!loading && (\n        <Modal\n          onCancel={close}\n          okText=\"Confirm\"\n          destroyOnClose\n          onOk={async () => {\n            const values = await form.validateFields();\n            if (values) {\n              await new CourseService(course.id).createMentor(session.githubId, {\n                maxStudentsLimit: values.maxStudentsLimit,\n                preferedStudentsLocation: values.preferedStudentsLocation,\n                students: values.students?.map((student: { value: number }) => Number(student.value)) ?? [],\n              });\n              close();\n            }\n          }}\n          title={\n            <>\n              Mentorship options: <Typography.Text type=\"secondary\">{course.name}</Typography.Text>\n            </>\n          }\n          open\n        >\n          <Typography.Text>\n            You have confirmed your desire to be a mentor in our course. Just in case you can change your preferences\n            below and select student which you want to mentor.\n          </Typography.Text>\n          <MentorOptions form={form} showSubmitButton={false} mentorData={options} course={course} />\n        </Modal>\n      )}\n    </Spin>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/RegistrationNotice.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { InterviewDto } from '@client/api';\nimport { RegistrationNoticeAlert } from './RegistrationNoticeAlert';\n\ndescribe('RegistrationNoticeAlert', () => {\n  beforeAll(() => vi.useFakeTimers().setSystemTime(new Date('2023-01-01')));\n\n  afterAll(() => vi.useRealTimers());\n\n  const interview: InterviewDto = {\n    id: 1,\n    startDate: '',\n    endDate: '',\n    description: '',\n    name: 'test course',\n    type: 'stage-interview',\n    descriptionUrl: '',\n    attributes: {},\n    studentRegistrationStartDate: new Date('2023-01-01').toISOString(),\n  };\n\n  it('should not render component if registration not yet started', () => {\n    render(<RegistrationNoticeAlert interview={interview} startDate=\"2023-02-01\" />);\n\n    expect(screen.queryByText('test course')).not.toBeInTheDocument();\n  });\n\n  it('should not render component if interview is not of stage type', () => {\n    render(<RegistrationNoticeAlert interview={{ ...interview, type: 'interview' }} startDate=\"2023-02-01\" />);\n\n    expect(screen.queryByText('test course')).not.toBeInTheDocument();\n  });\n\n  it('should render component if registration in progress', () => {\n    render(<RegistrationNoticeAlert interview={interview} startDate=\"2023-01-02\" />);\n\n    expect(screen.getByText('test course', { exact: false })).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/RegistrationNoticeAlert.module.css",
    "content": ".iconMentor {\n  background-image: url('https://cdn.rs.school/sloths/cleaned/mentor-new.svg');\n  background-position: center;\n  background-size: contain;\n  background-repeat: no-repeat;\n  width: 129px;\n  height: 170px;\n  margin: 10px auto;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/RegistrationNoticeAlert.tsx",
    "content": "import { Alert, Typography } from 'antd';\nimport InfoCircleTwoTone from '@ant-design/icons/InfoCircleTwoTone';\nimport dayjs from 'dayjs';\nimport { useContext } from 'react';\nimport { InterviewDto, TaskDtoTypeEnum } from '@client/api';\nimport { MentorOptionsContext } from './MentorPreferencesModal';\nimport { useAlert } from '../hooks/useAlert';\nimport styles from './RegistrationNoticeAlert.module.css';\n\nexport function RegistrationNoticeAlert(props: { interview: InterviewDto; startDate: string }) {\n  const { startDate, interview } = props;\n  const { showMentorOptions: openMentorOptions } = useContext(MentorOptionsContext);\n\n  const [isDismissed, setDismissed] = useAlert(`registration-notice-alert-${interview.id}`);\n\n  if (isDismissed) return null;\n\n  if (interview.type !== TaskDtoTypeEnum.StageInterview) {\n    return null;\n  }\n\n  return (\n    <>\n      <Alert\n        closable\n        message=\"Registration period\"\n        icon={<InfoCircleTwoTone />}\n        showIcon\n        onClose={() => setDismissed()}\n        description={\n          <>\n            <Typography.Text>\n              Students’ registration for {interview.name} continues until {dayjs(startDate).format('DD MMM hh:mm')}. You\n              can change <a onClick={showMentorOptions}>mentoring options</a> till this date.\n            </Typography.Text>\n            <div className={styles.iconMentor} />\n          </>\n        }\n        type=\"info\"\n      />\n    </>\n  );\n\n  function showMentorOptions(e: React.MouseEvent) {\n    e.stopPropagation();\n    openMentorOptions();\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/SelectMentorModal.tsx",
    "content": "import { Form, Row, Col, Select } from 'antd';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { MentorSearch } from '@client/shared/components/MentorSearch';\nimport { MentorInterview } from '@client/services/course';\n\ntype Props = {\n  interviews: MentorInterview[];\n  courseId: number;\n  onCancel: () => void;\n  onOk: (githubId: string, interviewId: number) => void;\n};\n\nexport function SelectMentorModal(props: Props) {\n  const { courseId, onCancel, onOk, interviews } = props;\n\n  return (\n    <ModalForm title=\"Mentor\" data={{}} submit={onSubmit} cancel={onCancel}>\n      <Row gutter={24}>\n        <Col span={24}>\n          <Form.Item name=\"interviewId\" rules={[{ required: true, message: 'Please select  student' }]} label=\"Student\">\n            <Select showSearch allowClear placeholder=\"'Select...\">\n              {interviews.map(({ id, student }) => {\n                return (\n                  <Select.Option key={id} value={id}>\n                    <GithubAvatar size={24} githubId={student.githubId} /> {student.name || student.githubId}\n                  </Select.Option>\n                );\n              })}\n            </Select>\n          </Form.Item>\n        </Col>\n      </Row>\n      <Row gutter={24}>\n        <Col span={24}>\n          <Form.Item name=\"githubId\" rules={[{ required: true, message: 'Please select  mentor' }]} label=\"Mentor\">\n            <MentorSearch keyField=\"githubId\" courseId={courseId} />\n          </Form.Item>\n        </Col>\n      </Row>\n    </ModalForm>\n  );\n\n  function onSubmit(values: { githubId: string; interviewId: number }) {\n    onOk(values.githubId, values.interviewId);\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/StudentInterview.module.css",
    "content": ".container {\n  border: 1px solid rgba(245, 245, 245, 1);\n  padding: 16px;\n  margin-bottom: 15px;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/StudentInterview.tsx",
    "content": "import { Col, Row, Button, Typography, Space, Popconfirm } from 'antd';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport GithubFilled from '@ant-design/icons/GithubFilled';\nimport { DecisionTag, getInterviewFeedbackUrl, InterviewStatus } from '@client/domain/interview';\nimport { CourseService, MentorInterview } from '@client/services/course';\nimport { useState } from 'react';\nimport styles from './StudentInterview.module.css';\nimport { CloseCircleOutlined } from '@ant-design/icons';\nimport { TaskDtoTypeEnum } from '@client/api';\nimport { useMessage } from '@client/hooks';\n\nexport function StudentInterview(props: {\n  interview: MentorInterview;\n  interviewTaskType: TaskDtoTypeEnum;\n  template?: string | null;\n  courseAlias: string;\n  courseId: number;\n}) {\n  const { message } = useMessage();\n  const { interview, interviewTaskType, template, courseAlias, courseId } = props;\n  const { student, completed } = interview;\n\n  const [isInterviewCompleted, setInterviewCompleted] = useState(completed);\n  const [popconfirmOpen, setPopconfirmOpen] = useState(false);\n\n  const courseService = new CourseService(courseId);\n  const isCoreJsInterview = interviewTaskType === TaskDtoTypeEnum.Interview;\n  const interviewStatus = isInterviewCompleted ? InterviewStatus.Completed : interview.status;\n\n  const interviewFeedbackUrl = getInterviewFeedbackUrl({\n    courseAlias,\n    interviewName: interview.name,\n    interviewId: interview.id,\n    studentGithubId: student.githubId,\n    studentId: student.id,\n    template: template,\n  });\n\n  const handleButtonClick = () => {\n    if (!isInterviewCompleted && isCoreJsInterview) {\n      setPopconfirmOpen(true);\n    } else {\n      window.location.href = interviewFeedbackUrl;\n    }\n  };\n\n  const popconfirmTitle = (\n    <div style={{ maxWidth: '270px', whiteSpace: 'pre-line' }}>\n      You can reject the interview with a result <strong>0</strong>, if student didn't connect with you, or for other\n      reasons.\n    </div>\n  );\n\n  const handlePopconfirmConfirm = async () => {\n    setPopconfirmOpen(false);\n    const score = 0;\n    const data = { score, comment: 'No Interview, Rejected' };\n    await courseService.postStudentInterviewResult(student.githubId, interview.id, data);\n\n    setInterviewCompleted(true);\n    message.success('You feedback with zero result has been submitted.');\n  };\n\n  const handleCancel = () => {\n    window.location.href = interviewFeedbackUrl;\n    setPopconfirmOpen(false);\n  };\n\n  return (\n    <Col className={styles.container}>\n      <Space size={21} direction=\"vertical\" style={{ width: '100%' }}>\n        <Row justify=\"space-between\" align=\"middle\">\n          <DecisionTag decision={interview.decision} status={interviewStatus} />\n          <Popconfirm\n            title={popconfirmTitle}\n            open={isCoreJsInterview && !isInterviewCompleted && popconfirmOpen}\n            onOpenChange={visible => setPopconfirmOpen(visible)}\n            onConfirm={handlePopconfirmConfirm}\n            onCancel={handleCancel}\n            okText=\"Reject\"\n            cancelText=\"Provide feedback\"\n            okButtonProps={{\n              danger: true,\n              icon: <CloseCircleOutlined />,\n            }}\n          >\n            <Button type=\"primary\" ghost size=\"small\" onClick={handleButtonClick}>\n              {isInterviewCompleted ? 'Edit feedback' : 'Provide feedback'}\n            </Button>\n          </Popconfirm>\n        </Row>\n        <Row>\n          <Space size={14} align=\"baseline\">\n            <GithubAvatar githubId={student.githubId} size={24} />\n            <Col>\n              <Typography.Title level={5}>\n                <Typography.Link target=\"_blank\" href={`/profile?githubId=${student.githubId}`}>\n                  {student.name || student.githubId}\n                </Typography.Link>\n              </Typography.Title>\n            </Col>\n            <Col>\n              <Typography.Link target=\"_blank\" href={`https://github.com/${student.githubId}`}>\n                <GithubFilled />\n              </Typography.Link>\n            </Col>\n          </Space>\n        </Row>\n      </Space>\n    </Col>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/WaitListAlert.module.css",
    "content": ".iconGroup {\n  background-image: url(/static/svg/sloths/students.svg);\n  background-position: center;\n  background-size: contain;\n  background-repeat: no-repeat;\n  width: 270px;\n  height: 160px;\n  margin: 10px auto;\n}\n\n.waitListAlert {\n  margin-bottom: 10px;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/components/WaitListAlert.tsx",
    "content": "import { Alert, theme, Typography } from 'antd';\nimport InfoCircleTwoTone from '@ant-design/icons/InfoCircleTwoTone';\nimport Link from 'next/link';\nimport { getInterviewWaitList } from '@client/domain/interview';\nimport { useAlert } from '../hooks/useAlert';\nimport styles from './WaitListAlert.module.css';\n\nexport function WaitListAlert({\n  courseAlias,\n  interviewId,\n}: {\n  courseAlias: string;\n  interviewId: number;\n  startDate: string;\n}) {\n  const { token } = theme.useToken();\n  const [isDismissed, setDismissed] = useAlert(`waitlist-alert-${interviewId}`);\n\n  if (isDismissed) return null;\n\n  return (\n    <div className={styles.waitListAlert}>\n      <Alert\n        closable\n        message=\"Do you want to interview more students?\"\n        icon={<InfoCircleTwoTone />}\n        onClose={() => setDismissed()}\n        showIcon\n        description={\n          <>\n            <Typography.Text onClick={e => e?.stopPropagation()}>\n              Excellent candidates are waiting for their mentor. Please check the{' '}\n              <Link\n                style={{ fontWeight: 'bold', color: token.blue7, letterSpacing: '0.1ch' }}\n                href={getInterviewWaitList(courseAlias, interviewId)}\n              >\n                students' waitlist.\n              </Link>\n            </Typography.Text>\n            <div className={styles.iconGroup} />\n          </>\n        }\n        type=\"info\"\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/hooks/useAlert.ts",
    "content": "import { useCallback } from 'react';\nimport { useSessionStorage } from 'react-use';\n\nexport function useAlert(key: string) {\n  const [isDismissed, setIsDismissed] = useSessionStorage(key, false);\n\n  const setDismissed = useCallback(() => setIsDismissed(true), [setIsDismissed]);\n\n  return [isDismissed, setDismissed] as const;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/index.module.css",
    "content": ".container {\n  display: flex;\n  margin: 0 auto;\n  width: 100%;\n  max-width: 1200px;\n  flex-direction: column;\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Interviews/index.tsx",
    "content": "import { CoursesInterviewsApi, InterviewDto, TaskDtoTypeEnum } from '@client/api';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { useLoading } from '@client/components/useLoading';\nimport { useCallback, useState, useContext } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService, MentorInterview } from '@client/services/course';\nimport { InterviewCard } from './components/InterviewCard';\nimport { MentorOptionsProvider } from './components/MentorPreferencesModal';\nimport groupBy from 'lodash/groupBy';\nimport type { Dictionary } from 'lodash';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport styles from './index.module.css';\n\nexport function Interviews() {\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const [interviews, setInterviews] = useState<InterviewDto[]>([]);\n  const [interviewsByTask, setInterviewsByTask] = useState<Dictionary<MentorInterview[]>>({});\n  const [loading, withLoading] = useLoading();\n\n  const fetchStudentInterviews = useCallback(async () => {\n    const interviews = await new CourseService(course.id).getMentorInterviews(session.githubId);\n    setInterviewsByTask(groupBy(interviews, 'name'));\n  }, [course.id, session.githubId]);\n\n  const loadData = async () => {\n    const [{ data }] = await Promise.all([\n      new CoursesInterviewsApi().getInterviews(course.id, false, [\n        TaskDtoTypeEnum.Interview,\n        TaskDtoTypeEnum.StageInterview,\n      ]),\n      fetchStudentInterviews(),\n    ]);\n\n    setInterviews(data);\n  };\n\n  useAsync(withLoading(loadData), []);\n\n  return (\n    <PageLayout loading={loading} title=\"Interviews\" showCourseName>\n      <MentorOptionsProvider course={course} session={session}>\n        <div className={styles.container}>\n          {interviews.map(interviewTask => (\n            <InterviewCard\n              interviewTask={interviewTask}\n              key={interviewTask.id}\n              course={course}\n              interviews={interviewsByTask[interviewTask.name] ?? []}\n              fetchStudentInterviews={fetchStudentInterviews}\n            />\n          ))}\n        </div>\n      </MentorOptionsProvider>\n    </PageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/StudentFeedback/index.tsx",
    "content": "import { PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { getMentorId } from '@client/domain/user';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useMentorStudents } from '@client/modules/Mentor/hooks/useMentorStudents';\nimport { useContext } from 'react';\nimport { CreateStudentFeedbackDto, StudentsFeedbacksApi } from '@client/api';\nimport { FeedbackForm } from '@client/modules/Feedback/components/FeedbackForm';\nimport { useRouter } from 'next/router';\nimport { useMessage } from '@client/hooks';\n\nconst api = new StudentsFeedbacksApi();\n\nexport function StudentFeedback() {\n  const { message } = useMessage();\n  const { course } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n  const { id: courseId } = course;\n  const mentorId = getMentorId(session, courseId);\n\n  const router = useRouter();\n  const studentId = router.query['studentId'] ? Number(router.query['studentId']) : null;\n\n  const { students, loading, reload } = useMentorStudents(mentorId);\n\n  const handleMentorSubmit = async (\n    studentId: number,\n    payload: CreateStudentFeedbackDto,\n    existingFeedbackId?: number,\n  ) => {\n    if (existingFeedbackId) {\n      try {\n        await api.updateStudentFeedback(studentId, existingFeedbackId, payload);\n        message.success('Feedback has been successfully updated');\n      } catch {\n        message.error('Failed to update feedback');\n      }\n    } else {\n      try {\n        await api.createStudentFeedback(studentId, payload);\n        message.success('Feedback has been successfully submitted');\n      } catch {\n        message.error('Failed to submit feedback');\n      }\n    }\n    await reload();\n  };\n\n  return (\n    <PageLayoutSimple noData={students?.length === 0} title=\"Recommendation Letter\" loading={loading}>\n      {studentId && <FeedbackForm students={students} studentId={studentId} onSubmit={handleMentorSubmit} />}\n    </PageLayoutSimple>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Mentor/pages/Students/index.tsx",
    "content": "import {\n  MessageOutlined,\n  MessageTwoTone,\n  InteractionTwoTone,\n  StarOutlined,\n  LockFilled,\n  TrophyOutlined,\n} from '@ant-design/icons';\nimport { Button, Card, Col, Empty, Row, Statistic, Typography } from 'antd';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { getMentorId } from '@client/domain/user';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useMentorStudents } from '@client/modules/Mentor/hooks/useMentorStudents';\nimport Link from 'next/link';\nimport { useContext } from 'react';\nimport * as routes from '@client/services/routes';\n\nexport function Students() {\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const { id: courseId, alias, completed } = course;\n  const mentorId = getMentorId(session, courseId);\n\n  const { students, loading } = useMentorStudents(mentorId);\n\n  return (\n    <PageLayoutSimple title=\"Your students\" loading={loading}>\n      {students?.length ? (\n        students.map(student => {\n          const [feedback] = student.feedbacks;\n          return (\n            <Card\n              key={student.githubId}\n              style={{ marginBottom: 32 }}\n              headStyle={{ border: 'none', paddingTop: 12 }}\n              size=\"small\"\n              title={\n                <>\n                  <div>\n                    <GithubUserLink value={student.githubId} />\n                  </div>\n                  <Link href={`/profile?githubId=${student.githubId}`}>{student.name}</Link>\n                  {student.repoUrl && (\n                    <div>\n                      <LockFilled />{' '}\n                      <a href={student.repoUrl} target=\"_blank\">\n                        {student.repoUrl.split('/').pop()}\n                      </a>\n                    </div>\n                  )}\n                </>\n              }\n              actions={[\n                <Link key=\"feedback\" href={routes.getStudentFeedbackRoute(alias, student.id)}>\n                  <Button type=\"link\" icon={completed ? <MessageTwoTone twoToneColor=\"red\" /> : <MessageOutlined />}>\n                    {feedback ? `Edit Feedback` : `Give Feedback`}\n                  </Button>\n                </Link>,\n                student.active ? (\n                  <Link key=\"expel\" href={routes.getExpelRoute(alias)} legacyBehavior>\n                    <Button type=\"link\" icon={<InteractionTwoTone twoToneColor=\"orange\" />}>\n                      Change Status\n                    </Button>\n                  </Link>\n                ) : null,\n              ].filter(Boolean)}\n              extra={\n                <Typography.Text ellipsis style={{ maxWidth: 160 }}>\n                  {student.cityName}, {student.countryName}\n                </Typography.Text>\n              }\n            >\n              <Row gutter={16}>\n                <Col flex={8}>\n                  <Statistic\n                    valueStyle={{ fontSize: 16 }}\n                    title=\"Rank\"\n                    value={student.rank}\n                    prefix={<TrophyOutlined />}\n                  />\n                </Col>\n                <Col flex={8}>\n                  <Statistic\n                    valueStyle={{ fontSize: 16 }}\n                    title=\"Score\"\n                    value={student.totalScore}\n                    prefix={<StarOutlined />}\n                  />\n                </Col>\n              </Row>\n            </Card>\n          );\n        })\n      ) : (\n        <div style={{ marginTop: 64 }}>\n          <Empty description={<span className=\"ant-empty-normal\">You do not have students</span>} />\n        </div>\n      )}\n    </PageLayoutSimple>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/components/InviteMentorsModal.tsx",
    "content": "import { Alert, Checkbox, Form, Select, Space, Spin } from 'antd';\nimport { useAsync } from 'react-use';\nimport { InviteMentorsDto } from '@client/api';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { useLoading } from '@client/components/useLoading';\nimport ReactQuill from 'react-quill';\nimport { MentorRegistryService } from '@client/services/mentorRegistry';\nimport { DisciplinesApi } from '@client/api';\n\nimport 'react-quill/dist/quill.snow.css';\nimport { useMessage } from '@client/hooks';\n\ntype Props = {\n  onCancel: () => void;\n};\nconst mentorRegistryService = new MentorRegistryService();\nconst disciplinesApi = new DisciplinesApi();\n\nfunction InviteMentorsModal({ onCancel }: Props) {\n  const { message } = useMessage();\n  const [loading, withLoading] = useLoading(false);\n  const submit = withLoading(async (data: InviteMentorsDto) => {\n    await mentorRegistryService.inviteMentors(data);\n    message.success('Invitation successfully send.');\n    onCancel();\n  });\n\n  const { loading: disciplinesLoading, value: disciplines = [] } = useAsync(async () => {\n    const { data } = await disciplinesApi.getDisciplines();\n    return data;\n  }, []);\n\n  return (\n    <ModalForm data={{}} title=\"Invite as a Mentor\" submit={submit} cancel={onCancel} loading={loading}>\n      <Space direction=\"vertical\" style={{ width: '100%' }}>\n        <Alert showIcon message=\"Invitation will be send to all mentors meeting the criteria below.\" type=\"info\" />\n        <Form.Item\n          name=\"disciplines\"\n          label=\"Disciplines\"\n          style={formItemStyle}\n          rules={[{ required: true, message: 'Please select disciplines.' }]}\n        >\n          <Select\n            mode=\"multiple\"\n            optionFilterProp=\"children\"\n            notFoundContent={disciplinesLoading ? <Spin size=\"small\" /> : null}\n          >\n            {disciplines.map(discipline => (\n              <Select.Option key={discipline.id} value={discipline.id}>\n                {discipline.name}\n              </Select.Option>\n            ))}\n          </Select>\n        </Form.Item>\n        <Form.Item name=\"isMentor\" style={formItemStyle} valuePropName=\"checked\">\n          <Checkbox>Mentor in the Past</Checkbox>\n        </Form.Item>\n        <Form.Item\n          name=\"text\"\n          label=\"Invitation Text\"\n          style={formItemStyle}\n          rules={[{ required: true, message: 'Please add invitation text.' }]}\n        >\n          <ReactQuill theme=\"snow\" placeholder=\"Write an invitation message\" />\n        </Form.Item>\n      </Space>\n    </ModalForm>\n  );\n}\n\nconst formItemStyle = { marginBottom: 0 };\n\nexport default InviteMentorsModal;\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/components/MentorRegistryDeleteModal.tsx",
    "content": "import { Col, Modal, Spin, Typography } from 'antd';\nimport { ExclamationCircleOutlined } from '@ant-design/icons';\n\ninterface MentorRegistryModalProps {\n  modalData: any;\n  modalLoading?: boolean;\n  onCancel: () => void;\n  cancelMentor: (githubId: string) => Promise<void>;\n}\n\nexport const MentorRegistryDeleteModal = (props: MentorRegistryModalProps) => {\n  const { modalData, modalLoading, cancelMentor, onCancel } = props;\n\n  return (\n    <Modal\n      width={420}\n      onOk={() => cancelMentor(modalData.record.githubId)}\n      open\n      onCancel={onCancel}\n      okText=\"Delete\"\n      okButtonProps={{ danger: true, type: 'primary' }}\n    >\n      <Spin spinning={modalLoading ?? false}>\n        <div style={{ display: 'flex' }}>\n          <ExclamationCircleOutlined style={{ fontSize: '32px', color: '#faad14', marginRight: 16 }} />\n          <Col>\n            <Typography.Title level={5}>Are you sure to delete this Mentor apply?</Typography.Title>\n            <Col>If you delete mentor's apply you can't restore it.</Col>\n          </Col>\n        </div>\n      </Spin>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/components/MentorRegistryResendModal.tsx",
    "content": "import { Col, Modal, Spin, Typography } from 'antd';\nimport { ExclamationCircleOutlined } from '@ant-design/icons';\nimport { MentorRegistryDto } from '@client/api';\n\ninterface MentorRegistryModalProps {\n  modalData: any;\n  modalLoading?: boolean;\n  resendConfirmation: (record: MentorRegistryDto) => void;\n  onCancel: () => void;\n}\n\nexport const MentorRegistryResendModal = (props: MentorRegistryModalProps) => {\n  const { modalData, modalLoading, resendConfirmation, onCancel } = props;\n\n  return (\n    <Modal width={420} onOk={() => resendConfirmation(modalData.record)} open onCancel={onCancel} okText=\"Re-send\">\n      <Spin spinning={modalLoading ?? false}>\n        <div style={{ display: 'flex' }}>\n          <ExclamationCircleOutlined style={{ fontSize: '32px', color: '#178df9', marginRight: 16 }} />\n          <Col>\n            <Typography.Title level={5}>Re-send Invitation for a Courses</Typography.Title>\n            <Col>Do you want resend invitation for a not accepted courses?</Col>\n          </Col>\n        </div>\n      </Spin>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/components/MentorRegistryTable.tsx",
    "content": "import { Dispatch, SetStateAction } from 'react';\nimport { Form, Table, TablePaginationConfig } from 'antd';\nimport { FilteredTags } from '@client/shared/components/FilteredTags';\nimport { FilterValue } from 'antd/lib/table/interface';\nimport { MentorRegistryDto } from '@client/api';\nimport { MentorRegistryTabsMode, MentorsRegistryColumnKey, PAGINATION } from '../constants';\nimport { ColumnType } from 'antd/lib/table';\n\ntype Props = {\n  setCurrentPage: Dispatch<SetStateAction<number>>;\n  total: Record<MentorRegistryTabsMode, number>;\n  currentPage: number;\n  tagFilters: string[];\n  filteredData: MentorRegistryDto[];\n  columns: ColumnType<MentorRegistryDto>[];\n  handleTagClose: (tag: string) => void;\n  handleClearAllButtonClick: () => void;\n  handleTableChange: (\n    _: TablePaginationConfig,\n    filters: Record<MentorsRegistryColumnKey, FilterValue | string[] | null>,\n  ) => void;\n  activeTab: MentorRegistryTabsMode;\n};\n\nexport function MentorRegistryTable(props: Props) {\n  const {\n    currentPage,\n    total,\n    tagFilters,\n    filteredData,\n    columns,\n    handleTagClose,\n    handleClearAllButtonClick,\n    handleTableChange,\n    setCurrentPage,\n    activeTab,\n  } = props;\n  const [form] = Form.useForm();\n\n  const tableWidth = 2000;\n  return (\n    <Form form={form} component={false}>\n      <FilteredTags\n        tagFilters={tagFilters}\n        onTagClose={handleTagClose}\n        onClearAllButtonClick={handleClearAllButtonClick}\n      />\n      <Table<MentorRegistryDto>\n        pagination={{ pageSize: PAGINATION, current: currentPage, onChange: setCurrentPage, total: total[activeTab] }}\n        rowKey=\"id\"\n        dataSource={filteredData}\n        scroll={{ x: tableWidth, y: 'calc(95vh - 340px)' }}\n        columns={columns}\n        onChange={handleTableChange}\n      />\n    </Form>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/components/MentorRegistryTableContainer.module.css",
    "content": ".infoIcons {\n  display: flex;\n  justify-content: center;\n}\n\n.infoIcons > div {\n  margin-right: 8px;\n}\n\n.iconCertificate :global(svg) {\n  width: 16px;\n  height: 16px;\n}\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/components/MentorRegistryTableContainer.tsx",
    "content": "import { Dispatch, SetStateAction } from 'react';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { SafetyCertificateTwoTone } from '@ant-design/icons';\nimport {\n  colorTagRenderer,\n  getColumnSearchProps,\n  stringSorter,\n  tagsRenderer,\n  dateSorter,\n} from '@client/shared/components/Table';\nimport { formatDate } from '@client/services/formatter';\nimport { Course } from '@client/services/models';\nimport CopyToClipboardButton from '@client/shared/components/CopyToClipboardButton';\nimport { MentorsRegistryColumnKey, MentorsRegistryColumnName, TABS, MentorRegistryTabsMode } from '../constants';\nimport { FilterValue } from 'antd/lib/table/interface';\nimport { Button, Dropdown, Tooltip, message, theme } from 'antd';\nimport { MoreOutlined, MessageTwoTone } from '@ant-design/icons';\nimport { ColumnType } from 'antd/lib/table';\nimport { DisciplineDto, MentorRegistryDto } from '@client/api';\nimport { ModalDataMode } from '@client/pages/admin/mentor-registry';\nimport { PublicSvgIcon } from '@client/shared/components/Icons';\nimport styles from './MentorRegistryTableContainer.module.css';\n\ninterface ChildrenProp {\n  setCurrentPage: Dispatch<SetStateAction<number>>;\n  currentPage: number;\n  total: Record<MentorRegistryTabsMode, number>;\n  tagFilters: string[];\n  filteredData: MentorRegistryDto[];\n  columns: ColumnType<MentorRegistryDto>[];\n  handleTagClose: (tag: string) => void;\n  handleClearAllButtonClick: () => void;\n  handleTableChange: (_: any, filters: Record<MentorsRegistryColumnKey, FilterValue | string[] | null>) => void;\n  activeTab: MentorRegistryTabsMode;\n}\n\ninterface Props {\n  mentors: MentorRegistryDto[];\n  courses: Course[];\n  activeTab: MentorRegistryTabsMode;\n  handleModalDataChange: (mode: ModalDataMode, record: MentorRegistryDto) => void;\n  children: (props: ChildrenProp) => JSX.Element;\n  disciplines: DisciplineDto[];\n  tagFilters: string[];\n  setTagFilters: Dispatch<SetStateAction<string[]>>;\n  combinedFilter: CombinedFilter;\n  setCombinedFilter: Dispatch<SetStateAction<CombinedFilter>>;\n  setCurrentPage: Dispatch<SetStateAction<number>>;\n  currentPage: number;\n  total: Record<MentorRegistryTabsMode, number>;\n}\n\nexport interface CombinedFilter {\n  preselectedCourses: number[];\n  preferredCourses: number[];\n  technicalMentoring: string[];\n  githubId: string[];\n  cityName: string[];\n  filterTags?: string[];\n  status: MentorRegistryTabsMode;\n}\n\nexport const MentorRegistryTableContainer = ({\n  children,\n  mentors,\n  courses,\n  activeTab,\n  handleModalDataChange,\n  disciplines,\n  tagFilters,\n  setTagFilters,\n  combinedFilter,\n  setCombinedFilter,\n  setCurrentPage,\n  currentPage,\n  total,\n}: Props) => {\n  const { token } = theme.useToken();\n\n  const renderPreselectedCourses = (courses: Course[]) => {\n    return (values: number[], record: MentorRegistryDto) => {\n      return values\n        .map(value => ({\n          value: courses.find(course => course.id === value)?.name ?? value.toString(),\n          alias: courses.find(course => course.id === value)?.alias ?? '',\n          color: record.courses.includes(value) ? '#87d068' : undefined,\n        }))\n        .map(v => (v.color ? colorTagRenderer(v.value, v.color) : renderTagWithCopyButton(v.value, v.alias)));\n    };\n  };\n\n  const renderTagWithCopyButton = (value: string, alias: string) => {\n    const link = `${window.location.origin}/course/mentor/confirm?course=${alias}`;\n    return (\n      <>\n        {colorTagRenderer(value)} <CopyToClipboardButton value={link} type=\"link\" />\n      </>\n    );\n  };\n\n  const renderInfo = (_: any, record: MentorRegistryDto) => {\n    const isMentor = record.courses.some(id => !record.preselectedCourses.includes(id));\n    return (\n      <div className={styles.infoIcons}>\n        {isMentor ? (\n          <div title=\"Mentor in the past\" style={{ color: token.colorTextSecondary }}>\n            <PublicSvgIcon src=\"/static/svg/master-yoda.svg\" size=\"1rem\" />\n          </div>\n        ) : null}\n        {record.comment && (\n          <Tooltip placement=\"top\" title={record.comment}>\n            <MessageTwoTone />\n          </Tooltip>\n        )}\n        {record.hasCertificate ? (\n          <SafetyCertificateTwoTone\n            title=\"Completed with certificate\"\n            className={styles.iconCertificate}\n            twoToneColor=\"#52c41a\"\n          />\n        ) : null}\n      </div>\n    );\n  };\n\n  const handleTableChange = async (\n    _: any,\n    filters: Record<MentorsRegistryColumnKey, FilterValue | string[] | null>,\n  ) => {\n    const combinedFilter: CombinedFilter = {\n      preferredCourses: filters.preferedCourses?.map(course => Number(course)) ?? [],\n      preselectedCourses: filters.preselectedCourses?.map(course => Number(course)) ?? [],\n      technicalMentoring: filters.technicalMentoring?.map(discipline => discipline.toString()) ?? [],\n      githubId: filters.githubId?.map(discipline => discipline.toString()) ?? [],\n      cityName: filters.cityName?.map(discipline => discipline.toString()) ?? [],\n      status: activeTab,\n    };\n\n    const filterTag: string[] = [\n      ...combinedFilter.technicalMentoring.map(discipline => `${MentorsRegistryColumnName.Tech}: ${discipline}`),\n      ...combinedFilter.preselectedCourses.map(\n        preselectedCourse =>\n          `${MentorsRegistryColumnName.Preselected}: ${courses.find(course => course.id === preselectedCourse)?.name}`,\n      ),\n      ...combinedFilter.preferredCourses.map(\n        preferredCourse =>\n          `${MentorsRegistryColumnName.PreferredCourses}: ${\n            courses.find(course => course.id === preferredCourse)?.name\n          }`,\n      ),\n    ];\n\n    setTagFilters(filterTag);\n    setCombinedFilter(combinedFilter);\n  };\n\n  const renderRestActions = (record: MentorRegistryDto) => {\n    return (\n      <Dropdown\n        menu={{\n          items: [\n            activeTab === MentorRegistryTabsMode.New\n              ? {\n                  key: 'resend',\n                  label: 'Re-send',\n                  onClick: () => handleModalDataChange(ModalDataMode.Resend, record),\n                  disabled: !record.preselectedCourses.length,\n                }\n              : null,\n            {\n              key: 'delete',\n              label: 'Delete',\n              onClick: () => handleModalDataChange(ModalDataMode.Delete, record),\n            },\n            {\n              key: 'comment',\n              label: record.comment ? 'Edit comment' : 'Add comment',\n              onClick: () => handleModalDataChange(ModalDataMode.Comment, record),\n            },\n          ],\n        }}\n      >\n        <Button type=\"link\">\n          <MoreOutlined />\n        </Button>\n      </Dropdown>\n    );\n  };\n\n  const getColumns = (combinedFilter: CombinedFilter, allCourses: Course[]): ColumnType<MentorRegistryDto>[] => {\n    const { preferredCourses, preselectedCourses, technicalMentoring, githubId, cityName } = combinedFilter;\n    const allColumns = [\n      {\n        key: MentorsRegistryColumnKey.GitHub,\n        title: MentorsRegistryColumnName.GitHub,\n        dataIndex: MentorsRegistryColumnKey.GitHub,\n        render: (value: string, { name }: { name: string }) => {\n          return (\n            <>\n              <GithubUserLink value={value} />\n              <div>{name}</div>\n            </>\n          );\n        },\n        sorter: stringSorter('githubId'),\n        ...getColumnSearchProps(['githubId', 'name']),\n        onFilter: undefined,\n        width: 200,\n        fixed: 'left' as const,\n        filteredValue: githubId || null,\n      },\n      {\n        key: MentorsRegistryColumnKey.Info,\n        title: MentorsRegistryColumnName.Info,\n        dataIndex: MentorsRegistryColumnKey.Info,\n        render: renderInfo,\n        width: 100,\n        filteredValue: null,\n      },\n      {\n        key: MentorsRegistryColumnKey.PreferredCourses,\n        title: MentorsRegistryColumnName.PreferredCourses,\n        dataIndex: MentorsRegistryColumnKey.PreferredCourses,\n        render: (courses: number[]) =>\n          tagsRenderer(\n            courses.map(course => allCourses.find(preferredCourse => preferredCourse.id === course)?.name ?? course),\n          ),\n        filters: allCourses.map(status => ({ text: status.name, value: status.id })),\n        defaultFilteredValue: preferredCourses,\n        filtered: preferredCourses?.length > 0,\n        filteredValue: preferredCourses || null,\n        width: 240,\n      },\n      {\n        key: MentorsRegistryColumnKey.ReceivedDate,\n        title: MentorsRegistryColumnName.ReceivedDate,\n        dataIndex: MentorsRegistryColumnKey.ReceivedDate,\n        render: (date: string) => formatDate(date),\n        sorter: dateSorter('receivedDate'),\n        width: 120,\n        filteredValue: null,\n      },\n      {\n        key: MentorsRegistryColumnKey.Preselected,\n        title: MentorsRegistryColumnName.Preselected,\n        dataIndex: MentorsRegistryColumnKey.Preselected,\n        render: renderPreselectedCourses(allCourses),\n        filters: allCourses?.map(status => ({ text: status.name, value: status.id })),\n        defaultFilteredValue: preselectedCourses,\n        filtered: preselectedCourses?.length > 0,\n        filteredValue: preselectedCourses || null,\n        width: 260,\n      },\n      {\n        key: MentorsRegistryColumnKey.SendDate,\n        title: MentorsRegistryColumnName.SendDate,\n        dataIndex: MentorsRegistryColumnKey.SendDate,\n        render: (date: string) => formatDate(date),\n        sorter: dateSorter('sendDate'),\n        width: 120,\n        filteredValue: null,\n      },\n      {\n        key: MentorsRegistryColumnKey.Tech,\n        title: MentorsRegistryColumnName.Tech,\n        dataIndex: MentorsRegistryColumnKey.Tech,\n        render: tagsRenderer,\n        filters: disciplines.map(discipline => {\n          return { text: discipline.name, value: discipline.name };\n        }),\n        defaultFilteredValue: technicalMentoring,\n        filtered: technicalMentoring?.length > 0,\n        filteredValue: technicalMentoring || null,\n        width: 240,\n      },\n      {\n        key: MentorsRegistryColumnKey.City,\n        title: MentorsRegistryColumnName.City,\n        dataIndex: MentorsRegistryColumnKey.City,\n        sorter: stringSorter('cityName'),\n        width: 150,\n        ...getColumnSearchProps('cityName'),\n        onFilter: undefined,\n        filteredValue: cityName || null,\n      },\n      {\n        key: MentorsRegistryColumnKey.Languages,\n        title: MentorsRegistryColumnName.Languages,\n        dataIndex: MentorsRegistryColumnKey.Languages,\n        render: tagsRenderer,\n        width: 130,\n        filteredValue: null,\n      },\n      {\n        key: MentorsRegistryColumnKey.StudentsLimit,\n        title: MentorsRegistryColumnName.StudentsLimit,\n        dataIndex: MentorsRegistryColumnKey.StudentsLimit,\n        width: 130,\n        filteredValue: null,\n      },\n      {\n        key: MentorsRegistryColumnKey.PreferredLocation,\n        title: MentorsRegistryColumnName.PreferredLocation,\n        dataIndex: MentorsRegistryColumnKey.PreferredLocation,\n        sorter: stringSorter('githubId'),\n        filteredValue: null,\n      },\n      {\n        key: MentorsRegistryColumnKey.Actions,\n        title: MentorsRegistryColumnName.Actions,\n        dataIndex: MentorsRegistryColumnKey.Actions,\n        render: (_: any, record: MentorRegistryDto) => (\n          <>\n            <Button type=\"link\" size=\"small\" onClick={() => handleModalDataChange(ModalDataMode.Invite, record)}>\n              Invite\n            </Button>\n            {renderRestActions(record)}\n          </>\n        ),\n        width: 140,\n        fixed: 'right' as const,\n      },\n    ];\n\n    return allColumns.filter(column => TABS[activeTab].find(tab => tab === column.dataIndex));\n  };\n\n  const handleTagClose = (removedTag: string) => {\n    const [removedTagName, removedTagValue] = removedTag.split(':');\n    if (!removedTagValue) return;\n\n    switch (removedTagName) {\n      case MentorsRegistryColumnName.Tech:\n        {\n          setCombinedFilter(prevState => ({\n            ...prevState,\n            technicalMentoring: combinedFilter.technicalMentoring.filter(tag => tag !== removedTagValue.trim()),\n          }));\n        }\n        break;\n      case MentorsRegistryColumnName.PreferredCourses:\n        {\n          setCombinedFilter(prevState => ({\n            ...prevState,\n            preferredCourses: combinedFilter.preferredCourses.filter(\n              tag => courses.find(course => course.id === tag)?.name !== removedTagValue.trim(),\n            ),\n          }));\n        }\n        break;\n      case MentorsRegistryColumnName.Preselected:\n        {\n          setCombinedFilter(prevState => ({\n            ...prevState,\n            preselectedCourses: combinedFilter.preselectedCourses.filter(\n              tag => courses.find(course => course.id === tag)?.name !== removedTagValue.trim(),\n            ),\n          }));\n        }\n        break;\n      default:\n        message.error('An error occurred. Please try again later.');\n        break;\n    }\n    setTagFilters(tagFilters.filter(filter => filter !== removedTag));\n  };\n\n  const handleClearAllButtonClick = () => {\n    setCombinedFilter(prev => ({\n      preferredCourses: [],\n      technicalMentoring: [],\n      preselectedCourses: [],\n      githubId: [],\n      cityName: [],\n      status: prev.status,\n    }));\n    setTagFilters([]);\n  };\n\n  return children({\n    tagFilters,\n    filteredData: mentors,\n    columns: getColumns(combinedFilter, courses),\n    currentPage,\n    total,\n    handleTagClose,\n    handleClearAllButtonClick,\n    handleTableChange,\n    setCurrentPage,\n    activeTab,\n  });\n};\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/constants.ts",
    "content": "export const PAGINATION = 100;\n\nexport enum MentorsRegistryColumnKey {\n  GitHub = 'githubId',\n  Info = 'info',\n  PreferredCourses = 'preferedCourses',\n  ReceivedDate = 'receivedDate',\n  Preselected = 'preselectedCourses',\n  SendDate = 'sendDate',\n  Tech = 'technicalMentoring',\n  Languages = 'languagesMentoring',\n  StudentsLimit = 'maxStudentsLimit',\n  City = 'cityName',\n  PreferredLocation = 'preferedStudentsLocation',\n  Actions = 'actions',\n}\n\nexport enum MentorsRegistryColumnName {\n  GitHub = 'GitHub',\n  Info = 'Additional',\n  PreferredCourses = 'Preferred',\n  ReceivedDate = 'Received',\n  Preselected = 'Pre-Selected',\n  SendDate = 'Send',\n  Tech = 'Technologies',\n  Languages = 'Languages',\n  StudentsLimit = 'Max students',\n  City = 'City',\n  PreferredLocation = 'Students Location',\n  Actions = 'Actions',\n}\n\nexport enum MentorRegistryTabsMode {\n  New = 'new',\n  All = 'all',\n}\n\nexport const TABS = {\n  [MentorRegistryTabsMode.New]: [\n    MentorsRegistryColumnKey.GitHub,\n    MentorsRegistryColumnKey.Info,\n    MentorsRegistryColumnKey.PreferredCourses,\n    MentorsRegistryColumnKey.Preselected,\n    MentorsRegistryColumnKey.ReceivedDate,\n    MentorsRegistryColumnKey.SendDate,\n    MentorsRegistryColumnKey.Tech,\n    MentorsRegistryColumnKey.City,\n    MentorsRegistryColumnKey.Languages,\n    MentorsRegistryColumnKey.StudentsLimit,\n    MentorsRegistryColumnKey.Actions,\n  ],\n  [MentorRegistryTabsMode.All]: [\n    MentorsRegistryColumnKey.GitHub,\n    MentorsRegistryColumnKey.Info,\n    MentorsRegistryColumnKey.PreferredCourses,\n    MentorsRegistryColumnKey.Preselected,\n    MentorsRegistryColumnKey.Tech,\n    MentorsRegistryColumnKey.City,\n    MentorsRegistryColumnKey.Languages,\n    MentorsRegistryColumnKey.StudentsLimit,\n    MentorsRegistryColumnKey.PreferredLocation,\n    MentorsRegistryColumnKey.Actions,\n  ],\n};\n"
  },
  {
    "path": "client/src/modules/MentorRegistry/index.ts",
    "content": "export * from './components/MentorRegistryTable';\nexport * from './components/MentorRegistryTableContainer';\nexport * from './components/MentorRegistryDeleteModal';\nexport * from './components/MentorRegistryResendModal';\nexport * from './constants';\n"
  },
  {
    "path": "client/src/modules/MentorTasksReview/components/AssignReviewerModal/AssignReviewerModal.tsx",
    "content": "import { Col, Form, message, Row, Typography } from 'antd';\nimport { useState } from 'react';\nimport { ModalSubmitForm } from '@client/shared/components/Forms';\nimport { MentorReviewAssignDto, MentorReviewDto, MentorReviewsApi } from '@client/api';\nimport isEmpty from 'lodash/isEmpty';\nimport { MentorSearch } from '@client/shared/components/MentorSearch';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport useRequest from 'ahooks/lib/useRequest';\n\nconst mentorReviewsApi = new MentorReviewsApi();\n\nexport interface AssignReviewerModalProps {\n  review: MentorReviewDto | null;\n  onClose: () => void;\n  onSubmit: () => void;\n}\n\nconst { Link } = Typography;\n\nconst MODAL_TITLE = 'Assign Reviewer for';\nconst SUCCESS_MESSAGE = 'Reviewer has been successfully assigned';\n\nfunction AssignReviewerModal({ review, onClose, onSubmit }: AssignReviewerModalProps) {\n  const { course } = useActiveCourseContext();\n\n  const [submitted, setSubmitted] = useState(false);\n  const [errorText, setErrorText] = useState('');\n\n  const courseId = course.id;\n  const { solutionUrl, taskDescriptionUrl, taskName, student, taskId, studentId } = review || {};\n\n  const { runAsync: assignReviewer, loading } = useRequest(mentorReviewsApi.assignReviewer, {\n    manual: true,\n    onError: () => message.error('An unexpected error occurred. Please try later.'),\n  });\n\n  const handleSubmit = async (values: { mentorId?: number }) => {\n    const { mentorId } = values;\n    try {\n      const payload: MentorReviewAssignDto = { courseTaskId: taskId!, studentId: studentId!, mentorId };\n      await assignReviewer(course.id, payload);\n      setSubmitted(true);\n      onSubmit();\n    } catch (e: any) {\n      const error = e.response?.data?.message ?? e.message;\n      setErrorText(error);\n    }\n  };\n\n  const handleClose = () => {\n    setErrorText('');\n    setSubmitted(false);\n    onClose();\n  };\n\n  return (\n    <ModalSubmitForm\n      title={`${MODAL_TITLE} ${student}`}\n      data={review}\n      submit={handleSubmit}\n      close={handleClose}\n      errorText={errorText}\n      loading={loading}\n      submitted={submitted}\n      successText={SUCCESS_MESSAGE}\n      open={!isEmpty(review)}\n    >\n      <Row>\n        <Col span={18} offset={3}>\n          <Form.Item label=\"Task Name\">\n            <Link href={taskDescriptionUrl} target=\"_blank\">\n              {taskName}\n            </Link>\n          </Form.Item>\n          <Form.Item label=\"Submitted Link\">\n            <Link href={solutionUrl} target=\"_blank\">\n              {solutionUrl}\n            </Link>\n          </Form.Item>\n          <Form.Item name=\"mentorId\" label=\"Mentor\">\n            <MentorSearch keyField=\"id\" courseId={courseId} allowClear />\n          </Form.Item>\n        </Col>\n      </Row>\n    </ModalSubmitForm>\n  );\n}\n\nexport default AssignReviewerModal;\n"
  },
  {
    "path": "client/src/modules/MentorTasksReview/components/AssignReviewerModal/index.ts",
    "content": "export { default } from './AssignReviewerModal';\n"
  },
  {
    "path": "client/src/modules/MentorTasksReview/components/ReviewsTable/index.tsx",
    "content": "import { Table, TablePaginationConfig, TableProps } from 'antd';\nimport { CourseTaskDto, MentorReviewDto } from '@client/api';\nimport { getColumns } from './renderers';\nimport AssignReviewerModal from '../AssignReviewerModal';\nimport { useState } from 'react';\n\ntype Props = {\n  content: MentorReviewDto[];\n  pagination: false | TablePaginationConfig;\n  handleChange?: TableProps<MentorReviewDto>['onChange'];\n  handleReviewerAssigned: () => void;\n  loading?: boolean;\n  tasks: CourseTaskDto[];\n  isManager: boolean;\n};\n\nexport default function MentorReviewsTable({\n  content,\n  pagination,\n  handleChange,\n  handleReviewerAssigned,\n  loading,\n  tasks,\n  isManager,\n}: Props) {\n  const [modalData, setModalData] = useState<MentorReviewDto | null>(null);\n\n  const handleClick = (review: MentorReviewDto) => setModalData(review);\n  const handleClose = () => setModalData(null);\n\n  return (\n    <>\n      <Table<MentorReviewDto>\n        showHeader\n        dataSource={content}\n        size=\"small\"\n        columns={getColumns(tasks, handleClick, isManager)}\n        onChange={handleChange}\n        rowKey=\"id\"\n        pagination={pagination}\n        loading={loading}\n      />\n      <AssignReviewerModal review={modalData} onClose={handleClose} onSubmit={handleReviewerAssigned} />\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/MentorTasksReview/components/ReviewsTable/renderers.tsx",
    "content": "import Button from 'antd/lib/button';\nimport { ColumnsType } from 'antd/lib/table';\nimport { CourseTaskDto, MentorReviewDto } from '@client/api';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport {\n  dateTimeRenderer,\n  getColumnSearchProps,\n  renderTask,\n  stringTrimRenderer,\n} from '@client/shared/components/Table';\n\nconst getSearchProps = (key: string) => ({\n  ...getColumnSearchProps(key),\n  onFilter: undefined,\n});\n\nexport enum ColumnKey {\n  TaskName = 'taskName',\n  Student = 'student',\n  SubmittedDate = 'submittedAt',\n  SubmittedLink = 'solutionUrl',\n  Checker = 'checker',\n  ReviewedDate = 'reviewedAt',\n  Score = 'score',\n  Actions = 'actions',\n}\n\nenum ColumnName {\n  TaskName = 'Task Name',\n  Student = 'Student',\n  SubmittedDate = 'Submitted Date',\n  SubmittedLink = 'Submitted Link',\n  Checker = 'Checker',\n  ReviewedDate = 'Reviewed Date',\n  Score = 'Score',\n  Actions = 'Actions',\n}\n\nexport const getColumns = (\n  tasks: CourseTaskDto[],\n  handleClick: (review: MentorReviewDto) => void,\n  isManager: boolean,\n): ColumnsType<MentorReviewDto> => {\n  const columns: ColumnsType<MentorReviewDto> = [\n    {\n      key: ColumnKey.TaskName,\n      title: ColumnName.TaskName,\n      dataIndex: ColumnKey.TaskName,\n      width: '15%',\n      render: (taskName, review) => renderTask(taskName, review.taskDescriptionUrl),\n      filters: tasks.map(task => ({ text: task.name, value: task.id })),\n    },\n    {\n      key: ColumnKey.Student,\n      title: ColumnName.Student,\n      dataIndex: ColumnKey.Student,\n      width: '12.5%',\n      render: (_v, review) => <GithubUserLink value={review.student} />,\n      ...getSearchProps(ColumnKey.Student),\n    },\n    {\n      key: ColumnKey.SubmittedDate,\n      title: ColumnName.SubmittedDate,\n      dataIndex: ColumnKey.SubmittedDate,\n      width: '12.5%',\n      sorter: true,\n      render: (_v, review) => dateTimeRenderer(review.submittedAt),\n    },\n    {\n      key: ColumnKey.SubmittedLink,\n      title: ColumnName.SubmittedLink,\n      dataIndex: ColumnKey.SubmittedLink,\n      width: '12.5%',\n      render: solutionUrl => (\n        <a target=\"_blank\" href={solutionUrl}>\n          {stringTrimRenderer(solutionUrl)}\n        </a>\n      ),\n    },\n    {\n      key: ColumnKey.Checker,\n      title: ColumnName.Checker,\n      dataIndex: ColumnKey.Checker,\n      width: '12.5%',\n      render: checker => (checker ? <GithubUserLink value={checker} /> : null),\n      ...getSearchProps(ColumnKey.Checker),\n    },\n    {\n      key: ColumnKey.ReviewedDate,\n      title: ColumnName.ReviewedDate,\n      dataIndex: ColumnKey.ReviewedDate,\n      width: '12.5%',\n      sorter: true,\n      render: (_v, review) => dateTimeRenderer(review.reviewedAt),\n    },\n    {\n      align: 'right',\n      key: ColumnKey.Score,\n      title: ColumnName.Score,\n      dataIndex: ColumnKey.Score,\n      width: '10%',\n      render: (_v, review) => (\n        <>\n          {review.score ?? 0} / {review.maxScore}\n        </>\n      ),\n    },\n    {\n      align: 'center',\n      key: ColumnKey.Actions,\n      title: ColumnName.Actions,\n      dataIndex: ColumnKey.Actions,\n      width: '12.5%',\n      render: (_v, review) => (\n        <Button type=\"link\" onClick={() => handleClick(review)} disabled={!!review.score}>\n          Assign Reviewer\n        </Button>\n      ),\n    },\n  ];\n\n  return isManager ? columns : columns.filter(column => column.key !== ColumnKey.Actions);\n};\n"
  },
  {
    "path": "client/src/modules/MentorTasksReview/pages/MentorTasksReview.constants.ts",
    "content": "export const sortDirectionMap = {\n  ascend: 'ASC',\n  descend: 'DESC',\n};\n"
  },
  {
    "path": "client/src/modules/MentorTasksReview/pages/MentorTasksReview.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { message, Space, TablePaginationConfig, Typography } from 'antd';\nimport { FilterValue } from 'antd/es/table/interface';\nimport { SorterResult } from 'antd/lib/table/interface';\nimport { CoursesTasksApi, CourseTaskDtoCheckerEnum, MentorReviewDto, MentorReviewsApi } from '@client/api';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { useLoading } from '@client/components/useLoading';\nimport { isCourseManager } from '@client/domain/user';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useContext, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport MentorReviewsTable from '../components/ReviewsTable';\nimport { ColumnKey } from '../components/ReviewsTable/renderers';\nimport { sortDirectionMap } from './MentorTasksReview.constants';\n\nconst { Text } = Typography;\n\nconst mentorReviewsApi = new MentorReviewsApi();\nconst coursesTasksApi = new CoursesTasksApi();\n\ntype ReviewsState = {\n  content: MentorReviewDto[];\n  pagination: IPaginationInfo;\n};\n\nexport const MentorTasksReview = () => {\n  const { courses, course } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n\n  const { data: tasks } = useRequest(async () => {\n    const { data } = await coursesTasksApi.getCourseTasks(course.id, undefined, CourseTaskDtoCheckerEnum.Mentor);\n    return data;\n  });\n\n  const isManager = useMemo(() => isCourseManager(session, course.id), [session, course.id]);\n\n  const [reviews, setReviews] = useState<ReviewsState>({\n    content: [],\n    pagination: { current: 1, pageSize: 20 },\n  });\n  const [loading, withLoading] = useLoading(false);\n\n  const getMentorReviews = withLoading(\n    async (\n      pagination: TablePaginationConfig,\n      filters?: Record<ColumnKey, FilterValue | null>,\n      sorter?: SorterResult<MentorReviewDto> | SorterResult<MentorReviewDto>[],\n    ) => {\n      const sortValues =\n        sorter && !Array.isArray(sorter) && sorter.order\n          ? [sorter.field?.toString(), sortDirectionMap[sorter.order]]\n          : [undefined, undefined];\n\n      try {\n        const { data } = await mentorReviewsApi.getMentorReviews(\n          String(pagination.current),\n          String(pagination.pageSize),\n          course.id,\n          filters?.taskName?.toString(),\n          filters?.student?.toString(),\n          filters?.checker?.toString(),\n          ...sortValues,\n        );\n        setReviews({ ...reviews, ...data });\n      } catch {\n        message.error('Failed to load mentor reviews. Please try later.');\n      }\n    },\n  );\n\n  const handleReviewerAssigned = async () => {\n    await getMentorReviews(reviews.pagination);\n  };\n\n  useAsync(async () => await getMentorReviews(reviews.pagination), [course]);\n\n  return (\n    <AdminPageLayout loading={loading} title=\"Mentor tasks review\" showCourseName courses={courses}>\n      <Space direction=\"vertical\">\n        <Space>\n          <Text strong>Submitted tasks</Text>\n          <Text>{course.name}</Text>\n        </Space>\n        {isManager && <Text type=\"secondary\">You can assign a checker for the student’s task</Text>}\n      </Space>\n      <MentorReviewsTable\n        content={reviews.content}\n        pagination={reviews.pagination}\n        handleChange={getMentorReviews}\n        handleReviewerAssigned={handleReviewerAssigned}\n        tasks={tasks ?? []}\n        isManager={isManager}\n      />\n    </AdminPageLayout>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/components/MentorCard/MentorCard.module.css",
    "content": ".card {\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.card :global(.ant-card-body) {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n}\n\n.totalStudentsCard {\n  text-align: center;\n}\n\n.totalStudentsCard :global(.ant-card-body) {\n  padding: 12px;\n}\n\n.certifiedText {\n  flex: 0 0 auto;\n  width: 72px;\n  text-align: center;\n  font-size: 0.875rem;\n  white-space: normal;\n  line-height: 1.25;\n  margin: 0 auto;\n}\n\n.courseList {\n  max-height: 150px;\n  overflow-y: auto;\n}\n\n.courseList :global(.ant-list-item) {\n  padding: 4px 0;\n}\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/components/MentorCard/MentorCard.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { TopMentorDto } from '@client/api';\nimport { MentorCard } from './MentorCard';\n\nvi.mock('next/config', () => () => ({}));\n\nconst mockMentor: TopMentorDto = {\n  rank: 1,\n  githubId: 'testmentor',\n  name: 'Test Mentor',\n  totalStudents: 25,\n  totalGratitudes: 12,\n  courseStats: [\n    { courseName: 'JS Course', studentsCount: 15 },\n    { courseName: 'React Course', studentsCount: 10 },\n  ],\n};\n\ndescribe('MentorCard', () => {\n  it('renders mentor name, githubId, and avatar', () => {\n    render(<MentorCard mentor={mockMentor} />);\n\n    expect(screen.getByText('Test Mentor')).toBeInTheDocument();\n    expect(screen.getByText('@testmentor')).toBeInTheDocument();\n    // Avatar is rendered\n    expect(screen.getAllByRole('img').length).toBeGreaterThan(0);\n  });\n\n  it('does not display rank badge', () => {\n    render(<MentorCard mentor={mockMentor} />);\n\n    expect(screen.queryByTitle('1')).not.toBeInTheDocument();\n  });\n\n  it('displays total students count', () => {\n    render(<MentorCard mentor={mockMentor} />);\n\n    expect(screen.getByText('25')).toBeInTheDocument();\n    expect(screen.getByText(/certified students/i)).toBeInTheDocument();\n  });\n\n  it('displays total gratitudes count with heart emoji', () => {\n    render(<MentorCard mentor={mockMentor} />);\n\n    expect(screen.getByText('12')).toBeInTheDocument();\n    expect(screen.getByText('❤️')).toBeInTheDocument();\n  });\n\n  it('renders course stats list', () => {\n    render(<MentorCard mentor={mockMentor} />);\n\n    expect(screen.getByText('JS Course')).toBeInTheDocument();\n    expect(screen.getByText('15')).toBeInTheDocument();\n    expect(screen.getByText('React Course')).toBeInTheDocument();\n    expect(screen.getByText('10')).toBeInTheDocument();\n  });\n\n  it('renders \"Say Thank you!\" button with link to /gratitude', () => {\n    render(<MentorCard mentor={mockMentor} />);\n\n    const link = screen.getByRole('link', { name: /say thank you/i });\n    expect(link).toBeInTheDocument();\n    expect(link).toHaveAttribute('href', '/gratitude?githubId=testmentor');\n  });\n\n  it('handles empty course stats gracefully', () => {\n    const mentorWithoutCourseStats: TopMentorDto = {\n      ...mockMentor,\n      courseStats: [],\n    };\n\n    render(<MentorCard mentor={mentorWithoutCourseStats} />);\n\n    expect(screen.getByText('Test Mentor')).toBeInTheDocument();\n    expect(screen.queryByText('JS Course')).not.toBeInTheDocument();\n  });\n\n  it('renders GitHub profile link', () => {\n    render(<MentorCard mentor={mockMentor} />);\n\n    const githubLink = screen.getByText('@testmentor');\n    expect(githubLink).toHaveAttribute('href', 'https://github.com/testmentor');\n    expect(githubLink).toHaveAttribute('target', '_blank');\n  });\n\n  it('renders zero students and gratitudes', () => {\n    const mentorWithZeroValues: TopMentorDto = {\n      ...mockMentor,\n      totalStudents: 0,\n      totalGratitudes: 0,\n    };\n\n    render(<MentorCard mentor={mentorWithZeroValues} />);\n\n    expect(screen.getAllByText('0')).toHaveLength(2);\n    expect(screen.getByText(/certified students/i)).toBeInTheDocument();\n  });\n\n  it('renders mentor when only firstName exists in name', () => {\n    const mentorWithFirstNameOnly: TopMentorDto = {\n      ...mockMentor,\n      name: 'John',\n    };\n\n    render(<MentorCard mentor={mentorWithFirstNameOnly} />);\n\n    expect(screen.getByText('John')).toBeInTheDocument();\n    expect(screen.queryByText('Test Mentor')).not.toBeInTheDocument();\n  });\n\n  it('renders mentor when only lastName exists in name', () => {\n    const mentorWithLastNameOnly: TopMentorDto = {\n      ...mockMentor,\n      name: 'Doe',\n    };\n\n    render(<MentorCard mentor={mentorWithLastNameOnly} />);\n\n    expect(screen.getByText('Doe')).toBeInTheDocument();\n    expect(screen.queryByText('Test Mentor')).not.toBeInTheDocument();\n  });\n\n  it('renders very long course names', () => {\n    const longCourseName =\n      'Very Long Course Name With Many Words For Overflow Testing Very Long Course Name With Many Words For Overflow Testing';\n    const mentorWithLongCourseName: TopMentorDto = {\n      ...mockMentor,\n      courseStats: [{ courseName: longCourseName, studentsCount: 7 }],\n    };\n\n    render(<MentorCard mentor={mentorWithLongCourseName} />);\n\n    expect(screen.getByText(longCourseName)).toBeInTheDocument();\n  });\n\n  it('renders very long mentor names', () => {\n    const longMentorName =\n      'Very Long Mentor Name With Many Words For Overflow Testing Very Long Mentor Name With Many Words For Overflow Testing';\n    const mentorWithLongName: TopMentorDto = {\n      ...mockMentor,\n      name: longMentorName,\n    };\n\n    render(<MentorCard mentor={mentorWithLongName} />);\n\n    expect(screen.getByText(longMentorName)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/components/MentorCard/MentorCard.tsx",
    "content": "import { Button, Card, Divider, Flex, List, Space, Typography } from 'antd';\nimport { HeartOutlined } from '@ant-design/icons';\nimport Link from 'next/link';\nimport { TopMentorDto } from '@client/api';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport styles from './MentorCard.module.css';\n\nconst { Title, Link: AntLink, Text } = Typography;\n\ninterface MentorCardProps {\n  mentor: TopMentorDto;\n}\n\nexport function MentorCard({ mentor }: MentorCardProps) {\n  const { githubId, name, totalStudents, totalGratitudes, courseStats } = mentor;\n\n  const gratitudeUrl = `/gratitude?githubId=${githubId}`;\n\n  return (\n    <Card\n      hoverable\n      className={styles.card}\n      actions={[\n        <Link href={gratitudeUrl} passHref legacyBehavior key=\"thank\">\n          <Button type=\"text\" icon={<HeartOutlined />}>\n            Say Thank you!\n          </Button>\n        </Link>,\n      ]}\n    >\n      <Space direction=\"vertical\" size=\"middle\" style={{ width: '100%' }}>\n        <Flex align=\"center\" gap=\"middle\">\n          <GithubAvatar githubId={githubId} size={48} />\n          <Space direction=\"vertical\" size={0} style={{ flex: 1, minWidth: 0 }}>\n            <Title level={5} style={{ margin: 0 }}>\n              {name}\n            </Title>\n            <AntLink href={`https://github.com/${githubId}`} target=\"_blank\" rel=\"noopener noreferrer\">\n              @{githubId}\n            </AntLink>\n          </Space>\n        </Flex>\n\n        <Card size=\"small\" className={styles.totalStudentsCard}>\n          <Flex justify=\"space-between\" align=\"center\" gap=\"small\" wrap=\"nowrap\">\n            <Text strong style={{ fontSize: '1.5rem', flexShrink: 0 }}>\n              {totalStudents}\n            </Text>\n            <Text type=\"secondary\" className={styles.certifiedText}>\n              certified students\n            </Text>\n            <Flex align=\"center\" gap={4} style={{ flexShrink: 0 }}>\n              <Text strong style={{ fontSize: '1.5rem' }}>\n                {totalGratitudes}\n              </Text>\n              <Text style={{ fontSize: '1.25rem' }}>❤️</Text>\n            </Flex>\n          </Flex>\n        </Card>\n\n        {courseStats.length > 0 && (\n          <>\n            <Divider style={{ margin: 0 }} />\n            <List\n              size=\"small\"\n              dataSource={courseStats}\n              renderItem={stat => (\n                <List.Item>\n                  <Flex justify=\"space-between\" style={{ width: '100%' }}>\n                    <Text style={{ flex: 1 }}>{stat.courseName}</Text>\n                    <Text strong>{stat.studentsCount}</Text>\n                  </Flex>\n                </List.Item>\n              )}\n              className={styles.courseList}\n            />\n          </>\n        )}\n      </Space>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/index.ts",
    "content": "export { MentorsHallOfFamePage } from './pages/MentorsHallOfFamePage';\nexport { MentorCard } from './components/MentorCard/MentorCard';\nexport * from './types';\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/pages/MentorsHallOfFamePage.test.tsx",
    "content": "import { render, screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { TopMentorDto } from '@client/api';\n\nvi.mock('next/config', () => () => ({}));\nconst mockedGetTopMentors = vi.fn();\nvi.mock('../services/mentors-hall-of-fame.service', () => ({\n  MentorsHallOfFameService: class {\n    getTopMentors(...args: unknown[]) {\n      return mockedGetTopMentors(...args);\n    }\n  },\n}));\n\nimport { MentorsHallOfFamePage } from './MentorsHallOfFamePage';\n\nconst lastYearMentors: TopMentorDto[] = [\n  {\n    rank: 1,\n    githubId: 'mentor-last-year',\n    name: 'Last Year Mentor',\n    totalStudents: 5,\n    totalGratitudes: 2,\n    courseStats: [{ courseName: 'JS', studentsCount: 5 }],\n  },\n];\n\nconst allTimeMentors: TopMentorDto[] = [\n  {\n    rank: 1,\n    githubId: 'mentor-all-time',\n    name: 'All Time Mentor',\n    totalStudents: 50,\n    totalGratitudes: 20,\n    courseStats: [{ courseName: 'React', studentsCount: 50 }],\n  },\n];\n\ndescribe('MentorsHallOfFamePage', () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n    mockedGetTopMentors.mockReset();\n    mockedGetTopMentors.mockResolvedValue([]);\n  });\n\n  it('renders page title', async () => {\n    mockedGetTopMentors.mockResolvedValueOnce(lastYearMentors);\n\n    render(<MentorsHallOfFamePage />);\n\n    expect(await screen.findByText('Mentors Hall of Fame')).toBeInTheDocument();\n  });\n\n  it('loads mentors data on mount', async () => {\n    mockedGetTopMentors.mockResolvedValueOnce(lastYearMentors);\n\n    render(<MentorsHallOfFamePage />);\n\n    await waitFor(() => {\n      expect(mockedGetTopMentors).toHaveBeenCalledWith(false);\n    });\n  });\n\n  it('shows loading state during request', async () => {\n    let resolveRequest: (value: TopMentorDto[]) => void = () => {};\n    const pendingPromise = new Promise<TopMentorDto[]>(resolve => {\n      resolveRequest = resolve;\n    });\n    mockedGetTopMentors.mockReturnValueOnce(pendingPromise);\n\n    render(<MentorsHallOfFamePage />);\n\n    expect(screen.getByText('Loading top mentors...')).toBeInTheDocument();\n\n    resolveRequest(lastYearMentors);\n    expect(await screen.findByText('Last Year Mentor')).toBeInTheDocument();\n  });\n\n  it('renders mentors list after successful load', async () => {\n    mockedGetTopMentors.mockResolvedValueOnce(lastYearMentors);\n\n    render(<MentorsHallOfFamePage />);\n\n    expect(await screen.findByText('Last Year Mentor')).toBeInTheDocument();\n    expect(screen.getByText('@mentor-last-year')).toBeInTheDocument();\n  });\n\n  it('switches period from lastYear to allTime', async () => {\n    const user = userEvent.setup();\n    mockedGetTopMentors.mockResolvedValueOnce(lastYearMentors).mockResolvedValueOnce(allTimeMentors);\n\n    render(<MentorsHallOfFamePage />);\n\n    await screen.findByText('Last Year Mentor');\n\n    await user.click(screen.getByText('All Time'));\n\n    await waitFor(() => {\n      expect(mockedGetTopMentors).toHaveBeenNthCalledWith(2, true);\n    });\n    expect(await screen.findByText('All Time Mentor')).toBeInTheDocument();\n  });\n\n  it('updates description when period changes', async () => {\n    const user = userEvent.setup();\n    mockedGetTopMentors.mockResolvedValueOnce(lastYearMentors).mockResolvedValueOnce(allTimeMentors);\n\n    render(<MentorsHallOfFamePage />);\n\n    expect(\n      await screen.findByText(\n        'Celebrating our top mentors who guided the most students to receive certificates in the last year',\n      ),\n    ).toBeInTheDocument();\n\n    await user.click(screen.getByText('All Time'));\n\n    expect(\n      await screen.findByText('Celebrating our top mentors who guided the most students to receive certificates'),\n    ).toBeInTheDocument();\n  });\n\n  it('renders empty state when there are no mentors', async () => {\n    mockedGetTopMentors.mockResolvedValueOnce([]);\n\n    render(<MentorsHallOfFamePage />);\n\n    expect(await screen.findByText('No mentors found')).toBeInTheDocument();\n  });\n\n  it('handles request error and shows empty state', async () => {\n    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation();\n    mockedGetTopMentors.mockRejectedValueOnce(new Error('Request failed'));\n\n    render(<MentorsHallOfFamePage />);\n\n    expect(await screen.findByText('No mentors found')).toBeInTheDocument();\n    expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to fetch top mentors:', expect.any(Error));\n\n    consoleErrorSpy.mockRestore();\n  });\n\n  it('refetches mentors when period changes', async () => {\n    const user = userEvent.setup();\n    mockedGetTopMentors.mockResolvedValueOnce(lastYearMentors).mockResolvedValueOnce(allTimeMentors);\n\n    render(<MentorsHallOfFamePage />);\n\n    await screen.findByText('Last Year Mentor');\n\n    await user.click(screen.getByText('All Time'));\n\n    await waitFor(() => {\n      expect(mockedGetTopMentors).toHaveBeenCalledTimes(2);\n    });\n    expect(mockedGetTopMentors).toHaveBeenNthCalledWith(1, false);\n    expect(mockedGetTopMentors).toHaveBeenNthCalledWith(2, true);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/pages/MentorsHallOfFamePage.tsx",
    "content": "import { useCallback, useEffect, useState } from 'react';\nimport { Col, Empty, Flex, Row, Segmented, Space, Spin, Typography } from 'antd';\nimport { TrophyOutlined } from '@ant-design/icons';\nimport { TopMentorDto } from '@client/api';\nimport { MentorCard } from '../components/MentorCard/MentorCard';\nimport { MentorsHallOfFameService } from '../services/mentors-hall-of-fame.service';\n\nconst { Title, Paragraph } = Typography;\n\nconst service = new MentorsHallOfFameService();\n\ntype TimePeriod = 'lastYear' | 'allTime';\n\nexport function MentorsHallOfFamePage() {\n  const [mentors, setMentors] = useState<TopMentorDto[]>([]);\n  const [loading, setLoading] = useState(true);\n  const [timePeriod, setTimePeriod] = useState<TimePeriod>('lastYear');\n\n  const allTime = timePeriod === 'allTime';\n\n  const fetchMentors = useCallback(async () => {\n    setLoading(true);\n    try {\n      const data = await service.getTopMentors(allTime);\n      setMentors(data);\n    } catch (error) {\n      console.error('Failed to fetch top mentors:', error);\n    } finally {\n      setLoading(false);\n    }\n  }, [allTime]);\n\n  useEffect(() => {\n    fetchMentors();\n  }, [fetchMentors]);\n\n  const description = allTime\n    ? 'Celebrating our top mentors who guided the most students to receive certificates'\n    : 'Celebrating our top mentors who guided the most students to receive certificates in the last year';\n\n  return (\n    <Flex justify=\"center\" style={{ padding: 24 }}>\n      <Space direction=\"vertical\" size=\"large\" style={{ width: '100%', maxWidth: 1200 }}>\n        <Flex vertical align=\"center\" gap=\"middle\">\n          <TrophyOutlined style={{ fontSize: 48, color: '#faad14' }} />\n          <Title level={1} style={{ margin: 0 }}>\n            Mentors Hall of Fame\n          </Title>\n          <Paragraph type=\"secondary\" style={{ fontSize: 16, margin: 0, textAlign: 'center' }}>\n            {description}\n          </Paragraph>\n          <Segmented\n            options={[\n              { label: 'Last Year', value: 'lastYear' },\n              { label: 'All Time', value: 'allTime' },\n            ]}\n            value={timePeriod}\n            onChange={value => setTimePeriod(value as TimePeriod)}\n          />\n        </Flex>\n\n        {loading ? (\n          <Flex vertical align=\"center\" gap=\"middle\" style={{ padding: 48 }}>\n            <Spin size=\"large\" />\n            <Paragraph type=\"secondary\">Loading top mentors...</Paragraph>\n          </Flex>\n        ) : mentors.length === 0 ? (\n          <Empty description=\"No mentors found\" />\n        ) : (\n          <Row gutter={[24, 24]}>\n            {mentors.map(mentor => (\n              <Col key={mentor.githubId} xs={24} sm={12} lg={8} xl={6}>\n                <MentorCard mentor={mentor} />\n              </Col>\n            ))}\n          </Row>\n        )}\n      </Space>\n    </Flex>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/services/mentors-hall-of-fame.service.test.ts",
    "content": "import { MentorsHallOfFameApi, TopMentorDto } from '@client/api';\nimport { MentorsHallOfFameService } from './mentors-hall-of-fame.service';\n\nvi.mock('@client/api');\n\ndescribe('MentorsHallOfFameService', () => {\n  const getTopMentorsApiMock = vi.mocked(MentorsHallOfFameApi.prototype.getTopMentors);\n\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it('calls API with provided allTime parameter', async () => {\n    getTopMentorsApiMock.mockResolvedValueOnce({ data: [] });\n\n    const service = new MentorsHallOfFameService();\n    await service.getTopMentors(true);\n\n    expect(getTopMentorsApiMock).toHaveBeenCalledWith(true);\n  });\n\n  it('returns data from successful API response', async () => {\n    const mentors: TopMentorDto[] = [\n      {\n        rank: 1,\n        githubId: 'mentor-1',\n        name: 'Mentor One',\n        totalStudents: 10,\n        totalGratitudes: 4,\n        courseStats: [],\n      },\n    ];\n    getTopMentorsApiMock.mockResolvedValueOnce({ data: mentors });\n\n    const service = new MentorsHallOfFameService();\n    const result = await service.getTopMentors(false);\n\n    expect(result).toEqual(mentors);\n  });\n\n  it('throws when API request fails', async () => {\n    const requestError = new Error('API failed');\n    getTopMentorsApiMock.mockRejectedValueOnce(requestError);\n\n    const service = new MentorsHallOfFameService();\n\n    await expect(service.getTopMentors()).rejects.toThrow('API failed');\n  });\n});\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/services/mentors-hall-of-fame.service.ts",
    "content": "import { MentorsHallOfFameApi, TopMentorDto } from '@client/api';\n\nconst mentorsHallOfFameApi = new MentorsHallOfFameApi();\n\nexport class MentorsHallOfFameService {\n  async getTopMentors(allTime = false): Promise<TopMentorDto[]> {\n    const { data } = await mentorsHallOfFameApi.getTopMentors(allTime);\n    return data;\n  }\n}\n"
  },
  {
    "path": "client/src/modules/MentorsHallOfFame/types.ts",
    "content": "import { MentorCourseStatsDto, TopMentorDto } from '@client/api';\n\nexport type { TopMentorDto, MentorCourseStatsDto } from '@client/api';\nexport type TopMentor = TopMentorDto;\nexport type CourseStats = MentorCourseStatsDto;\n"
  },
  {
    "path": "client/src/modules/NotAccess/NotAccess.tsx",
    "content": "import { Row, Image } from 'antd';\nimport { PageLayout } from '@client/shared/components/PageLayout';\n\nexport default function NotAccess() {\n  return (\n    <PageLayout loading={false}>\n      <Row justify=\"center\" style={{ margin: '65px 0 25px 0' }}>\n        <Image\n          src=\"https://cdn.rs.school/sloths/stickers/what-is-it/image.svg\"\n          alt=\"Error 403\"\n          preview={false}\n          width={175}\n          height={175}\n        />\n      </Row>\n      <Row justify=\"center\">\n        <h2>Sorry, you do not have access to this page.</h2>\n      </Row>\n    </PageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/NotAccess/index.ts",
    "content": "export { default as NotAccess } from './NotAccess';\n"
  },
  {
    "path": "client/src/modules/Notifications/components/Consents.tsx",
    "content": "import { message, Alert, Space } from 'antd';\nimport { EmailConfirmation } from '@client/components/Profile/EmailConfirmation';\nimport discordIntegration from '@client/configs/discord-integration';\nimport Link from 'next/link';\nimport { useCallback } from 'react';\nimport { UserService } from '@client/services/user';\n\nconst rsschoolBotLink = 'https://t.me/rsschool_bot?start=connect';\n\nexport type Connection = {\n  value: string;\n  enabled: boolean;\n  lastLinkSentAt?: string;\n};\n\nexport function Consents({\n  email,\n  telegram,\n  discord,\n}: {\n  email?: Connection;\n  telegram?: Connection;\n  discord?: Connection;\n}) {\n  const hasEmail = !!email?.enabled;\n  const hasTelegram = !!telegram?.enabled;\n  const hasDiscord = !!discord?.enabled || true;\n  const hasContacts = hasEmail && hasTelegram && hasDiscord;\n\n  const emailAdded = email?.value;\n  const emailVerified = email?.enabled;\n\n  const sendEmailConfirmationLink = useCallback(async () => {\n    try {\n      await new UserService().sendEmailConfirmationLink();\n    } catch {\n      message.error('Error has occured. Please try again later');\n    }\n  }, []);\n\n  return !hasContacts ? (\n    <Space direction=\"vertical\" style={{ width: '100%' }}>\n      <Alert\n        message={\n          hasTelegram ? (\n            <>\n              Telegram notifications are sent from <a href={rsschoolBotLink}>@rsschool_bot</a>\n            </>\n          ) : (\n            <>\n              Note: To enable Telegram notifications please open the <a href={rsschoolBotLink}>@rsschool_bot</a> and\n              click the <b>Start</b> button to set it up\n            </>\n          )\n        }\n        type=\"info\"\n      />\n\n      {!hasDiscord && (\n        <Alert\n          message={\n            <div>\n              Note: To enable discord notifications please <a href={discordIntegration.api.auth}>authorize</a> first\n            </div>\n          }\n          type=\"info\"\n        />\n      )}\n      {!emailAdded && (\n        <Alert\n          message={\n            <div>\n              To set up email notification, please enter your email on <Link href=\"/profile\">Profile</Link> page\n            </div>\n          }\n          type=\"info\"\n        />\n      )}\n      {emailAdded && !emailVerified && (\n        <EmailConfirmation connection={email} sendConfirmationEmail={sendEmailConfirmationLink} />\n      )}\n    </Space>\n  ) : null;\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/components/NotificationSettingsModal.module.css",
    "content": ".tabs :global(.ant-tabs-tab-btn) {\n  text-transform: capitalize;\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/components/NotificationSettingsModal.tsx",
    "content": "import { Form, Input, Select, Checkbox, Tabs } from 'antd';\nimport { NotificationDto, NotificationType } from '@client/api';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { NotificationTemlate } from '../services/notifications';\nimport styles from './NotificationSettingsModal.module.css';\n\nconst { TextArea } = Input;\n\ntype Props = {\n  notification?: NotificationDto;\n  onCancel: () => void;\n  onOk: (notification: NotificationDto) => void;\n  notifications: Pick<NotificationDto, 'id' | 'name'>[];\n};\n\nexport function NotificationSettingsModal(props: Props) {\n  const {\n    notification = {\n      enabled: false,\n    } as NotificationDto,\n    onCancel,\n    onOk,\n    notifications = [],\n  } = props;\n\n  const initialValue = {\n    ...notification,\n    channels: defaultChannels.map(channel => {\n      const existing = notification.channels?.find(existing => channel.channelId === existing.channelId);\n      return {\n        ...channel,\n        ...existing,\n      };\n    }),\n  };\n  const { channels } = initialValue;\n  const parentNotifications = [\n    { id: undefined, name: 'Empty' },\n    ...notifications.filter(n => n.id !== notification.id),\n  ];\n\n  const tabItems = [\n    {\n      key: 'sd',\n      label: 'Settings',\n      forceRender: true,\n      destroyInactiveTabPane: false,\n      children: (\n        <>\n          <Form.Item name=\"id\" label=\"Id\" rules={[{ required: true, message: 'Please enter id' }]}>\n            <Input disabled={!!notification.id} />\n          </Form.Item>\n          <Form.Item name=\"name\" rules={[{ required: true, message: 'Please enter name' }]} label=\"Name\">\n            <Input />\n          </Form.Item>\n          <Form.Item name=\"enabled\" valuePropName=\"checked\">\n            <Checkbox>Active</Checkbox>\n          </Form.Item>\n          <Form.Item name=\"type\" rules={[{ required: true, message: 'Please select type' }]} label=\"Type\">\n            <Select placeholder=\"Please select type\">\n              {Object.values(NotificationType).map(type => (\n                <Select.Option key={type} value={type}>\n                  {type}\n                </Select.Option>\n              ))}\n            </Select>\n          </Form.Item>\n          {notifications.length > 1 && (\n            <Form.Item name=\"parentId\" label=\"Parent\">\n              <Select placeholder=\"Please select parent\">\n                {parentNotifications.map(({ id, name }) => (\n                  <Select.Option key={id} value={id}>\n                    {name}\n                  </Select.Option>\n                ))}\n              </Select>\n            </Form.Item>\n          )}\n        </>\n      ),\n    },\n    ...channels.map((channel, index) => ({\n      key: channel.channelId,\n      label: channel.channelId,\n      forceRender: true,\n      children: (\n        <>\n          <Form.Item hidden label={channel.channelId} name={['channels', index, 'channelId']}>\n            <Input></Input>\n          </Form.Item>\n          {channel.channelId === 'email' && (\n            <Form.Item label=\"subject\" name={['channels', index, 'template', 'subject']}>\n              <Input />\n            </Form.Item>\n          )}\n          <Form.Item label=\"body\" name={['channels', index, 'template', 'body']}>\n            <TextArea rows={20} />\n          </Form.Item>\n        </>\n      ),\n    })),\n  ];\n\n  return (\n    <ModalForm title=\"Notification Settings\" data={initialValue} submit={handleSubmit} cancel={onCancel}>\n      <div className={styles.tabs}>\n        <Tabs items={tabItems} />\n      </div>\n    </ModalForm>\n  );\n\n  function handleSubmit(notification: NotificationDto) {\n    onOk(notification);\n  }\n}\nconst defaultChannels = [{ channelId: 'email' }, { channelId: 'telegram' }, { channelId: 'discord' }] as {\n  channelId: string;\n  template?: NotificationTemlate;\n}[];\n"
  },
  {
    "path": "client/src/modules/Notifications/components/NotificationSettingsTable.tsx",
    "content": "import { Table, Typography } from 'antd';\nimport { ColumnType } from 'antd/lib/table';\nimport { NotificationDto } from '@client/api';\nimport { CustomPopconfirm } from '@client/components/common/CustomPopconfirm';\nimport { boolIconRenderer } from '@client/shared/components/Table';\nimport { useMemo } from 'react';\n\nexport function NotificationSettingsTable({\n  notifications,\n  onEdit,\n  onDelete,\n}: {\n  notifications: NotificationDto[];\n  onEdit: (record: NotificationDto) => void;\n  onDelete: (record: NotificationDto) => void;\n}) {\n  const columns = useMemo(() => buildColumns(onEdit, onDelete), [onEdit]);\n\n  return (\n    <>\n      <Table\n        size=\"small\"\n        style={{ marginTop: 8 }}\n        dataSource={notifications}\n        rowKey=\"id\"\n        columns={columns}\n        pagination={false}\n      />\n    </>\n  );\n}\n\nfunction buildColumns(onEdit: (record: NotificationDto) => void, onDelete: (record: NotificationDto) => void) {\n  const columns: ColumnType<NotificationDto>[] = [\n    {\n      title: 'Notification',\n      dataIndex: ['name'],\n    },\n    {\n      title: 'Active',\n      dataIndex: ['enabled'],\n      align: 'center',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Actions',\n      align: 'center',\n      render: (_: unknown, record: any) => (\n        <>\n          <div>\n            <a onClick={() => onEdit(record)}>Edit</a>\n          </div>\n          <div>\n            <CustomPopconfirm\n              title=\"Are you sure to delete this notification?\"\n              onConfirm={() => onDelete(record)}\n              okText=\"Yes\"\n              cancelText=\"No\"\n            >\n              <Typography.Link>Delete</Typography.Link>\n            </CustomPopconfirm>\n          </div>\n        </>\n      ),\n    },\n  ];\n\n  return columns;\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/components/NotificationsUserSettingsTable.module.css",
    "content": ".column {\n  text-transform: capitalize;\n}\n\n.disabled {\n  cursor: no-drop;\n}\n\n.disabled > :global(*) {\n  opacity: 0.2;\n  pointer-events: none;\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/components/NotificationsUserSettingsTable.tsx",
    "content": "import { useMemo } from 'react';\nimport { Table } from 'antd';\n\nimport styles from './NotificationsUserSettingsTable.module.css';\nimport { ColumnType } from 'antd/lib/table';\nimport { buildCheckBoxRenderer } from '@client/shared/components/Table';\nimport { NotificationChannel, UserNotificationSettings } from '../services/notifications';\n\nexport function NotificationsTable({\n  notifications,\n  disabledChannels,\n  onCheck,\n}: {\n  notifications: UserNotificationSettings[];\n  disabledChannels?: NotificationChannel[];\n  onCheck: (dataIndex: string[], record: UserNotificationSettings, checked: boolean) => void;\n}) {\n  const columns = useMemo(() => buildColumns(onCheck, disabledChannels), [onCheck, disabledChannels]);\n\n  return (\n    <Table\n      size=\"small\"\n      style={{ marginTop: 8 }}\n      dataSource={notifications}\n      rowKey=\"name\"\n      columns={columns}\n      pagination={false}\n    />\n  );\n}\n\nfunction buildColumns(\n  onCheck: (dataIndex: string[], record: UserNotificationSettings, checked: boolean) => void,\n  disabledChannels: NotificationChannel[] = [],\n) {\n  const columns: ColumnType<UserNotificationSettings>[] = [\n    {\n      title: 'Notification',\n      dataIndex: ['name'],\n    },\n  ];\n\n  return columns.concat(\n    Object.keys(NotificationChannel)\n      .filter(channel => channel !== 'discord')\n      .map<ColumnType<UserNotificationSettings>>(channel => {\n        const dataIndex = ['settings', channel];\n        return {\n          align: 'center',\n          className: `${styles.column} ${\n            disabledChannels.includes(channel as NotificationChannel) ? styles.disabled : ''\n          }`,\n          title: channel,\n          dataIndex,\n          render: buildCheckBoxRenderer<UserNotificationSettings>(dataIndex, onCheck, channel !== 'discord'),\n        };\n      }),\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/pages/AdminNotificationsPage/AdminNotificationsSettingsPage.tsx",
    "content": "import { useState, useMemo, useCallback, ReactNode } from 'react';\nimport { Button, Spin } from 'antd';\nimport { NotificationsService } from '@client/modules/Notifications/services/notifications';\nimport { useLoading } from '@client/components/useLoading';\nimport { useAsync } from 'react-use';\nimport { NotificationSettingsTable } from '@client/modules/Notifications/components/NotificationSettingsTable';\nimport { NotificationSettingsModal } from '@client/modules/Notifications/components/NotificationSettingsModal';\nimport { NotificationDto } from '@client/api';\nimport { useMessage } from '@client/hooks';\n\nexport function AdminNotificationsPage() {\n  const { message } = useMessage();\n  const [notifications, setNotifications] = useState<NotificationDto[]>([]);\n  const [loading, withLoading] = useLoading(false);\n  const service = useMemo(() => new NotificationsService(), []);\n  const [modal, setModal] = useState<ReactNode>();\n\n  const loadData = useCallback(\n    withLoading(async () => {\n      setNotifications(await service.getNotifications());\n    }),\n    [],\n  );\n\n  useAsync(loadData, []);\n\n  const edit = useCallback(\n    (notification: NotificationDto) => {\n      setModal(\n        <NotificationSettingsModal\n          notifications={notifications}\n          notification={notification}\n          onCancel={() => setModal(undefined)}\n          onOk={saveNotification}\n        />,\n      );\n    },\n    [saveNotification, notifications],\n  );\n\n  const create = useCallback(() => {\n    setModal(\n      <NotificationSettingsModal\n        onCancel={() => setModal(undefined)}\n        onOk={saveNotification}\n        notifications={notifications}\n      />,\n    );\n  }, [notifications]);\n\n  return (\n    <Spin spinning={loading}>\n      <Button type=\"primary\" onClick={create}>\n        Add Notification\n      </Button>\n      <NotificationSettingsTable onEdit={edit} onDelete={deleteNotification} notifications={notifications} />\n      {modal}\n    </Spin>\n  );\n\n  async function saveNotification(notification: NotificationDto) {\n    try {\n      const isSave = notifications.find(({ id }) => notification.id === id);\n      const { data } = await (isSave\n        ? service.saveNotification(notification)\n        : service.createNotification(notification));\n\n      setNotifications(notifications => {\n        return isSave\n          ? notifications.map(notification => (notification.id === data.id ? data : notification))\n          : [...notifications, data];\n      });\n      setModal(null);\n      message.success('New notification settings saved.');\n    } catch {\n      message.error('Failed to save settings.');\n    }\n  }\n\n  async function deleteNotification(notification: NotificationDto) {\n    try {\n      await service.deleteNotification(notification.id);\n      setNotifications(notifications => notifications.filter(n => n.id !== notification.id));\n      message.success('Notification is deleted.');\n    } catch {\n      message.error('Failed to delete notification.');\n    }\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/pages/AdminNotificationsPage/index.tsx",
    "content": "import { Tabs } from 'antd';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { AdminNotificationsPage } from './AdminNotificationsSettingsPage';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\nexport function AdminPage() {\n  const { courses } = useActiveCourseContext();\n\n  return (\n    <AdminPageLayout title=\"Notifications\" loading={false} courses={courses}>\n      <Tabs type=\"card\" items={[{ key: '1', label: 'Settings', children: <AdminNotificationsPage /> }]} />\n    </AdminPageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/pages/ConnectionConfirmedPage.tsx",
    "content": "import { Alert, Layout } from 'antd';\nimport { FooterLayout } from '@client/components/Footer';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport Link from 'next/link';\nimport { useState } from 'react';\n\nexport function ConnectionConfirmed() {\n  const [connectionType] = useState(window ? new URLSearchParams(window.location.search).get('connectionType') : null);\n\n  return (\n    <Layout style={{ minHeight: '100vh' }}>\n      <PageLayout loading={false} title=\"Connection confirmed\">\n        <Alert\n          type=\"success\"\n          showIcon\n          message={`Your have successfully connected your ${connectionType}`}\n          description={\n            <div>\n              Now you can subscribe to <Link href=\"/profile/notifications\">notifications</Link>\n            </div>\n          }\n        />\n      </PageLayout>\n      <FooterLayout />\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/pages/UserNotificationsSettingsPage.tsx",
    "content": "import { useState, useMemo, useCallback } from 'react';\nimport { Button, Space } from 'antd';\nimport {\n  NotificationsService,\n  NotificationChannel,\n  UserNotificationSettings,\n} from '@client/modules/Notifications/services/notifications';\nimport set from 'lodash/set';\nimport { useLoading } from '@client/components/useLoading';\nimport { useAsync } from 'react-use';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { NotificationsTable } from '../components/NotificationsUserSettingsTable';\nimport { Consents, Connection } from '../components/Consents';\nimport { UpdateNotificationUserSettingsDto } from '@client/api';\nimport { useMessage } from '@client/hooks';\n\nexport function UserNotificationsPage() {\n  const { message } = useMessage();\n  const [notifications, setNotifications] = useState<UserNotificationSettings[]>([]);\n  const [loading, withLoading] = useLoading(false);\n  const service = useMemo(() => new NotificationsService(), []);\n  const [email, setEmail] = useState<Connection>();\n  const [telegram, setTelegram] = useState<Connection>();\n  const [discord, setDiscord] = useState<Connection>();\n  const [disabledChannels, setDisabledChannels] = useState<NotificationChannel[]>([]);\n\n  const loadData = useCallback(\n    withLoading(async () => {\n      const { connections, notifications } = await service.getUserNotificationSettings();\n      setNotifications(notifications);\n\n      const { email, telegram, discord } = connections as Record<NotificationChannel, Connection | undefined>;\n      setEmail(email);\n      setTelegram(telegram);\n      setDiscord(discord);\n      const hasEmail = !!email?.enabled;\n      const hasTelegram = !!telegram?.enabled;\n      const hasDiscord = !!discord?.enabled;\n\n      const disabledChannels = [];\n      if (!hasEmail) {\n        disabledChannels.push(NotificationChannel.email);\n      }\n      if (!hasTelegram) {\n        disabledChannels.push(NotificationChannel.telegram);\n      }\n      if (!hasDiscord) {\n        disabledChannels.push(NotificationChannel.discord);\n      }\n      setDisabledChannels(disabledChannels);\n    }),\n    [],\n  );\n\n  useAsync(loadData, []);\n\n  const onCheck = useCallback(\n    async (dataIndex: string[], record: UserNotificationSettings, checked: boolean) => {\n      const newData = [...notifications];\n      const index = notifications.findIndex(item => record.id === item.id);\n      if (index >= 0 && newData[index]) {\n        newData[index] = { ...newData[index] };\n        const notification = newData[index];\n        if (notification) {\n          set(notification, dataIndex, checked);\n        }\n      }\n\n      setNotifications(newData);\n    },\n    [notifications],\n  );\n\n  const hasConnections = Object.keys(NotificationChannel).length !== disabledChannels.length;\n\n  return (\n    <PageLayout loading={loading} title=\"Notifications\" showCourseName>\n      <Space direction=\"vertical\" style={{ width: '100%' }}>\n        {!loading && <Consents email={email} telegram={telegram} discord={discord} />}\n        <Space direction=\"horizontal\" style={{ width: '100%', justifyContent: 'flex-end' }}>\n          <Button disabled={!hasConnections} type=\"primary\" onClick={saveSettings}>\n            Save\n          </Button>\n        </Space>\n      </Space>\n      <NotificationsTable disabledChannels={disabledChannels} notifications={notifications} onCheck={onCheck} />\n    </PageLayout>\n  );\n\n  async function saveSettings() {\n    try {\n      await service.saveUserNotifications(\n        notifications.reduce((raw: UpdateNotificationUserSettingsDto[], notification) => {\n          Object.keys(notification.settings).forEach(channelId => {\n            raw.push({\n              channelId,\n              enabled: (notification.settings as Record<string, boolean>)[channelId] ?? false,\n              notificationId: notification.id,\n            });\n          });\n          return raw;\n        }, []),\n      );\n      message.success('New notification settings saved.');\n    } catch {\n      message.error('Failed to save settings.');\n    }\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Notifications/services/notifications.ts",
    "content": "import {\n  NotificationDto,\n  NotificationsApi,\n  NotificationUserSettingsDto,\n  UpdateNotificationDto,\n  UpdateNotificationUserSettingsDto,\n  UsersNotificationsApi,\n} from '@client/api';\n\nexport class NotificationsService {\n  constructor(\n    private notificationsApi = new NotificationsApi(),\n    private usersApi = new UsersNotificationsApi(),\n  ) {}\n\n  // System notifications settings\n  async getNotifications(): Promise<NotificationDto[]> {\n    const { data } = await this.notificationsApi.getNotifications();\n\n    return data;\n  }\n\n  async saveNotification(notification: UpdateNotificationDto) {\n    return this.notificationsApi.updateNotification(notification);\n  }\n\n  async createNotification(notification: UpdateNotificationDto) {\n    return this.notificationsApi.createNotification(notification);\n  }\n\n  deleteNotification(id: string) {\n    return this.notificationsApi.deleteNotification(id);\n  }\n\n  // user notification settings\n  async saveUserNotifications(notifications: UpdateNotificationUserSettingsDto[]) {\n    return this.usersApi.updateUserNotifications(notifications);\n  }\n\n  async getUserNotificationSettings() {\n    const { data } = await this.usersApi.getUserNotifications();\n    return data;\n  }\n\n  async getUserConnections() {\n    const { data } = await this.usersApi.getUserNotificationConnections();\n    return data.connections;\n  }\n}\n\nexport type UserNotificationSettings = NotificationUserSettingsDto;\n\nexport enum NotificationChannel {\n  email = 'email',\n  telegram = 'telegram',\n  discord = 'discord',\n}\n\nexport type MessagePayload = EmailPayload | TelegramPayload;\n\nexport type EmailPayload = {\n  subject: string;\n  body: string;\n  userIds: number[];\n};\n\nexport type TelegramPayload = {\n  userIds: number[];\n  body: string;\n};\n\nexport type NotificationTemlate = TelegramTemplate | EmailTemplate | DiscordTemplate;\n\ntype TelegramTemplate = {\n  body: string;\n};\n\ntype EmailTemplate = {\n  subject: string;\n  body: string;\n};\n\ntype DiscordTemplate = {\n  body: string;\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/AvatarCv/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { AvatarCv } from './index';\n\nconst mockUrl = 'https://example.com';\n\ndescribe('AvatarCv', () => {\n  test('should render img with proper src if provided', () => {\n    render(<AvatarCv src={mockUrl} />);\n    const img = screen.getByRole('img');\n    expect(img).toHaveAttribute('src', mockUrl);\n  });\n\n  test('should render icon if src is not provided', () => {\n    render(<AvatarCv src={null} />);\n    const img = screen.queryByRole('img');\n    expect(img).not.toHaveAttribute('src');\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/AvatarCv/index.tsx",
    "content": "import { Avatar } from 'antd';\nimport { UserOutlined } from '@ant-design/icons';\n\ntype Props = {\n  src: string | null;\n};\n\nconst AVATAR_SIZE = 80;\n\nexport const AvatarCv = (props: Props) => {\n  const src = props.src ?? undefined;\n  const icon = src ? null : <UserOutlined />;\n  return <Avatar src={src} icon={icon} size={AVATAR_SIZE} />;\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/ContactsForm/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ContactsForm } from './index';\n\nconst mockContactsList = {\n  email: 'example@example.com',\n  githubUsername: 'some-github',\n  linkedin: 'https://linked.in',\n  phone: '+1111111111111',\n  skype: 'some_skype',\n  telegram: 'some_telegram',\n  website: 'https://example.com',\n};\n\ndescribe('ContactsForm', () => {\n  test.each`\n    value                              | placeholder                 | labelText\n    ${mockContactsList.email}          | ${'Email'}                  | ${'Email'}\n    ${mockContactsList.githubUsername} | ${'GitHub username'}        | ${'GitHub'}\n    ${mockContactsList.linkedin}       | ${'LinkedIn username'}      | ${'LinkedIn'}\n    ${mockContactsList.phone}          | ${'+12025550111'}           | ${'Phone'}\n    ${mockContactsList.skype}          | ${'Skype id'}               | ${'Skype'}\n    ${mockContactsList.telegram}       | ${'Telegram public name'}   | ${'Telegram'}\n    ${mockContactsList.website}        | ${'Enter your website URL'} | ${'Website'}\n  `('form field should have proper value, placeholder and label', async ({ value, placeholder, labelText }) => {\n    render(<ContactsForm contactsList={mockContactsList} />);\n\n    const fieldDisplayedValue = await screen.findByDisplayValue(value);\n    const fieldPlaceholder = await screen.findByPlaceholderText(placeholder);\n    const fieldLabel = await screen.findByLabelText(labelText);\n\n    expect(fieldDisplayedValue).toBeInTheDocument();\n    expect(fieldPlaceholder).toBeInTheDocument();\n    expect(fieldLabel).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/ContactsForm/index.tsx",
    "content": "import { forwardRef, useEffect, ForwardedRef } from 'react';\nimport { Form, Input, Card, FormInstance, Typography } from 'antd';\nimport { Contacts } from '@client/modules/Opportunities/models';\nimport { contactsValidationRules as validationRules } from '../form-validation';\n\nconst { Item } = Form;\nconst { Text } = Typography;\n\ntype Props = {\n  contactsList: Contacts;\n};\n\nexport const ContactsForm = forwardRef((props: Props, ref: ForwardedRef<FormInstance>) => {\n  const { contactsList } = props;\n\n  const [form] = Form.useForm();\n\n  useEffect(() => {\n    form.setFieldsValue(contactsList);\n    form.validateFields();\n  }, [contactsList]);\n\n  const inputStyle = {\n    maxWidth: '400px',\n  };\n\n  return (\n    <Card title={<Text strong>Contacts</Text>} style={{ width: '70vw', marginBottom: '20px' }}>\n      <Form\n        form={form}\n        ref={ref}\n        name=\"contacts\"\n        labelCol={{ span: 9 }}\n        wrapperCol={{ span: 10 }}\n        style={{ width: '100%' }}\n      >\n        <Item label=\"Phone\" name=\"phone\" rules={[...validationRules['phone']]}>\n          <Input style={inputStyle} placeholder=\"+12025550111\" />\n        </Item>\n        <Item label=\"Email\" name=\"email\" rules={[...validationRules['email']]}>\n          <Input style={inputStyle} placeholder=\"Email\" />\n        </Item>\n        <Item label=\"Skype\" name=\"skype\" rules={[...validationRules['skype']]}>\n          <Input style={inputStyle} placeholder=\"Skype id\" />\n        </Item>\n        <Item label=\"Telegram\" name=\"telegram\" rules={[...validationRules['telegram']]}>\n          <Input style={inputStyle} placeholder=\"Telegram public name\" />\n        </Item>\n        <Item label=\"LinkedIn\" name=\"linkedin\" rules={[...validationRules['linkedin']]}>\n          <Input style={inputStyle} placeholder=\"LinkedIn username\" />\n        </Item>\n        <Item label=\"GitHub\" name=\"githubUsername\" rules={[...validationRules['github']]}>\n          <Input style={inputStyle} placeholder=\"GitHub username\" />\n        </Item>\n        <Item label=\"Website\" name=\"website\" rules={[...validationRules['website']]}>\n          <Input style={inputStyle} placeholder=\"Enter your website URL\" />\n        </Item>\n      </Form>\n    </Card>\n  );\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/GeneralInfoForm/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ResumeDtoEnglishLevelEnum, ResumeDtoMilitaryServiceEnum } from '@client/api';\nimport { GeneralInfoForm } from './index';\n\nconst mockUserData = {\n  notes: 'Some interesting facts about me lalala lalala lala',\n  name: 'John Doe',\n  selfIntroLink: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',\n  militaryService: ResumeDtoMilitaryServiceEnum.NotLiable,\n  avatarLink: 'https://example.com/avatar.jpg',\n  desiredPosition: 'Cookies Engineer',\n  englishLevel: ResumeDtoEnglishLevelEnum.A2,\n  locations: 'Some, Loc, Ations',\n  startFrom: '2022-09-26',\n  fullTime: true,\n};\n\ndescribe('GeneralInfoForm', () => {\n  test('should render form items with proper values', async () => {\n    render(<GeneralInfoForm userData={mockUserData} />);\n\n    const name = await screen.findByDisplayValue(mockUserData.name);\n    const desiredPosition = await screen.findByDisplayValue(mockUserData.desiredPosition);\n    const locations = await screen.findByDisplayValue(mockUserData.locations);\n    const englishLevel = await screen.findByText(mockUserData.englishLevel);\n    const militaryService = await screen.findByText('Not liable');\n    const startFrom = await screen.findByDisplayValue(mockUserData.startFrom);\n    const fullTime = await screen.findByRole('checkbox', { name: /ready to work full time/i });\n    const avatarLink = await screen.findByDisplayValue(mockUserData.avatarLink);\n    const selfIntroLink = await screen.findByDisplayValue(mockUserData.selfIntroLink);\n    const notes = await screen.findByDisplayValue(mockUserData.notes);\n\n    expect(name).toBeInTheDocument();\n    expect(desiredPosition).toBeInTheDocument();\n    expect(locations).toBeInTheDocument();\n    expect(englishLevel).toBeInTheDocument();\n    expect(militaryService).toBeInTheDocument();\n    expect(startFrom).toBeInTheDocument();\n    expect(fullTime).toBeChecked();\n    expect(avatarLink).toBeInTheDocument();\n    expect(selfIntroLink).toBeInTheDocument();\n    expect(notes).toBeInTheDocument();\n  });\n\n  test.each`\n    label\n    ${'Name'}\n    ${'Desired position'}\n    ${'Locations'}\n    ${'Select your English level'}\n    ${'Military service'}\n    ${'Ready to start work from'}\n    ${'Ready to work full time'}\n    ${'Photo'}\n    ${'Self introduction video'}\n    ${'About me'}\n  `('should render field with $label label', async ({ label }) => {\n    render(<GeneralInfoForm userData={mockUserData} />);\n    const fieldLabel = await screen.findByLabelText(label);\n    expect(fieldLabel).toBeInTheDocument();\n  });\n\n  test('should render form items with proper placeholders', async () => {\n    render(<GeneralInfoForm userData={{ ...mockUserData, englishLevel: null, militaryService: null }} />);\n\n    const name = await screen.findByPlaceholderText('Enter your name');\n    const desiredPosition = await screen.findByPlaceholderText('Enter desired position');\n    const locations = await screen.findByPlaceholderText('Enter locations');\n    const englishLevel = await screen.findByText(/english level is not selected yet/i);\n    const militaryService = await screen.findByText(/military service status is not selected yet/i);\n    const startFrom = await screen.findByPlaceholderText('Not selected yet');\n    const avatarLink = await screen.findByPlaceholderText('Enter link to your photo');\n    const selfIntroLink = await screen.findByPlaceholderText('Link to video with self introduction');\n    const notes = await screen.findByPlaceholderText('Short info about you (50-1500 symbols)');\n\n    expect(name).toBeInTheDocument();\n    expect(desiredPosition).toBeInTheDocument();\n    expect(locations).toBeInTheDocument();\n    expect(englishLevel).toBeInTheDocument();\n    expect(militaryService).toBeInTheDocument();\n    expect(startFrom).toBeInTheDocument();\n    expect(avatarLink).toBeInTheDocument();\n    expect(selfIntroLink).toBeInTheDocument();\n    expect(notes).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/GeneralInfoForm/index.tsx",
    "content": "import { forwardRef, useEffect, ForwardedRef } from 'react';\nimport dayjs from 'dayjs';\nimport { Form, Input, Select, DatePicker, Checkbox, Card, FormInstance, Typography, Tooltip } from 'antd';\nimport { InfoCircleOutlined } from '@ant-design/icons';\nimport { UserData } from '@client/modules/Opportunities/models';\nimport { ENGLISH_LEVELS } from '@client/data/english';\nimport { userDataValidationRules as validationRules } from '../form-validation';\n\nconst { Item } = Form;\nconst { Option } = Select;\nconst { TextArea } = Input;\nconst { Text } = Typography;\n\nconst LabelWithTooltip = ({ label, tooltip }: { label: string; tooltip: string }) => (\n  <Text>\n    {label}{' '}\n    <Tooltip title={tooltip}>\n      <InfoCircleOutlined style={{ fontSize: 12, opacity: 0.7 }} />\n    </Tooltip>\n  </Text>\n);\n\ntype Props = {\n  userData: UserData;\n};\n\nexport const GeneralInfoForm = forwardRef((props: Props, ref: ForwardedRef<FormInstance>) => {\n  const { userData } = props;\n\n  const { avatarLink, name, desiredPosition, selfIntroLink, englishLevel, militaryService, notes, locations } =\n    userData;\n\n  const startFrom = userData.startFrom ? dayjs(userData.startFrom, 'YYYY.MM.DD') : undefined;\n  const fullTime = userData.fullTime ?? false;\n\n  const formValues = {\n    avatarLink,\n    name,\n    desiredPosition,\n    selfIntroLink,\n    englishLevel,\n    militaryService,\n    notes,\n    startFrom,\n    fullTime,\n    locations,\n  };\n\n  const [form] = Form.useForm();\n\n  useEffect(() => {\n    form.setFieldsValue(formValues);\n    form.validateFields();\n  }, [userData]);\n\n  const inputStyle = {\n    maxWidth: '400px',\n  };\n\n  return (\n    <Card title={<Text strong>General info</Text>} style={{ width: '70vw', marginBottom: '20px' }}>\n      <Form\n        form={form}\n        ref={ref}\n        name=\"userData\"\n        labelCol={{ span: 9 }}\n        wrapperCol={{ span: 10 }}\n        style={{ width: '100%' }}\n      >\n        <Item label=\"Name\" name=\"name\" rules={[...validationRules['name']]}>\n          <Input style={inputStyle} placeholder=\"Enter your name\" />\n        </Item>\n        <Item label=\"Desired position\" name=\"desiredPosition\" rules={[...validationRules['desiredPosition']]}>\n          <Input style={inputStyle} placeholder=\"Enter desired position\" />\n        </Item>\n        <Item\n          label={<LabelWithTooltip label=\"Locations\" tooltip=\"Up to 3, comma-separated\" />}\n          name=\"locations\"\n          rules={[...validationRules['locations']]}\n        >\n          <Input style={inputStyle} placeholder=\"Enter locations\" />\n        </Item>\n        <Item label=\"Select your English level\" name=\"englishLevel\" rules={[...validationRules['englishLevel']]}>\n          <Select style={inputStyle} placeholder=\"English level is not selected yet\">\n            {ENGLISH_LEVELS.map((level, idx) => (\n              <Option value={level} key={idx}>\n                {level}\n              </Option>\n            ))}\n          </Select>\n        </Item>\n        <Item label=\"Military service\" name=\"militaryService\" rules={[...validationRules['militaryService']]}>\n          <Select style={inputStyle} placeholder=\"Military service status is not selected yet\">\n            <Option value=\"served\">Served</Option>\n            <Option value=\"liable\">Liable</Option>\n            <Option value=\"notLiable\">Not liable</Option>\n          </Select>\n        </Item>\n        <Item label=\"Ready to start work from\" name=\"startFrom\" rules={[...validationRules['startFrom']]}>\n          <DatePicker\n            style={inputStyle}\n            placeholder=\"Not selected yet\"\n            picker=\"date\"\n            disabledDate={currDate => currDate.valueOf() < dayjs().subtract(1, 'days').valueOf()}\n          />\n        </Item>\n        <Item label=\"Ready to work full time\" colon={false} name=\"fullTime\" valuePropName=\"checked\">\n          <Checkbox />\n        </Item>\n        <Item\n          label={<LabelWithTooltip label=\"Photo\" tooltip=\"Direct link to the image\" />}\n          name=\"avatarLink\"\n          rules={[...validationRules['avatarLink']]}\n        >\n          <Input style={inputStyle} placeholder=\"Enter link to your photo\" />\n        </Item>\n        <Item label=\"Self introduction video\" name=\"selfIntroLink\" rules={[...validationRules['selfIntroLink']]}>\n          <Input style={inputStyle} placeholder=\"Link to video with self introduction\" />\n        </Item>\n        <Item label=\"About me\" name=\"notes\" rules={[...validationRules['notes']]}>\n          <TextArea\n            showCount\n            style={inputStyle}\n            maxLength={1500}\n            rows={4}\n            placeholder=\"Short info about you (50-1500 symbols)\"\n          />\n        </Item>\n      </Form>\n    </Card>\n  );\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/VisibleCoursesForm/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ResumeCourseDto } from '@client/api';\nimport { VisibleCoursesForm } from './index';\n\nconst mockCourses = [\n  {\n    id: 1,\n    fullName: 'Rolling Scopes School 2020 Q1: JavaScript/Front-end',\n    rank: 111,\n  },\n  {\n    id: 2,\n    fullName: 'Rolling Scopes School 2020 Q3: Node.js',\n    rank: 222,\n  },\n  {\n    id: 3,\n    fullName: 'Rolling Scopes School 2020 Q1: Machine Learning',\n    rank: 333,\n  },\n  {\n    id: 4,\n    fullName: 'Rolling Scopes School 2020 Q4: Android',\n    rank: 444,\n  },\n] as ResumeCourseDto[];\n\ndescribe('VisibleCoursesForm', () => {\n  test('should display all courses with positions', () => {\n    render(<VisibleCoursesForm courses={mockCourses} visibleCourses={[]} />);\n\n    mockCourses.forEach(({ fullName, rank }) => {\n      const courseName = screen.getByText(fullName);\n      const coursePosition = screen.getByText(`Position: ${rank}`);\n\n      expect(courseName).toBeInTheDocument();\n      expect(coursePosition).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/VisibleCoursesForm/index.tsx",
    "content": "import { ForwardedRef, forwardRef } from 'react';\nimport { Card, Checkbox, Form, FormInstance, Typography, Tooltip } from 'antd';\nimport { InfoCircleOutlined } from '@ant-design/icons';\nimport { ResumeCourseDto } from '@client/api';\n\nconst { Item } = Form;\nconst { Text } = Typography;\n\ntype Props = {\n  courses: ResumeCourseDto[] | null;\n  visibleCourses: number[];\n};\n\nexport const VisibleCoursesForm = forwardRef((props: Props, ref: ForwardedRef<FormInstance>) => {\n  const [form] = Form.useForm();\n  const { courses, visibleCourses } = props;\n\n  if (!courses?.length) return <Typography.Text>No courses to show</Typography.Text>;\n\n  const data = courses.reduce((acc: Record<string, boolean>, { id }) => {\n    acc[id] = visibleCourses.includes(id);\n    return acc;\n  }, {});\n\n  return (\n    <Card\n      title={\n        <Text>\n          Show RS Courses{' '}\n          <Tooltip title=\"Selected courses will be displayed in your CV\">\n            <InfoCircleOutlined style={{ fontSize: 12, opacity: 0.7 }} />\n          </Tooltip>\n        </Text>\n      }\n      style={{ width: '70vw' }}\n    >\n      <Form\n        style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '100%' }}\n        ref={ref}\n        initialValues={data}\n        form={form}\n        labelCol={{ span: 9, offset: 4 }}\n        wrapperCol={{ span: 10 }}\n      >\n        {courses.map(({ id, fullName, rank }) => (\n          <Item\n            key={id}\n            name={id}\n            colon={false}\n            label={\n              <div style={{ whiteSpace: 'normal' }}>\n                <Text>{fullName}</Text>\n                <br />\n                <Text type=\"secondary\">Position: {rank}</Text>\n              </div>\n            }\n            style={{ overflow: 'hidden' }}\n            valuePropName=\"checked\"\n          >\n            <Checkbox />\n          </Item>\n        ))}\n      </Form>\n    </Card>\n  );\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/form-validation.ts",
    "content": "import { Rule } from 'antd/lib/form';\nimport { githubUsernamePattern } from '@client/services/validators';\n\nconst validationMessages = {\n  required: \"Field can't be empty\",\n  min: (length: number): string => `Minimal text length is ${length} symbols`,\n  max: (length: number): string => `Maximal text length is ${length} symbols`,\n  whitespace: \"Field can't contain only whitespaces\",\n  invalid: (fieldName: string): string => `This is not a valid ${fieldName}`,\n};\n\nexport const contactsValidationRules = {\n  phone: [\n    {\n      max: 25,\n      message: validationMessages.max(25),\n    },\n    () => ({\n      async validator(_, value) {\n        /* phonePattern is created inside of the validation function\n        due to a bug where the validation does not work correctly\n        if the phonePattern is taken from the upper scope */\n        const phonePattern = /^\\+[1-9]{1}[0-9]{3,14}$/gi;\n        if (!value || phonePattern.test(value)) {\n          return Promise.resolve();\n        }\n        throw new Error(validationMessages.invalid('phone number'));\n      },\n    }),\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ] as Rule[],\n  email: [\n    {\n      type: 'email',\n      message: validationMessages.invalid('email'),\n    },\n    {\n      max: 50,\n      message: validationMessages.max(50),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  skype: [\n    {\n      max: 30,\n      message: validationMessages.max(30),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  telegram: [\n    {\n      max: 30,\n      message: validationMessages.max(30),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  linkedin: [\n    {\n      type: 'url',\n      message: validationMessages.invalid('URL'),\n    },\n  ],\n  github: [\n    {\n      max: 30,\n      message: validationMessages.max(30),\n    },\n    () => ({\n      async validator(_, value) {\n        if (!value || githubUsernamePattern.test(value)) {\n          return Promise.resolve();\n        }\n        throw new Error(validationMessages.invalid('github username'));\n      },\n    }),\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ] as Rule[],\n  website: [\n    {\n      type: 'url',\n      message: validationMessages.invalid('URL'),\n    },\n    {\n      max: 100,\n      message: validationMessages.max(100),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n} as const;\n\nexport const userDataValidationRules = {\n  name: [\n    {\n      required: true,\n      message: validationMessages.required,\n    },\n    {\n      max: 100,\n      message: validationMessages.max(100),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  desiredPosition: [\n    {\n      required: true,\n      message: validationMessages.required,\n    },\n    {\n      max: 300,\n      message: validationMessages.max(300),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  locations: [\n    {\n      required: true,\n      message: validationMessages.required,\n    },\n    {\n      max: 300,\n      message: validationMessages.max(300),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  selfIntroLink: [\n    {\n      type: 'url',\n      message: validationMessages.invalid('URL'),\n    },\n    {\n      max: 300,\n      message: validationMessages.max(300),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  avatarLink: [\n    {\n      type: 'url',\n      message: validationMessages.invalid('URL'),\n    },\n    {\n      max: 300,\n      message: validationMessages.max(300),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n  englishLevel: [\n    {\n      required: true,\n      message: validationMessages.required,\n    },\n  ],\n  militaryService: [\n    {\n      required: true,\n      message: validationMessages.required,\n    },\n  ],\n  startFrom: [\n    {\n      required: true,\n      message: validationMessages.required,\n    },\n  ],\n  notes: [\n    {\n      max: 1500,\n      message: validationMessages.max(1500),\n    },\n    {\n      min: 50,\n      message: validationMessages.min(50),\n    },\n    {\n      whitespace: true,\n      message: validationMessages.whitespace,\n    },\n  ],\n} as const;\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/index.test.tsx",
    "content": "import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { forwardRef } from 'react';\nimport { AxiosResponse } from 'axios';\nimport { Contacts, UserData } from '@client/modules/Opportunities/models';\nimport { OpportunitiesApi } from '@client/api';\nimport { EditCV } from './index';\n\nconst mockSuccessNotification = vi.fn();\nvi.mock('@client/hooks', () => ({\n  useMessage: () => ({\n    notification: {\n      success: mockSuccessNotification,\n    },\n  }),\n}));\n\nconst enum EditCvForms {\n  GeneralInfoForm,\n  ContactsForm,\n  VisibleCoursesForm,\n}\n\nvi.mock('./GeneralInfoForm', () => ({\n  GeneralInfoForm: forwardRef(() => <div>{EditCvForms.GeneralInfoForm}</div>),\n}));\n\nvi.mock('./ContactsForm', () => ({\n  ContactsForm: forwardRef(() => <div>{EditCvForms.ContactsForm}</div>),\n}));\n\nvi.mock('./VisibleCoursesForm', () => ({\n  VisibleCoursesForm: forwardRef(() => <div>{EditCvForms.VisibleCoursesForm}</div>),\n}));\n\nconst mockGithubId = 'some-github';\n\nconst mockSwitchView = vi.fn();\nconst mockOnUpdateResume = vi.fn();\n\ndescribe('EditCV', () => {\n  test('should display forms and control buttons', () => {\n    render(\n      <EditCV\n        githubId={mockGithubId}\n        contacts={{} as Contacts}\n        userData={{} as UserData}\n        switchView={mockSwitchView}\n        onUpdateResume={mockOnUpdateResume}\n        visibleCourses={[]}\n        courses={[]}\n      />,\n    );\n\n    const generalInfoForm = screen.getByText(EditCvForms.GeneralInfoForm);\n    const contactsForm = screen.getByText(EditCvForms.ContactsForm);\n    const visibleCoursesForm = screen.getByText(EditCvForms.VisibleCoursesForm);\n    const saveButton = screen.getByRole('button', { name: /save cv/i });\n    const cancelButton = screen.getByRole('button', { name: /cancel/i });\n\n    expect(generalInfoForm).toBeInTheDocument();\n    expect(contactsForm).toBeInTheDocument();\n    expect(visibleCoursesForm).toBeInTheDocument();\n    expect(saveButton).toBeInTheDocument();\n    expect(cancelButton).toBeInTheDocument();\n  });\n\n  test('should switch view on Cancel button click', () => {\n    render(\n      <EditCV\n        githubId={mockGithubId}\n        contacts={{} as Contacts}\n        userData={{} as UserData}\n        switchView={mockSwitchView}\n        onUpdateResume={mockOnUpdateResume}\n        visibleCourses={[]}\n        courses={[]}\n      />,\n    );\n\n    const cancelButton = screen.getByRole('button', { name: /cancel/i });\n\n    fireEvent.click(cancelButton);\n\n    expect(mockSwitchView).toHaveBeenCalled();\n  });\n\n  test('should show notification view on Cancel button click', () => {\n    render(\n      <EditCV\n        githubId={mockGithubId}\n        contacts={{} as Contacts}\n        userData={{} as UserData}\n        switchView={mockSwitchView}\n        onUpdateResume={mockOnUpdateResume}\n        visibleCourses={[]}\n        courses={[]}\n      />,\n    );\n\n    const cancelButton = screen.getByRole('button', { name: /cancel/i });\n\n    fireEvent.click(cancelButton);\n\n    expect(mockSwitchView).toHaveBeenCalled();\n  });\n\n  test('should save data on Save button click and show success notification', async () => {\n    const mockSaveResume = vi\n      .spyOn(OpportunitiesApi.prototype, 'saveResume')\n      .mockResolvedValue({ data: {} } as AxiosResponse);\n\n    render(\n      <EditCV\n        githubId={mockGithubId}\n        contacts={{} as Contacts}\n        userData={{} as UserData}\n        switchView={mockSwitchView}\n        onUpdateResume={mockOnUpdateResume}\n        visibleCourses={[]}\n        courses={[]}\n      />,\n    );\n\n    const saveButton = await screen.findByRole('button', { name: /save cv/i });\n\n    fireEvent.click(saveButton);\n\n    expect(mockSaveResume).toHaveBeenCalledWith(mockGithubId, expect.any(Object));\n\n    await waitFor(() => {\n      expect(mockOnUpdateResume).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditCv/index.tsx",
    "content": "import { createRef, RefObject, useState } from 'react';\nimport { Alert, Button, Col, Layout, Row, Space } from 'antd';\nimport { SaveOutlined } from '@ant-design/icons';\nimport { FormInstance } from 'antd/lib/form';\nimport { FormDataDto, OpportunitiesApi, ResumeCourseDto } from '@client/api';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport { ContactsForm } from './ContactsForm';\nimport { GeneralInfoForm } from './GeneralInfoForm';\nimport { VisibleCoursesForm } from './VisibleCoursesForm';\nimport {\n  AllDataToSubmit,\n  AllUserCVData,\n  Contacts,\n  UserData,\n  UserDataToSubmit,\n  VisibleCourses,\n  VisibleCoursesFormData,\n} from '@client/modules/Opportunities/models';\nimport { splitDataForForms, transformFieldsData } from '@client/modules/Opportunities/transformers';\nimport { useMessage } from '@client/hooks';\n\nconst { Content } = Layout;\n\ntype Props = {\n  githubId: string;\n  contacts: Contacts | null;\n  userData: UserData | null;\n  visibleCourses: number[];\n  courses: ResumeCourseDto[] | null;\n  switchView: () => void;\n  onUpdateResume?: () => void;\n};\n\nconst service = new OpportunitiesApi();\n\nconst buttonStyle = { width: 'fit-content', margin: '5px' };\n\nexport const EditCV = (props: Props) => {\n  const { notification } = useMessage();\n  const [loading, setLoading] = useState<boolean>(false);\n  const [contacts, setContacts] = useState<Contacts | null>(props.contacts);\n  const [userData, setUserData] = useState<UserData | null>(props.userData);\n  const [visibleCourses, setVisibleCourses] = useState<number[] | null>(props.visibleCourses);\n  const [validationFailed, setValidationFailed] = useState<boolean>(false);\n\n  const userFormRef: RefObject<FormInstance> = createRef();\n  const contactsFormRef: RefObject<FormInstance> = createRef();\n  const visibleCoursesFormRef: RefObject<FormInstance> = createRef();\n\n  const submitData = async (data: AllUserCVData) => {\n    const newData = await service.saveResume(props.githubId, data as FormDataDto);\n\n    const { userData, contacts, visibleCourses } = splitDataForForms(newData.data as AllUserCVData);\n\n    setUserData(userData);\n    setContacts(contacts);\n    setVisibleCourses(visibleCourses);\n\n    props.onUpdateResume?.();\n  };\n\n  const saveData = async (data: AllDataToSubmit) => {\n    const dataToSubmit = transformFieldsData(data);\n    await submitData(dataToSubmit);\n  };\n\n  const hasInvalidFields = (form: FormInstance | null) =>\n    !form ? false : form.getFieldsError().some(field => field.errors.length);\n\n  const getDataFromForms = () => {\n    const userFormData: UserDataToSubmit = userFormRef.current?.getFieldsValue();\n    const contactsFormData: Contacts = contactsFormRef.current?.getFieldsValue();\n    const visibleCoursesFormData: VisibleCoursesFormData = visibleCoursesFormRef.current?.getFieldsValue() ?? {};\n\n    const visibleCourses = Object.entries(visibleCoursesFormData).reduce<VisibleCourses>((acc, [id, isVisible]) => {\n      if (isVisible) acc.push(Number(id));\n      return acc;\n    }, []);\n\n    return {\n      ...userFormData,\n      ...contactsFormData,\n      visibleCourses,\n    };\n  };\n\n  const handleSave = async () => {\n    if (hasInvalidFields(userFormRef.current) || hasInvalidFields(contactsFormRef.current)) {\n      setValidationFailed(true);\n      setTimeout(() => setValidationFailed(false), 2000);\n      return;\n    }\n\n    const values = getDataFromForms();\n\n    setLoading(true);\n\n    await saveData(values);\n\n    setLoading(false);\n\n    notification.success({ message: 'CV successfully updated', duration: 2 });\n  };\n\n  return (\n    <LoadingScreen show={loading}>\n      <Content>\n        <Space\n          direction=\"horizontal\"\n          align=\"start\"\n          style={{\n            width: '100%',\n            display: 'flex',\n            justifyContent: 'space-around',\n          }}\n        >\n          <Col\n            style={{\n              margin: '10px auto',\n              maxWidth: '960px',\n            }}\n          >\n            <Row justify=\"center\" style={{ marginBottom: '15px' }}>\n              <Button\n                style={{\n                  ...buttonStyle,\n                  backgroundColor: '#52C41A',\n                  borderColor: '#52C41A',\n                }}\n                type=\"primary\"\n                color=\"green\"\n                htmlType=\"button\"\n                onClick={handleSave}\n                icon={<SaveOutlined />}\n              >\n                Save CV\n              </Button>\n              <Button style={buttonStyle} type=\"default\" htmlType=\"button\" onClick={() => props.switchView()}>\n                Cancel\n              </Button>\n            </Row>\n            <Row>\n              {validationFailed ? (\n                <Alert\n                  style={{ marginBottom: '10px', width: '100%' }}\n                  showIcon\n                  type=\"error\"\n                  message=\"All required fields must be filled first\"\n                />\n              ) : null}\n            </Row>\n            <Row>{userData && <GeneralInfoForm ref={userFormRef} userData={userData} />}</Row>\n            <Row>{contacts && <ContactsForm ref={contactsFormRef} contactsList={contacts} />}</Row>\n            <Row>\n              {visibleCourses && (\n                <VisibleCoursesForm\n                  ref={visibleCoursesFormRef}\n                  courses={props.courses}\n                  visibleCourses={visibleCourses}\n                />\n              )}\n            </Row>\n          </Col>\n        </Space>\n      </Content>\n    </LoadingScreen>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditViewCv/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { EditViewCv, ResumeProps } from './index';\n\nconst enum CvComponents {\n  NoConsentView,\n  EditCV,\n  ViewCV,\n}\n\nvi.mock('../NoConsentView', () => ({\n  NoConsentView: () => <div>{CvComponents.NoConsentView}</div>,\n}));\n\nvi.mock('../EditCv', () => ({\n  EditCV: () => <div>{CvComponents.EditCV}</div>,\n}));\n\nvi.mock('../ViewCv', () => ({\n  ViewCV: () => <div>{CvComponents.ViewCV}</div>,\n}));\n\ndescribe('EditViewCv', () => {\n  test('should display no consent view', () => {\n    const mockProps = { consent: false } as ResumeProps;\n\n    render(<EditViewCv {...mockProps} />);\n\n    const noConsentView = screen.getByText(CvComponents.NoConsentView);\n    const editCv = screen.queryByText(CvComponents.EditCV);\n    const viewCv = screen.queryByText(CvComponents.ViewCV);\n\n    expect(noConsentView).toBeInTheDocument();\n    expect(editCv).not.toBeInTheDocument();\n    expect(viewCv).not.toBeInTheDocument();\n  });\n\n  test('should display EditCv in case of edit mode enabled', () => {\n    const mockProps = { consent: true, editMode: true } as ResumeProps;\n\n    render(<EditViewCv {...mockProps} />);\n\n    const noConsentView = screen.queryByText(CvComponents.NoConsentView);\n    const editCv = screen.getByText(CvComponents.EditCV);\n    const viewCv = screen.queryByText(CvComponents.ViewCV);\n\n    expect(noConsentView).not.toBeInTheDocument();\n    expect(editCv).toBeInTheDocument();\n    expect(viewCv).not.toBeInTheDocument();\n  });\n\n  test('should display EditCv in case of no data available', () => {\n    const mockProps = { consent: true, editMode: false, data: null } as ResumeProps;\n\n    render(<EditViewCv {...mockProps} />);\n\n    const noConsentView = screen.queryByText(CvComponents.NoConsentView);\n    const editCv = screen.getByText(CvComponents.EditCV);\n    const viewCv = screen.queryByText(CvComponents.ViewCV);\n\n    expect(noConsentView).not.toBeInTheDocument();\n    expect(editCv).toBeInTheDocument();\n    expect(viewCv).not.toBeInTheDocument();\n  });\n\n  test('should display ViewCv in case no edit mode and data available', () => {\n    const mockProps = { consent: true, editMode: false, data: {} } as ResumeProps;\n\n    render(<EditViewCv {...mockProps} />);\n\n    const noConsentView = screen.queryByText(CvComponents.NoConsentView);\n    const editCv = screen.queryByText(CvComponents.EditCV);\n    const viewCv = screen.getByText(CvComponents.ViewCV);\n\n    expect(noConsentView).not.toBeInTheDocument();\n    expect(editCv).not.toBeInTheDocument();\n    expect(viewCv).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/EditViewCv/index.tsx",
    "content": "import { ResumeDto } from '@client/api';\nimport { transformInitialCvData } from '../../transformers/transformInitialCvData';\nimport { EditCV } from '../EditCv';\nimport { ViewCV } from '../ViewCv';\nimport { NoConsentView } from '../NoConsentView';\n\nexport type ResumeProps = {\n  githubId: string;\n  consent: boolean;\n  error?: Error;\n  data: ResumeDto | null;\n  editMode: boolean;\n  switchView: () => void;\n  onRemoveConsent: () => void;\n  onCreateConsent: () => void;\n  onUpdateResume?: () => void;\n};\n\nexport const EditViewCv = (props: ResumeProps) => {\n  const { githubId, data, consent, editMode, switchView, onUpdateResume, onRemoveConsent, onCreateConsent } = props;\n\n  if (!consent) {\n    return <NoConsentView isOwner={true} giveConsent={onCreateConsent} />;\n  }\n\n  const { userData, contacts, visibleCourses, courses } = transformInitialCvData(data);\n\n  const editing = editMode || data == null;\n\n  return (\n    <>\n      {editing ? (\n        <EditCV\n          courses={courses}\n          userData={userData}\n          contacts={contacts}\n          visibleCourses={visibleCourses}\n          onUpdateResume={onUpdateResume}\n          githubId={githubId}\n          switchView={switchView}\n        />\n      ) : (\n        <ViewCV onRemoveConsent={onRemoveConsent} switchView={switchView} initialData={data} />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ExpirationTooltip/index.test.tsx",
    "content": "import { render, screen, fireEvent, within, waitFor } from '@testing-library/react';\nimport { ExpirationState } from '@client/modules/Opportunities/constants';\nimport { ExpirationTooltip } from './index';\n\nconst mockSystemTime = new Date('2022-09-26T13:41:39.161Z');\n\ndescribe('ExpirationTooltip', () => {\n  beforeAll(() => {\n    vi.useFakeTimers({ shouldAdvanceTime: true }).setSystemTime(mockSystemTime);\n  });\n\n  afterAll(() => {\n    vi.useRealTimers();\n  });\n\n  test('should show corresponding title and text in case if CV is far from expiration', async () => {\n    const datestring30DaysAfter = '2022-10-26';\n\n    render(<ExpirationTooltip expirationDate={datestring30DaysAfter} expirationState={ExpirationState.NotExpired} />);\n\n    const button = screen.getByRole('button', { name: 'Public' });\n    const expirationText = screen.getByText(`Expires on ${datestring30DaysAfter}`);\n\n    expect(button).toBeInTheDocument();\n    expect(expirationText).toBeInTheDocument();\n\n    fireEvent.click(button);\n\n    const modal = await screen.findByRole('dialog');\n\n    expect(modal).toBeInTheDocument();\n    const title = within(modal).getAllByText(`Your CV is public until ${datestring30DaysAfter}`)[0];\n    const text = within(modal).getByText(/If you won't renew your CV until this date/);\n    expect(title).toBeInTheDocument();\n    expect(text).toBeInTheDocument();\n    expect(within(modal).getAllByRole('button')).toHaveLength(2);\n\n    // Modal is rendered outside of the container, this is custom cleanup\n    modal.remove();\n  });\n\n  test('should show corresponding title and text in case if CV is nearly expired', async () => {\n    const datestring1DayAfter = '2022-09-27';\n\n    render(<ExpirationTooltip expirationDate={datestring1DayAfter} expirationState={ExpirationState.NearlyExpired} />);\n\n    const button = await screen.findByRole('button', { name: 'Public' });\n    const expirationText = await screen.findByText(`Expires on ${datestring1DayAfter}`);\n\n    expect(button).toBeInTheDocument();\n    expect(expirationText).toBeInTheDocument();\n\n    fireEvent.click(button);\n\n    const modal = await screen.findByRole('dialog');\n\n    expect(modal).toBeInTheDocument();\n\n    const title = within(modal).getAllByText(`Your CV will expire in 2 days on ${datestring1DayAfter}`)[0];\n    const text = within(modal).getByText(/If you won't renew your CV until this date/);\n\n    expect(title).toBeInTheDocument();\n    expect(text).toBeInTheDocument();\n\n    // Modal is rendered outside of the container, this is custom cleanup\n    modal.remove();\n  });\n\n  test('should show expiration modal without click on button in case if CV is expired in no public mode', async () => {\n    const datestring1DayBefore = '2022-09-25';\n\n    render(<ExpirationTooltip expirationDate={datestring1DayBefore} expirationState={ExpirationState.Expired} />);\n\n    const button = await screen.findByRole('button', { name: 'Archived' });\n    const expirationText = await screen.findByText(`Expired on ${datestring1DayBefore}`);\n\n    expect(button).toBeInTheDocument();\n    expect(expirationText).toBeInTheDocument();\n\n    const modal = await screen.findByRole('dialog');\n\n    expect(modal).toBeInTheDocument();\n\n    const title = within(modal).getAllByText('Your CV is archived')[0];\n    const text = within(modal).getByText(/You need to renew your resume/i);\n\n    expect(title).toBeInTheDocument();\n    expect(text).toBeInTheDocument();\n\n    // Modal is rendered outside of the container, this is custom cleanup\n    modal.remove();\n  });\n\n  test('should show expiration modal on click in case if CV is expired in no public mode', async () => {\n    const datestring1DayBefore = '2022-09-25';\n\n    render(<ExpirationTooltip expirationDate={datestring1DayBefore} expirationState={ExpirationState.Expired} />);\n\n    // Close initially opened modal\n    fireEvent.click(await screen.findByText('Cancel'));\n\n    const button = await screen.findByRole('button', { name: 'Archived' });\n\n    fireEvent.click(button);\n\n    const modal = await screen.findByRole('dialog');\n\n    expect(modal).toBeInTheDocument();\n\n    const title = within(modal).getAllByText('Your CV is archived')[0];\n    const text = within(modal).getByText(/You need to renew your resume/i);\n\n    expect(title).toBeInTheDocument();\n    expect(text).toBeInTheDocument();\n\n    // Modal is rendered outside of the container, this is custom cleanup\n    modal.remove();\n  });\n\n  test('should not show expiration modal in public mode', async () => {\n    const datestring1DayBefore = '2022-09-25';\n\n    render(\n      <ExpirationTooltip\n        publicMode={true}\n        expirationDate={datestring1DayBefore}\n        expirationState={ExpirationState.Expired}\n      />,\n    );\n\n    const button = await screen.findByRole('button', { name: 'Archived' });\n\n    await waitFor(() => {\n      const modal = screen.queryByRole('dialog');\n      expect(modal).not.toBeInTheDocument();\n    });\n\n    fireEvent.click(button);\n\n    await waitFor(() => {\n      const modal = screen.queryByRole('dialog');\n      expect(modal).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ExpirationTooltip/index.tsx",
    "content": "import { Typography, Button, Modal } from 'antd';\nimport { useCallback } from 'react';\nimport { useEffectOnce } from 'react-use';\nimport {\n  ExclamationCircleTwoTone,\n  QuestionCircleOutlined,\n  CloseCircleTwoTone,\n  ClockCircleOutlined,\n} from '@ant-design/icons';\nimport { OpportunitiesApi } from '@client/api';\nimport { ExpirationState } from '@client/modules/Opportunities/constants';\nimport { useMessage } from '@client/hooks';\n\nconst { Text, Paragraph } = Typography;\n\nconst service = new OpportunitiesApi();\n\ntype Props = {\n  expirationDate: string;\n  expirationState: ExpirationState;\n  publicMode?: boolean;\n};\n\nexport const ExpirationTooltip = ({ expirationDate, expirationState, publicMode }: Props) => {\n  const { notification } = useMessage();\n  const [modal, contextHolder] = Modal.useModal();\n\n  const textStyle = { fontSize: '12px' };\n\n  useEffectOnce(() => {\n    if (expirationState === ExpirationState.Expired && !publicMode) showRenewModal();\n  });\n\n  const showRenewModal = useCallback(() => {\n    if (publicMode) return;\n\n    const title =\n      expirationState === ExpirationState.NotExpired\n        ? `Your CV is public until ${expirationDate}`\n        : expirationState === ExpirationState.NearlyExpired\n          ? `Your CV will expire in 2 days on ${expirationDate}`\n          : 'Your CV is archived';\n    const content =\n      expirationState === ExpirationState.Expired ? (\n        <Paragraph>You need to renew your resume to make it visible to other users again.</Paragraph>\n      ) : (\n        <Paragraph>\n          If you won't renew your CV until this date, it will get archived and will not be visible to other users of RS\n          App.\n        </Paragraph>\n      );\n    const icon =\n      expirationState === ExpirationState.NotExpired ? (\n        <ExclamationCircleTwoTone twoToneColor=\"#1677ff\" />\n      ) : expirationState === ExpirationState.Expired ? (\n        <CloseCircleTwoTone twoToneColor=\"#FF4D4F\" />\n      ) : (\n        <ExclamationCircleTwoTone twoToneColor=\"#FFA940\" />\n      );\n\n    modal.confirm({\n      icon,\n      title,\n      content,\n      okText: (\n        <span>\n          <ClockCircleOutlined /> Renew\n        </span>\n      ),\n      maskClosable: true,\n      onOk: async () => {\n        await service.prolong();\n        notification.success({\n          message: 'CV successfully renewed',\n          placement: 'topRight',\n          closeIcon: ' ',\n          duration: 2,\n        });\n      },\n    });\n  }, [expirationDate, expirationState]);\n\n  const PublicButton = () => (\n    <Button\n      size=\"small\"\n      onClick={showRenewModal}\n      style={{ backgroundColor: '#F6FFED', color: '#52C41A', borderColor: '#B7EB8F' }}\n    >\n      Public\n    </Button>\n  );\n\n  const ArchivedButton = () => (\n    <Button\n      size=\"small\"\n      onClick={showRenewModal}\n      style={{ backgroundColor: '#FFF1F0', color: '#F5222D', borderColor: '#FFA39E' }}\n    >\n      Archived\n    </Button>\n  );\n\n  const expiredContent = (\n    <>\n      <Text style={{ ...textStyle, color: '#FF4D4F' }}>\n        Expired on {expirationDate} <QuestionCircleOutlined />\n      </Text>{' '}\n      <ArchivedButton />\n    </>\n  );\n\n  const nearlyExpiredContent = (\n    <>\n      <Text style={{ ...textStyle, color: '#FAAD14' }}>\n        Expires on {expirationDate} <QuestionCircleOutlined />\n      </Text>{' '}\n      <PublicButton />\n    </>\n  );\n\n  const notExpiredContent = (\n    <>\n      <Text style={{ ...textStyle, color: '#FFFFFF' }}>\n        Expires on {expirationDate} <QuestionCircleOutlined />\n      </Text>{' '}\n      <PublicButton />\n    </>\n  );\n\n  let content;\n\n  switch (expirationState) {\n    case ExpirationState.Expired:\n      content = expiredContent;\n      break;\n\n    case ExpirationState.NearlyExpired:\n      content = nearlyExpiredContent;\n      break;\n\n    case ExpirationState.NotExpired:\n      content = notExpiredContent;\n      break;\n\n    default:\n      return null;\n  }\n\n  return (\n    <>\n      {contextHolder}\n      <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%' }} className=\"no-print\">\n        {content}\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/Link/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Link } from './index';\n\nconst mockUrl = 'https://example.com';\nconst mockTitle = 'Example title';\nconst mockText = 'Example text';\n\ndescribe('Link', () => {\n  test('should render working link without title and text', () => {\n    render(<Link url={mockUrl} />);\n\n    const link = screen.getByRole('link');\n\n    expect(link).toHaveAttribute('href', mockUrl);\n    expect(link).toHaveClass('rs-link');\n  });\n\n  test('should render working link with title and text', () => {\n    render(<Link url={mockUrl} title={mockTitle} text={mockText} />);\n\n    const link = screen.getByTitle(mockTitle);\n\n    expect(link).toHaveAttribute('href', mockUrl);\n    expect(screen.getByText(mockText)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/Link/index.tsx",
    "content": "type Props = {\n  url: string;\n  title?: string;\n  text?: string;\n};\n\nexport const Link = ({ url, text, title }: Props) => {\n  return (\n    <a title={title} className=\"rs-link\" target=\"_blank\" rel=\"nofollow\" href={url}>\n      {text}\n    </a>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/NameTitle/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { UserData } from '@client/modules/Opportunities/models';\nimport { NameTitle } from './index';\n\nvi.mock('../AvatarCv', () => ({\n  AvatarCv: ({ src }: { src: string }) => <div>{src}</div>,\n}));\n\nconst mockUserData = {\n  name: 'Example name',\n  avatarLink: 'https://example.com',\n  desiredPosition: 'Example position',\n};\n\ndescribe('NameTitle', () => {\n  test('should be displayed correctly', () => {\n    render(<NameTitle userData={mockUserData as UserData} />);\n\n    const displayedMockAvatar = screen.getByText(mockUserData.avatarLink);\n    const displayedName = screen.getByText(mockUserData.name);\n    const displayedPosition = screen.getByText(mockUserData.desiredPosition);\n\n    expect(displayedMockAvatar).toBeInTheDocument();\n    expect(displayedName).toBeInTheDocument();\n    expect(displayedPosition).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/NameTitle/index.tsx",
    "content": "import { UserData } from '@client/modules/Opportunities/models';\nimport { AvatarCv } from '../AvatarCv';\n\ntype Props = {\n  userData: UserData;\n};\n\nexport const NameTitle = ({ userData }: Props) => {\n  const { name, avatarLink, desiredPosition } = userData;\n\n  return (\n    <div style={{ display: 'flex', flexDirection: 'column' }}>\n      <div>\n        <div style={{ paddingRight: 16 }}>\n          <AvatarCv src={avatarLink} />\n        </div>\n      </div>\n      <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>\n        <div style={{ fontSize: 24, lineHeight: '28px' }}>{name}</div>\n        <div style={{ fontSize: 18, paddingTop: 8 }}>{desiredPosition}</div>\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/NoConsentView/index.test.tsx",
    "content": "import { render, screen, waitFor, fireEvent } from '@testing-library/react';\nimport { NoConsentView, confirmationModalInfo } from '../NoConsentView';\n\ndescribe('NoConsentView', () => {\n  it('should render 403 correctly', () => {\n    render(<NoConsentView giveConsent={vi.fn()} />);\n\n    expect(screen.getByText(\"This user doesn't have CV yet\")).toBeInTheDocument();\n  });\n\n  it('should render initial owner view correctly', () => {\n    render(<NoConsentView isOwner={true} giveConsent={vi.fn()} />);\n\n    const title = screen.getByRole('heading', { name: \"You don't have a CV yet.\" });\n    const createCvButton = screen.getByRole('button', { name: 'plus Create CV' });\n\n    expect(title).toBeInTheDocument();\n    expect(createCvButton).toBeInTheDocument();\n  });\n\n  it('should show confirmation modal', async () => {\n    render(<NoConsentView isOwner={true} giveConsent={vi.fn()} />);\n\n    const createCvButton = screen.getByRole('button', { name: 'plus Create CV' });\n\n    fireEvent.click(createCvButton);\n\n    const modal = await screen.findByRole('dialog');\n    const modalTitle = await screen.findByText(confirmationModalInfo.en.header);\n\n    expect(modal).toBeInTheDocument();\n    expect(modalTitle).toBeInTheDocument();\n\n    // close modal\n    fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n\n    await waitFor(() => {\n      const modal = screen.queryByRole('dialog');\n      expect(modal).not.toBeInTheDocument();\n    });\n  });\n\n  it('should render tooltip', async () => {\n    render(<NoConsentView isOwner={true} giveConsent={vi.fn()} />);\n\n    const createCvButton = screen.getByRole('button', { name: 'plus Create CV' });\n\n    fireEvent.click(createCvButton);\n\n    const titleTooltipIcon = await screen.findByTestId(confirmationModalInfo.ru.header);\n    expect(titleTooltipIcon).toBeInTheDocument();\n\n    fireEvent.mouseEnter(titleTooltipIcon);\n\n    await waitFor(() => {\n      expect(titleTooltipIcon).toHaveAttribute('data-testid', confirmationModalInfo.ru.header);\n    });\n\n    // close modal\n    fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n\n    await waitFor(() => {\n      const modal = screen.queryByRole('dialog');\n      expect(modal).not.toBeInTheDocument();\n    });\n  });\n\n  it.each`\n    text\n    ${confirmationModalInfo.en.availableDataList[0]}\n    ${confirmationModalInfo.en.availableDataList[1]}\n    ${confirmationModalInfo.en.availableDataList[2]}\n    ${confirmationModalInfo.en.availableDataList[3]}\n  `('should render visible text $text', async ({ text }) => {\n    render(<NoConsentView isOwner={true} giveConsent={vi.fn()} />);\n\n    const createCvButton = screen.getByRole('button', { name: 'plus Create CV' });\n\n    fireEvent.click(createCvButton);\n\n    await waitFor(() => {\n      expect(screen.getByText(text)).toBeInTheDocument();\n    });\n\n    // close modal\n    fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n\n    await waitFor(() => {\n      const modal = screen.queryByRole('dialog');\n      expect(modal).not.toBeInTheDocument();\n    });\n  });\n\n  it.each`\n    text\n    ${confirmationModalInfo.ru.availableDataList[0]}\n    ${confirmationModalInfo.ru.availableDataList[1]}\n    ${confirmationModalInfo.ru.availableDataList[2]}\n    ${confirmationModalInfo.ru.availableDataList[3]}\n  `('should render tooltip $text', async ({ text }) => {\n    render(<NoConsentView isOwner={true} giveConsent={vi.fn()} />);\n\n    const createCvButton = screen.getByRole('button', { name: 'plus Create CV' });\n\n    fireEvent.click(createCvButton);\n\n    const tooltipIcon = await screen.findByTestId(text);\n    expect(tooltipIcon).toBeInTheDocument();\n\n    fireEvent.mouseEnter(tooltipIcon);\n\n    await waitFor(() => {\n      expect(tooltipIcon).toHaveAttribute('data-testid', text);\n    });\n\n    // close modal\n    fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));\n\n    await waitFor(() => {\n      const modal = screen.queryByRole('dialog');\n      expect(modal).not.toBeInTheDocument();\n    });\n  });\n\n  it('should handle cancel correctly', async () => {\n    render(<NoConsentView isOwner={true} giveConsent={vi.fn()} />);\n\n    const createCvButton = screen.getByRole('button', { name: 'plus Create CV' });\n    expect(createCvButton).toBeInTheDocument();\n\n    fireEvent.click(createCvButton);\n\n    const cancelButton = await screen.findByRole('button', { name: 'Cancel' });\n    expect(cancelButton).toBeInTheDocument();\n\n    // close modal\n    fireEvent.click(cancelButton);\n\n    await waitFor(() => {\n      const modal = screen.queryByRole('dialog');\n      expect(modal).not.toBeInTheDocument();\n    });\n  });\n\n  it('should handle consent correctly', async () => {\n    const mockGiveConsent = vi.fn();\n\n    render(<NoConsentView isOwner={true} giveConsent={mockGiveConsent} />);\n\n    const createCvButton = screen.getByRole('button', { name: 'plus Create CV' });\n\n    fireEvent.click(createCvButton);\n\n    const consentButton = await screen.findByRole('button', { name: 'I consent' });\n\n    fireEvent.click(consentButton);\n\n    expect(mockGiveConsent).toHaveBeenCalled();\n\n    await waitFor(() => {\n      expect(screen.queryByRole('dialog')).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/NoConsentView/index.tsx",
    "content": "import { Button, Modal, List, Result, Tooltip, Typography } from 'antd';\nimport { QuestionCircleOutlined, PlusOutlined } from '@ant-design/icons';\n\nconst { Paragraph, Title, Text } = Typography;\nconst { Item } = List;\n\ntype Props = {\n  isOwner?: boolean;\n  giveConsent: () => void;\n};\n\nexport const confirmationModalInfo = {\n  en: {\n    header: 'Attention! The following information will be public:',\n    availableDataList: [\n      'Personal information (Name, Desired Position, English level, Military Service, Avatar, Link to a presentation, Self-Description, etc.)',\n      'Contact details (Phone, Email, Skype, Telegram, LinkedIn, Location, GitHub username, Website Link)',\n      'Information about passed school courses (Courses Info, Mentor, Course Status, Score, Position);',\n      'Public feedback information (Gratitudes)',\n    ],\n  },\n  ru: {\n    header: 'Внимание! Следующая информация будет публичной:',\n    availableDataList: [\n      'Личная информация (имя, желаемая позиция, уровень английского, отношение к военной службе, аватар, ссылка на самопрезентацию, краткое самоописание и т.д.)',\n      'Контактные данные (телефон, электронная почта, Skype, Telegram, LinkedIn, локация, в которой хотите работать, GitHub, ссылка на веб-сайт)',\n      'Информация о пройденных в школе курсах (в каких курсах принято участие, статус курса для участника, скор, место в скоре)',\n      'Информация о публичной обратной связи (отзывы в RS School App)',\n    ],\n  },\n};\n\nexport const NoConsentView = (props: Props) => {\n  const { isOwner, giveConsent } = props;\n  const [modal, contextHolder] = Modal.useModal();\n\n  const confirmationModalContent = (\n    <List\n      header={\n        <Title level={4} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>\n          {confirmationModalInfo.en.header}\n          <Tooltip\n            placement=\"topLeft\"\n            title={confirmationModalInfo.ru.header}\n            getPopupContainer={triggerNode => triggerNode.parentElement || document.body}\n          >\n            <QuestionCircleOutlined data-testid={confirmationModalInfo.ru.header} />\n          </Tooltip>\n        </Title>\n      }\n      dataSource={confirmationModalInfo.en.availableDataList}\n      renderItem={(text, idx) => (\n        <Item style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>\n          <Paragraph>{text}</Paragraph>\n          <Tooltip\n            placement=\"topLeft\"\n            title={confirmationModalInfo.ru.availableDataList[idx]}\n            getPopupContainer={triggerNode => triggerNode.parentElement || document.body}\n          >\n            <QuestionCircleOutlined data-testid={confirmationModalInfo.ru.availableDataList[idx]} />\n          </Tooltip>\n        </Item>\n      )}\n    />\n  );\n\n  const showConfirmationModal = () => {\n    modal.confirm({\n      icon: null,\n      content: confirmationModalContent,\n      maskClosable: true,\n      onOk() {\n        giveConsent();\n      },\n      okText: 'I consent',\n    });\n  };\n\n  return (\n    <>\n      {contextHolder}\n      {isOwner ? (\n        <Result\n          icon={<span></span>}\n          title={<Title>You don't have a CV yet.</Title>}\n          subTitle={\n            <Text style={{ fontSize: '24px' }}>You can create a public CV that can be shared with employers.</Text>\n          }\n          extra={\n            <Button\n              style={{ width: '140px', height: '44px' }}\n              type=\"primary\"\n              htmlType=\"button\"\n              onClick={showConfirmationModal}\n            >\n              <PlusOutlined /> Create CV\n            </Button>\n          }\n        />\n      ) : (\n        <Result status={403} title=\"This user doesn't have CV yet\" />\n      )}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/PublicLink/index.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { PublicLink } from './index';\n\nconst mockUrl = 'https://expample.com';\n\nconst mockCopyToClipboard = vi.fn();\n\nvi.mock('react-use', () => ({\n  useCopyToClipboard: () => [vi.fn(), mockCopyToClipboard],\n}));\n\ndescribe('PublicLink', () => {\n  test('should not render anything if url is not provided', () => {\n    render(<PublicLink url={null} />);\n\n    const title = screen.queryByText('Public Link');\n    const link = screen.queryByRole('link');\n    const copyBtn = screen.queryByRole('button');\n\n    expect(title).not.toBeInTheDocument();\n    expect(link).not.toBeInTheDocument();\n    expect(copyBtn).not.toBeInTheDocument();\n  });\n\n  test('should display title and link', () => {\n    render(<PublicLink url={mockUrl} />);\n\n    const title = screen.getByText('Public Link');\n    const link = screen.getByRole('link', { name: mockUrl });\n\n    expect(title).toBeInTheDocument();\n    expect(link).toBeInTheDocument();\n    expect(link).toHaveAttribute('href', mockUrl);\n  });\n\n  test('should copy link', async () => {\n    render(<PublicLink url={mockUrl} />);\n\n    const copyBtn = screen.getByRole('button');\n\n    fireEvent.click(copyBtn);\n\n    const notification = await screen.findByText('Copied to clipboard');\n    expect(notification).toBeInTheDocument();\n\n    expect(mockCopyToClipboard).toHaveBeenCalledWith(mockUrl);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/PublicLink/index.tsx",
    "content": "import { CopyOutlined } from '@ant-design/icons';\nimport { Alert, Button, notification } from 'antd';\nimport { useCopyToClipboard } from 'react-use';\n\ntype Props = {\n  url: string | null;\n};\n\nexport const PublicLink = ({ url }: Props) => {\n  const [, copyToClipboard] = useCopyToClipboard();\n\n  if (!url) {\n    return null;\n  }\n\n  return (\n    <Alert\n      message={\n        <>\n          Public Link{' '}\n          <Button target=\"_blank\" type=\"link\" href={url}>\n            {url}\n          </Button>\n          <Button\n            onClick={() => {\n              copyToClipboard(url ?? '');\n              notification.success({ message: 'Copied to clipboard' });\n            }}\n            size=\"small\"\n            type=\"text\"\n            icon={<CopyOutlined />}\n          />\n        </>\n      }\n      type=\"info\"\n    ></Alert>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/StudentStatus/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { StudentStatus } from './index';\n\nconst mockId = '12345';\n\ndescribe('StudentStatus', () => {\n  test('should display certificate link if certificateId is provided', () => {\n    render(<StudentStatus certificateId={mockId} isCourseCompleted={true} />);\n\n    const certificateLink = screen.getByRole('link', { name: /certificate/i });\n\n    expect(certificateLink).toBeInTheDocument();\n    expect(certificateLink).toHaveAttribute('href', `/certificate/${mockId}`);\n  });\n\n  test.each`\n    isCourseCompleted | expectedText\n    ${true}           | ${'Completed'}\n    ${false}          | ${'In Progress'}\n  `(\n    'should display \"$expectedText\" if isCourseCompleted is $isCourseCompleted',\n    ({ isCourseCompleted, expectedText }) => {\n      render(<StudentStatus certificateId={null} isCourseCompleted={isCourseCompleted} />);\n      const status = screen.getByText(expectedText);\n\n      expect(status).toBeInTheDocument();\n    },\n  );\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/StudentStatus/index.tsx",
    "content": "import { SafetyCertificateTwoTone, ClockCircleTwoTone, CheckCircleTwoTone } from '@ant-design/icons';\n\nexport const StudentStatus = (props: { certificateId: string | null; isCourseCompleted: boolean }) => {\n  const { certificateId, isCourseCompleted } = props;\n\n  if (certificateId) {\n    return (\n      <span>\n        <SafetyCertificateTwoTone twoToneColor=\"#52c41a\" /> Completed with{' '}\n        <a target=\"_blank\" rel=\"nofollow\" href={`/certificate/${certificateId}`}>\n          certificate\n        </a>\n      </span>\n    );\n  }\n\n  if (isCourseCompleted) {\n    return (\n      <span>\n        <CheckCircleTwoTone /> Completed\n      </span>\n    );\n  }\n\n  return (\n    <span>\n      <ClockCircleTwoTone twoToneColor=\"#ec9607\" /> In Progress\n    </span>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/AboutSection/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { AboutSection } from './index';\n\nconst mockNotes = 'Some notes';\n\ndescribe('AboutSection', () => {\n  test('should display notes', () => {\n    render(<AboutSection notes={mockNotes} />);\n\n    const notes = screen.getByText(mockNotes);\n\n    expect(notes).toBeInTheDocument();\n  });\n\n  test('should not display section if notes are not provided', () => {\n    const { container } = render(<AboutSection notes={null} />);\n\n    expect(container).toBeEmptyDOMElement();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/AboutSection/index.tsx",
    "content": "import { Row, Col, Typography } from 'antd';\nimport { BaseSection } from '../BaseSection';\n\nconst { Text } = Typography;\n\ntype Props = {\n  notes: React.ReactNode;\n};\n\nexport const AboutSection = (props: Props) => {\n  const { notes } = props;\n\n  if (!notes) {\n    return null;\n  }\n\n  return (\n    <BaseSection title=\"About\">\n      <Row>\n        <Col>\n          <Text style={{ fontSize: 16, whiteSpace: 'pre-line' }}>{notes}</Text>\n        </Col>\n      </Row>\n    </BaseSection>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/ActionButtons/index.test.tsx",
    "content": "import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { ActionButtons } from './index';\n\nconst mockUrl = 'https://example.com';\n\nconst mockSwitchView = vi.fn();\nconst mockCopyToClipboard = vi.fn();\nconst mockOnRemoveConsent = vi.fn();\nvi.mock('react-use', () => ({\n  useCopyToClipboard: () => [vi.fn(), mockCopyToClipboard],\n}));\n\nconst mockSuccessNotification = vi.fn();\nvi.mock('@client/hooks', () => ({\n  useMessage: () => ({\n    notification: {\n      success: mockSuccessNotification,\n    },\n  }),\n}));\n\ndescribe('ActionButtons', () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n  });\n\n  test('should have Edit, Share, Delete buttons', () => {\n    render(<ActionButtons isExpired={false} />);\n\n    const editButton = screen.getByRole('button', { name: /edit cv/i });\n    const shareButton = screen.getByRole('button', { name: /share/i });\n    const deleteButton = screen.getByRole('button', { name: /delete/i });\n\n    expect(editButton).toBeInTheDocument();\n    expect(shareButton).toBeInTheDocument();\n    expect(deleteButton).toBeInTheDocument();\n  });\n\n  test('should switch view by click on Edit button', () => {\n    render(<ActionButtons isExpired={false} switchView={mockSwitchView} />);\n\n    const editButton = screen.getByRole('button', { name: /edit cv/i });\n\n    fireEvent.click(editButton);\n\n    expect(mockSwitchView).toHaveBeenCalled();\n  });\n\n  test('should copy to clipboard by click on Share button if url is provided', async () => {\n    render(<ActionButtons url={mockUrl} isExpired={false} switchView={mockSwitchView} />);\n\n    const shareButton = screen.getByRole('button', { name: /share/i });\n\n    fireEvent.click(shareButton);\n\n    expect(mockCopyToClipboard).toHaveBeenCalledWith(mockUrl);\n  });\n\n  test('should not to clipboard by click on Share button if url is not provided', async () => {\n    render(<ActionButtons isExpired={false} switchView={mockSwitchView} />);\n\n    const shareButton = screen.getByRole('button', { name: /share/i });\n\n    fireEvent.click(shareButton);\n\n    expect(mockCopyToClipboard).not.toHaveBeenCalled();\n    expect(mockSuccessNotification).not.toHaveBeenCalled();\n  });\n\n  test('should disable Share button should if CV is expired', () => {\n    render(<ActionButtons isExpired={true} />);\n    const shareButton = screen.getByRole('button', { name: /share/i });\n\n    expect(shareButton).toBeDisabled();\n  });\n\n  test('should show and hide delete confirmation modal correctly', async () => {\n    render(<ActionButtons isExpired={true} />);\n\n    const deleteButton = screen.getByRole('button', { name: /delete/i });\n\n    fireEvent.click(deleteButton);\n\n    const modalTitle = await screen.findByText('Delete your CV', { selector: '.ant-modal-confirm-title' });\n    const modalBodyFragment = await screen.findByText(/are you sure you want to delete your cv/i);\n    const modalConfirmButton = await screen.findByRole('button', { name: /delete cv/i });\n    const modalCancelButton = await screen.findByRole('button', { name: /cancel/i });\n\n    expect(modalTitle).toBeInTheDocument();\n    expect(modalBodyFragment).toBeInTheDocument();\n    expect(modalConfirmButton).toBeInTheDocument();\n    expect(modalCancelButton).toBeInTheDocument();\n\n    fireEvent.click(modalCancelButton);\n\n    await waitFor(() => expect(modalTitle).not.toBeInTheDocument());\n    await waitFor(() => expect(modalBodyFragment).not.toBeInTheDocument());\n    await waitFor(() => expect(modalConfirmButton).not.toBeInTheDocument());\n    await waitFor(() => expect(modalCancelButton).not.toBeInTheDocument());\n  });\n\n  test('should delete CV after confirmation', async () => {\n    render(<ActionButtons isExpired={true} onRemoveConsent={mockOnRemoveConsent} />);\n\n    const deleteButton = screen.getByRole('button', { name: /delete/i });\n\n    fireEvent.click(deleteButton);\n\n    const modalConfirmButton = await screen.findByRole('button', { name: /delete cv/i });\n\n    expect(modalConfirmButton).toBeInTheDocument();\n\n    fireEvent.click(modalConfirmButton);\n\n    expect(mockOnRemoveConsent).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/ActionButtons/index.tsx",
    "content": "import { Row, Modal, Button, Divider, Alert, Typography } from 'antd';\nimport { useCallback } from 'react';\nimport { useCopyToClipboard } from 'react-use';\nimport { DeleteOutlined, EditOutlined, ShareAltOutlined } from '@ant-design/icons';\nimport { useMessage } from '@client/hooks';\n\nconst { Paragraph } = Typography;\n\nconst buttonStyle = { width: 'fit-content', margin: '5px' };\n\ntype Props = {\n  url?: string;\n  switchView?: () => void;\n  onRemoveConsent?: () => void;\n  isExpired: boolean;\n};\n\nexport const ActionButtons = ({ onRemoveConsent, switchView, url, isExpired }: Props) => {\n  const { notification } = useMessage();\n  const [, copyToClipboard] = useCopyToClipboard();\n  const [modal, contextHolder] = Modal.useModal();\n\n  const showDeletionConfirmationModal = useCallback(() => {\n    const title = 'Delete your CV';\n\n    const message =\n      \"All information from your resume will be permanently deleted. This action won't affect your profile info.\";\n    const messageRu =\n      'Вся информация из резюме будет удалена навсегда. Это действие не повлияет на содержание вашего личного профиля.';\n    const confirmationModalContent = (\n      <>\n        <Alert\n          type=\"warning\"\n          showIcon\n          message={\n            <>\n              <Paragraph>{message}</Paragraph>\n              <Paragraph>{messageRu}</Paragraph>\n            </>\n          }\n        />\n        <Paragraph>Are you sure you want to delete your CV? Employers will not be able to access it anymore.</Paragraph>\n        <Paragraph>\n          Вы уверены, что хотите удалить резюме? Работодатели больше не смогут получить к нему доступ.\n        </Paragraph>\n        <Divider style={{ marginTop: '10px', marginBottom: '5px' }} />\n      </>\n    );\n\n    modal.confirm({\n      icon: null,\n      title: title,\n      content: confirmationModalContent,\n      centered: true,\n      maskClosable: true,\n      okText: 'Delete CV',\n      okButtonProps: { danger: true },\n      onOk: () => onRemoveConsent && onRemoveConsent(),\n    });\n  }, [onRemoveConsent]);\n\n  return (\n    <>\n      {contextHolder}\n      <Row justify=\"center\" style={{ paddingTop: '10px' }} className=\"no-print\">\n        <Button style={buttonStyle} type=\"primary\" htmlType=\"button\" onClick={switchView} icon={<EditOutlined />}>\n          Edit CV\n        </Button>\n        <Button\n          disabled={isExpired}\n          style={buttonStyle}\n          htmlType=\"button\"\n          onClick={() => {\n            if (url) {\n              copyToClipboard(url);\n              notification.success({ message: 'Copied to clipboard' });\n            }\n          }}\n          icon={<ShareAltOutlined />}\n        >\n          Share\n        </Button>\n        <Button style={buttonStyle} htmlType=\"button\" onClick={showDeletionConfirmationModal} icon={<DeleteOutlined />}>\n          Delete\n        </Button>\n      </Row>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/BaseSection/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ExclamationCircleOutlined } from '@ant-design/icons';\nimport { BaseSection } from './index';\n\nconst mockTestId = 'test-id';\nconst mockTitle = 'Some title';\nconst mockIcon = <ExclamationCircleOutlined data-testid={mockTestId} />;\n\ndescribe('BaseSection', () => {\n  test('should display title and icon if provided', () => {\n    render(<BaseSection title={mockTitle} icon={mockIcon} />);\n\n    const title = screen.getByText(mockTitle);\n    const icon = screen.getByTestId(mockTestId);\n\n    expect(title).toBeInTheDocument();\n    expect(icon).toBeInTheDocument();\n  });\n\n  test('should render children if provided', () => {\n    const MockChild = () => <div>Some child</div>;\n\n    render(\n      <BaseSection>\n        <MockChild />\n      </BaseSection>,\n    );\n\n    const child = screen.getByText('Some child');\n\n    expect(child).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/BaseSection/index.tsx",
    "content": "import * as React from 'react';\nimport { Card, Avatar, Typography } from 'antd';\n\nconst { Title } = Typography;\n\ntype Props = React.PropsWithChildren<{\n  title?: React.ReactNode;\n  icon?: React.ReactNode;\n}>;\n\nexport const BaseSection = (props: Props) => {\n  const { title, icon, children } = props;\n\n  const avatar = icon ? <Avatar size={24} icon={icon} /> : null;\n\n  const complexTitle = title ? (\n    <Title level={4} style={{ display: 'inline-block' }}>\n      {avatar} {title}\n    </Title>\n  ) : null;\n\n  return (\n    <Card className=\"cv-card-section\" size=\"small\" title={complexTitle} bordered={false}>\n      {children}\n    </Card>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/ContactsSection/ContactsList/index.module.css",
    "content": ".container {\n  max-width: 100%;\n  display: flex;\n}\n\n.value {\n  word-break: break-all;\n}\n\n@media print {\n  .icon {\n    color: #000 !important;\n  }\n\n  .icon :global(.anticon svg) {\n    fill: #000 !important;\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/ContactsSection/ContactsList/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ContactsList } from './index';\n\nconst mockContacts = {\n  email: 'example@example.com',\n  githubUsername: 'some-github',\n  linkedin: 'https://linked.in',\n  phone: '+1111111111111',\n  skype: 'some_skype',\n  telegram: 'some_telegram',\n  website: 'https://example.com',\n};\n\ndescribe('ContactsList', () => {\n  test('should display proper values', () => {\n    render(<ContactsList contacts={mockContacts} />);\n\n    const email = screen.getByText(mockContacts.email);\n    const githubUsername = screen.getByText(mockContacts.githubUsername);\n    const linkedin = screen.getByText(mockContacts.linkedin);\n    const phone = screen.getByText(mockContacts.phone);\n    const skype = screen.getByText(mockContacts.skype);\n    const telegram = screen.getByText(`@${mockContacts.telegram}`);\n    const website = screen.getByText(mockContacts.website);\n\n    expect(email).toBeInTheDocument();\n    expect(githubUsername).toBeInTheDocument();\n    expect(linkedin).toBeInTheDocument();\n    expect(phone).toBeInTheDocument();\n    expect(skype).toBeInTheDocument();\n    expect(telegram).toBeInTheDocument();\n    expect(website).toBeInTheDocument();\n  });\n\n  test('should display corresponding icons', () => {\n    render(<ContactsList contacts={mockContacts} />);\n\n    const emailIcon = screen.getByRole('img', { name: 'mail' });\n    const githubIcon = screen.getByRole('img', { name: 'github' });\n    const linkedinIcon = screen.getByRole('img', { name: 'linkedin' });\n    const phoneIcon = screen.getByRole('img', { name: 'phone' });\n    const skypeIcon = screen.getByRole('img', { name: 'skype' });\n    const telegramIcon = screen.getByRole('img', { name: 'message' });\n    const websiteIcon = screen.getByRole('img', { name: 'idcard' });\n\n    expect(emailIcon).toBeInTheDocument();\n    expect(githubIcon).toBeInTheDocument();\n    expect(linkedinIcon).toBeInTheDocument();\n    expect(phoneIcon).toBeInTheDocument();\n    expect(skypeIcon).toBeInTheDocument();\n    expect(telegramIcon).toBeInTheDocument();\n    expect(websiteIcon).toBeInTheDocument();\n  });\n\n  test('should have corresponding links', () => {\n    render(<ContactsList contacts={mockContacts} />);\n\n    const links = screen.getAllByRole('link');\n    expect(links).toHaveLength(7);\n\n    const [emailIcon, githubIcon, linkedinIcon, phoneIcon, skypeIcon, telegramIcon, websiteIcon] = links;\n\n    expect(emailIcon).toHaveAttribute('title', 'E-mail');\n    expect(githubIcon).toHaveAttribute('title', 'GitHub');\n    expect(linkedinIcon).toHaveAttribute('title', 'LinkedIn');\n    expect(phoneIcon).toHaveAttribute('title', 'Phone');\n    expect(skypeIcon).toHaveAttribute('title', 'Skype');\n    expect(telegramIcon).toHaveAttribute('title', 'Telegram');\n    expect(websiteIcon).toHaveAttribute('title', 'Website');\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/ContactsSection/ContactsList/index.tsx",
    "content": "import { useCopyToClipboard } from 'react-use';\nimport { Typography, Button } from 'antd';\nimport {\n  PhoneOutlined,\n  MailOutlined,\n  SkypeOutlined,\n  LinkedinOutlined,\n  GithubOutlined,\n  IdcardOutlined,\n  MessageOutlined,\n  CopyOutlined,\n} from '@ant-design/icons';\nimport { Contacts, ContactType } from '@client/modules/Opportunities/models';\nimport { getContactsToRender } from '@client/modules/Opportunities/data/getContactsToRender';\n\nimport { Link } from '@client/modules/Opportunities/components/Link';\nimport { useMessage } from '@client/hooks';\nimport styles from './index.module.css';\n\nconst { Text } = Typography;\n\ntype Props = {\n  contacts: Contacts;\n};\n\nexport const ContactsList = ({ contacts }: Props) => {\n  const { notification } = useMessage();\n  const contactsToRender = getContactsToRender(contacts);\n  const [, copyToClipboard] = useCopyToClipboard();\n\n  const list = contactsToRender.map(([type, text]) => {\n    const { icon, render } = contactRendererMap[type];\n    const node = render?.(text as string) ?? <Text>{text}</Text>;\n    return { icon, node, value: text };\n  });\n\n  return (\n    <div>\n      {list.map(({ icon, node, value }, i) => (\n        <div className={styles.container} key={i} style={{ paddingTop: 3 }}>\n          <div className={styles.icon} style={{ paddingRight: 8 }}>\n            {icon}\n          </div>\n          <div className={styles.value}>{node}</div>\n          <Button\n            onClick={() => {\n              copyToClipboard(value ?? '');\n              notification.success({ message: 'Copied to clipboard' });\n            }}\n            style={{ color: '#fff' }}\n            size=\"small\"\n            type=\"text\"\n            icon={<CopyOutlined />}\n          />\n        </div>\n      ))}\n    </div>\n  );\n};\n\ntype AllowedContacts = {\n  [key in ContactType]: {\n    icon: React.ReactNode;\n    render?: (contact: string) => React.ReactNode;\n  };\n};\n\nconst contactRendererMap: AllowedContacts = {\n  phone: {\n    icon: <PhoneOutlined />,\n    render: contact => <Link title=\"Phone\" url={`tel:${contact}`} text={contact} />,\n  },\n  email: {\n    icon: <MailOutlined />,\n    render: contact => <Link title=\"E-mail\" url={`mailto:${contact}`} text={contact} />,\n  },\n  skype: {\n    icon: <SkypeOutlined />,\n    render: contact => <Link title=\"Skype\" url={`skype:${contact}?chat`} text={contact} />,\n  },\n  telegram: {\n    icon: <MessageOutlined />,\n    render: contact => <Link title=\"Telegram\" url={`https://t.me/${contact}`} text={`@${contact}`} />,\n  },\n  linkedin: {\n    icon: <LinkedinOutlined />,\n    render: contact => <Link title=\"LinkedIn\" url={contact} text={contact} />,\n  },\n  githubUsername: {\n    icon: <GithubOutlined />,\n    render: contact => <Link title=\"GitHub\" url={`https://github.com/${contact}`} text={contact} />,\n  },\n  website: {\n    icon: <IdcardOutlined />,\n    render: contact => <Link title=\"Website\" url={contact} text={contact} />,\n  },\n};\n\nexport default ContactsList;\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/ContactsSection/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Contacts } from '@client/modules/Opportunities/models';\nimport { ContactsSection } from './index';\n\nvi.mock('./ContactsList', () => ({\n  ContactsList: () => <div>Mock Contacts</div>,\n}));\n\ndescribe('ContactsSection', () => {\n  test('should display nothing if contacts are not provided', () => {\n    const { container } = render(<ContactsSection contacts={null} />);\n    expect(container).toBeEmptyDOMElement();\n  });\n\n  test('should display section if contacts are provided', () => {\n    render(<ContactsSection contacts={{} as Contacts} />);\n    const contactsList = screen.getByText('Mock Contacts');\n    expect(contactsList).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/ContactsSection/index.tsx",
    "content": "import { Contacts } from '@client/modules/Opportunities/models';\nimport { ContactsList } from './ContactsList';\n\ntype Props = {\n  contacts: Contacts | null;\n};\n\nexport const ContactsSection = ({ contacts }: Props) => {\n  if (contacts == null) {\n    return null;\n  }\n\n  return (\n    <div>\n      <ContactsList contacts={contacts} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/CoursesSection/CoursesSection.module.css",
    "content": ".courseDataKey {\n  font-size: 14px;\n  padding-right: 8px;\n  white-space: nowrap;\n  width: 80px;\n  display: inline-block;\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/CoursesSection/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ResumeCourseDto } from '@client/api';\nimport { CoursesSection } from './index';\n\nconst courseWithFullData: ResumeCourseDto = {\n  id: 1,\n  name: 'JS/FE 2020Q1',\n  fullName: 'Rolling Scopes School 2020 Q1: JavaScript/Front-end',\n  certificateId: 'abc123',\n  completed: true,\n  totalScore: 100,\n  locationName: 'Minsk',\n  mentor: {\n    id: 123,\n    name: 'Mentor Name',\n    githubId: 'mentor-github-id',\n  },\n  rank: 111,\n};\n\nconst mockCourses = [\n  courseWithFullData,\n  {\n    id: 2,\n    fullName: 'Rolling Scopes School 2020 Q3: Node.js',\n    rank: 222,\n  },\n  {\n    id: 3,\n    fullName: 'Rolling Scopes School 2020 Q1: Machine Learning',\n    rank: 333,\n  },\n  {\n    id: 4,\n    fullName: 'Rolling Scopes School 2020 Q4: Android',\n    rank: 444,\n  },\n] as [ResumeCourseDto, ResumeCourseDto, ResumeCourseDto, ResumeCourseDto];\n\ndescribe('CoursesSection', () => {\n  test('should display nothing if courses are not provided', () => {\n    const { container } = render(<CoursesSection courses={[]} visibleCourses={[]} />);\n    expect(container).toBeEmptyDOMElement();\n  });\n\n  test('should display all course data correctly', () => {\n    render(<CoursesSection courses={[courseWithFullData]} visibleCourses={[courseWithFullData.id]} />);\n\n    const sectionHead = screen.getByRole('heading', { name: /rs school courses/i });\n    const fullName = screen.getByText(`${courseWithFullData.fullName} (${courseWithFullData.locationName})`);\n    const certificateIcon = screen.getByRole('img', { name: /safety-certificate/i });\n    const certificateLink = screen.getByRole('link', { name: /certificate/i });\n    const mentorLink = screen.getByRole('link', { name: new RegExp(courseWithFullData.mentor?.name as string, 'i') });\n    const position = screen.getByText(`Position: ${courseWithFullData.rank}`);\n    const score = screen.getByText(`Score: ${courseWithFullData.totalScore}`);\n\n    expect(sectionHead).toBeInTheDocument();\n    expect(fullName).toBeInTheDocument();\n    expect(certificateIcon).toBeInTheDocument();\n    expect(certificateLink).toBeInTheDocument();\n    expect(certificateLink).toHaveAttribute('href', `/certificate/${courseWithFullData.certificateId}`);\n    expect(mentorLink).toBeInTheDocument();\n    expect(mentorLink).toHaveAttribute('href', `https://github.com/${courseWithFullData.mentor?.githubId}`);\n    expect(position).toBeInTheDocument();\n    expect(score).toBeInTheDocument();\n  });\n\n  test('should display all courses if visible courses are empty', () => {\n    render(<CoursesSection courses={mockCourses} visibleCourses={[]} />);\n\n    mockCourses.forEach(({ fullName, rank }) => {\n      const courseName = screen.getByText(fullName, { exact: false });\n      const coursePosition = screen.getByText(`Position: ${rank}`);\n\n      expect(courseName).toBeInTheDocument();\n      expect(coursePosition).toBeInTheDocument();\n    });\n  });\n\n  test('should display only visible courses if provided', () => {\n    const mockVisibleCourses = [mockCourses[0].id, mockCourses[2].id];\n\n    render(<CoursesSection courses={mockCourses} visibleCourses={mockVisibleCourses} />);\n\n    mockVisibleCourses.forEach(courseId => {\n      const course = mockCourses.find(({ id }) => id === courseId);\n      const courseName = screen.getByText(course?.fullName as string, { exact: false });\n      const coursePosition = screen.getByText(`Position: ${course?.rank}`);\n\n      expect(courseName).toBeInTheDocument();\n      expect(coursePosition).toBeInTheDocument();\n    });\n\n    const invisibleCourses = mockCourses.filter(({ id }) => !mockVisibleCourses.includes(id));\n\n    invisibleCourses.forEach(({ fullName, rank }) => {\n      const courseName = screen.queryByText(fullName, { exact: false });\n      const coursePosition = screen.queryByText(`Position: ${rank}`);\n\n      expect(courseName).not.toBeInTheDocument();\n      expect(coursePosition).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/CoursesSection/index.tsx",
    "content": "import { StarOutlined, TrophyOutlined } from '@ant-design/icons';\nimport { Col, List, Row, Typography } from 'antd';\nimport { ResumeCourseDto, ResumeDto } from '@client/api';\nimport { BaseSection } from '../BaseSection';\n\nimport styles from './CoursesSection.module.css';\nimport { StudentStatus } from '@client/modules/Opportunities/components/StudentStatus';\nimport { DataTextValue } from '@client/modules/Opportunities/components/ViewCv/DataTextValue';\n\nconst { Text } = Typography;\nconst { Item } = List;\n\ntype Props = {\n  courses: ResumeDto['courses'];\n  visibleCourses: ResumeDto['visibleCourses'];\n};\n\nexport const CoursesSection = ({ courses, visibleCourses }: Props) => {\n  if (courses.length === 0) {\n    return null;\n  }\n\n  const coursesToRender =\n    visibleCourses.length > 0 ? courses.filter(course => visibleCourses.includes(course.id)) : courses;\n\n  return (\n    <BaseSection title=\"RS School Courses\">\n      <List dataSource={coursesToRender} size=\"small\" style={{ fontSize: '15px' }} renderItem={renderItem} />\n    </BaseSection>\n  );\n};\n\nfunction renderItem(record: ResumeCourseDto) {\n  const { fullName, certificateId, completed, totalScore, rank, locationName, mentor } = record;\n\n  const title = `${fullName}${locationName ? ` (${locationName})` : ''}`;\n  return (\n    <Item>\n      <Col flex=\"1\">\n        <Text strong>{title}</Text>\n        <Row justify=\"space-between\" style={{ marginTop: 8, width: '100%' }}>\n          <Col flex={1}>\n            <Row>\n              <span className={styles.courseDataKey}>\n                Status: <StudentStatus certificateId={certificateId} isCourseCompleted={completed} />\n              </span>\n            </Row>\n            <Row>\n              <DataTextValue>\n                Mentor:{' '}\n                {mentor?.name ? (\n                  <a className=\"black-on-print\" href={`https://github.com/${mentor.githubId}`}>\n                    {mentor.name}\n                  </a>\n                ) : (\n                  <Text>No mentor</Text>\n                )}\n              </DataTextValue>\n            </Row>\n          </Col>\n          <Col style={{ minWidth: 130, maxWidth: 130 }}>\n            <Row>\n              <DataTextValue>\n                <TrophyOutlined /> Position: {rank}\n              </DataTextValue>\n            </Row>\n            <Row>\n              <DataTextValue>\n                <StarOutlined /> Score: {totalScore}\n              </DataTextValue>\n            </Row>\n          </Col>\n        </Row>\n      </Col>\n    </Item>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/DataTextValue/DataTextValue.module.css",
    "content": ".value {\n  font-size: 14px;\n  padding-right: 8px;\n  white-space: nowrap;\n  width: 80px;\n  display: inline-block;\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/DataTextValue/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { DataTextValue } from './index';\n\nconst MockContent = () => <div>Mock content</div>;\n\ndescribe('DataTextValue', () => {\n  test('should display content', () => {\n    render(\n      <DataTextValue>\n        <MockContent />\n      </DataTextValue>,\n    );\n    const mockContent = screen.getByText(/mock content/i);\n    expect(mockContent).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/DataTextValue/index.tsx",
    "content": "import { PropsWithChildren } from 'react';\n\nimport styles from './DataTextValue.module.css';\n\nexport const DataTextValue = ({ children }: PropsWithChildren<unknown>) => {\n  return <span className={styles.value}>{children}</span>;\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/FeedbackSection/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { FeedbackDto, FeedbackSoftSkillIdEnum, FeedbackSoftSkillValueEnum } from '@client/api';\nimport { FeedbackSection } from './index';\n\nconst mockFeedback = {\n  date: '2022-09-29T12:12:28.228Z',\n  recommendationComment: 'Recommendation comment',\n  englishLevel: 'A1',\n  suggestions: 'Suggestion 1, Suggestion 2',\n  mentor: {\n    id: 1,\n    name: 'Mentor Name',\n    githubId: 'mentor-github-id',\n  },\n  softSkills: [\n    {\n      id: FeedbackSoftSkillIdEnum.Communicable,\n      value: FeedbackSoftSkillValueEnum.Excellent,\n    },\n    {\n      id: FeedbackSoftSkillIdEnum.Responsible,\n      value: FeedbackSoftSkillValueEnum.Great,\n    },\n    {\n      id: FeedbackSoftSkillIdEnum.TeamPlayer,\n      value: FeedbackSoftSkillValueEnum.Poor,\n    },\n  ],\n  course: {\n    id: 1,\n    name: 'Course Name',\n  },\n} as FeedbackDto;\n\ndescribe('FeedbackSection', () => {\n  test('should display nothing if feedback is not provided', () => {\n    const { container } = render(<FeedbackSection data={[]} />);\n    expect(container).toBeEmptyDOMElement();\n  });\n\n  test('should display feedback if provided', () => {\n    render(<FeedbackSection data={[mockFeedback]} />);\n\n    const sectionHeading = screen.getByRole('heading', { name: /mentor's feedback/i });\n    const mentorLink = screen.getByRole('link', {\n      name: new RegExp(mockFeedback.mentor.name, 'i'),\n    });\n    const courseName = screen.getByText(new RegExp(mockFeedback.course.name, 'i'));\n    const recommendation = screen.getByText('Recommend To Hire');\n    const recommendationComment = screen.getByText(mockFeedback.recommendationComment);\n    const suggestions = screen.getByText(mockFeedback.suggestions);\n    const englishLevel = screen.getByText(mockFeedback.englishLevel);\n    const communicationSkill = screen.getByText('Communication: Excellent');\n    const responsibilitySkill = screen.getByText('Responsibility: Great');\n    const teamPlayerSkill = screen.getByText('Team Player: Poor');\n\n    expect(sectionHeading).toBeInTheDocument();\n    expect(mentorLink).toBeInTheDocument();\n    expect(courseName).toBeInTheDocument();\n    expect(recommendation).toBeInTheDocument();\n    expect(recommendationComment).toBeInTheDocument();\n    expect(suggestions).toBeInTheDocument();\n    expect(englishLevel).toBeInTheDocument();\n    expect(communicationSkill).toBeInTheDocument();\n    expect(responsibilitySkill).toBeInTheDocument();\n    expect(teamPlayerSkill).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/FeedbackSection/index.tsx",
    "content": "import { Badge, Card, Col, List, Typography } from 'antd';\nimport { FeedbackDto, FeedbackSoftSkillIdEnum } from '@client/api';\nimport { DataTextValue } from '@client/modules/Opportunities/components/ViewCv/DataTextValue';\nimport { BaseSection } from '../BaseSection';\n\nconst { Text, Paragraph } = Typography;\n\ntype Props = {\n  data: FeedbackDto[] | null;\n};\n\nconst formatSoftSkill = (skillId: FeedbackSoftSkillIdEnum) => {\n  switch (skillId) {\n    case FeedbackSoftSkillIdEnum.Communicable:\n      return 'Communication';\n    case FeedbackSoftSkillIdEnum.Responsible:\n      return 'Responsibility';\n    case FeedbackSoftSkillIdEnum.TeamPlayer:\n      return 'Team Player';\n    default:\n      return 'Unknown';\n  }\n};\n\nexport const FeedbackSection = ({ data }: Props) => {\n  if (!data?.length) {\n    return null;\n  }\n\n  return (\n    <BaseSection title=\"Mentor's Feedback\">\n      <List\n        dataSource={data}\n        size=\"small\"\n        renderItem={({ recommendationComment, suggestions, softSkills, englishLevel, course, mentor }, i) => (\n          <List.Item key={i}>\n            <Col flex={1} style={{ paddingRight: 16 }}>\n              <Badge.Ribbon text=\"Recommend To Hire\" color=\"green\">\n                <Card\n                  bordered={false}\n                  title={\n                    <DataTextValue>\n                      <div>\n                        Mentor:{' '}\n                        <a className=\"black-on-print\" href={`https://github.com/${mentor.githubId}`}>\n                          {mentor.name}\n                        </a>\n                      </div>\n                      <div>Course: {course.name}</div>\n                    </DataTextValue>\n                  }\n                  size=\"small\"\n                >\n                  <Paragraph italic>{recommendationComment}</Paragraph>\n                  <Text type=\"secondary\">Suggestions</Text>\n                  <Paragraph italic>{suggestions}</Paragraph>\n\n                  <Text type=\"secondary\">Estimated English Level</Text>\n                  <Paragraph italic>{englishLevel?.toUpperCase()}</Paragraph>\n\n                  <Text type=\"secondary\">Soft Skills</Text>\n                  <Paragraph italic>\n                    <ul>\n                      {softSkills.map(skill => (\n                        <li key={skill.id}>\n                          {formatSoftSkill(skill.id)}: {skill.value}\n                        </li>\n                      ))}\n                    </ul>\n                  </Paragraph>\n                </Card>\n              </Badge.Ribbon>\n            </Col>\n          </List.Item>\n        )}\n      />\n    </BaseSection>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/GratitudeSection/GratitudeList/index.test.tsx",
    "content": "import assert from 'node:assert';\nimport { fireEvent, render, screen } from '@testing-library/react';\nimport { GratitudeDto } from '@client/api';\nimport { GratitudeList } from './index';\n\nconst mockGratitudes = [\n  {\n    date: '2022-09-29T13:40:08.254Z',\n    comment: 'Gratitude comment 1',\n  },\n  {\n    date: '2022-09-28T13:40:09.260Z',\n    comment: 'Gratitude comment 2',\n  },\n  {\n    date: '2022-06-29T13:40:10.262Z',\n    comment: 'Gratitude comment 3',\n  },\n] as GratitudeDto[];\n\ndescribe('GratitudeList', () => {\n  beforeAll(() => {\n    vi.useFakeTimers().setSystemTime(1664499199062);\n  });\n\n  afterAll(() => {\n    vi.useRealTimers();\n  });\n  test('should display nothing if gratitude list is empty', () => {\n    const { container } = render(<GratitudeList feedback={[]} showCount={5} />);\n    expect(container).toBeEmptyDOMElement();\n  });\n\n  test('should display gratitudes list if provided', () => {\n    render(<GratitudeList feedback={mockGratitudes} showCount={mockGratitudes.length} />);\n\n    assert.ok(mockGratitudes.length === 3);\n\n    const gratitudeComment1 = screen.getByText(mockGratitudes[0]!.comment);\n    const timeAgo1 = screen.getByText('11 hours ago');\n    const gratitudeComment2 = screen.getByText(mockGratitudes[1]!.comment);\n    const timeAgo2 = screen.getByText('a day ago');\n    const gratitudeComment3 = screen.getByText(mockGratitudes[2]!.comment);\n    const timeAgo3 = screen.getByText('3 months ago');\n\n    expect(gratitudeComment1).toBeInTheDocument();\n    expect(timeAgo1).toBeInTheDocument();\n    expect(gratitudeComment2).toBeInTheDocument();\n    expect(timeAgo2).toBeInTheDocument();\n    expect(gratitudeComment3).toBeInTheDocument();\n    expect(timeAgo3).toBeInTheDocument();\n  });\n\n  test('should display number of feedbacks equal to showCount and Show All button if number of feedbacks is greater than showCount', () => {\n    const mockShowCount = 1;\n\n    render(<GratitudeList feedback={mockGratitudes} showCount={mockShowCount} />);\n\n    const feedbacksCount = screen.getAllByRole('listitem');\n    const showAllButton = screen.getByRole('button', { name: 'Show all' });\n\n    expect(feedbacksCount.length).toBe(mockShowCount);\n    expect(showAllButton).toBeInTheDocument();\n  });\n\n  test('should display number of feedbacks equal to showCount and not show Show All button if number of feedbacks is not greater than showCount', () => {\n    render(<GratitudeList feedback={mockGratitudes} showCount={mockGratitudes.length} />);\n\n    const feedbacksCount = screen.getAllByRole('listitem');\n    const showAllButton = screen.queryByRole('button', { name: 'Show all' });\n\n    expect(feedbacksCount).toHaveLength(mockGratitudes.length);\n    expect(showAllButton).not.toBeInTheDocument();\n  });\n\n  test('should collapse and expand the list of feedbacks correctly', () => {\n    const mockShowCount = 1;\n\n    render(<GratitudeList feedback={mockGratitudes} showCount={mockShowCount} />);\n\n    expect(screen.getAllByRole('listitem').length).toBe(mockShowCount);\n\n    fireEvent.click(screen.getByRole('button', { name: 'Show all' }));\n\n    expect(screen.getAllByRole('listitem').length).toBe(mockGratitudes.length);\n\n    fireEvent.click(screen.getByRole('button', { name: 'Show partially' }));\n\n    expect(screen.getAllByRole('listitem').length).toBe(mockShowCount);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/GratitudeSection/GratitudeList/index.tsx",
    "content": "import { Button, Col, List, Tooltip, Typography } from 'antd';\nimport dayjs from 'dayjs';\nimport relativeTime from 'dayjs/plugin/relativeTime';\nimport { useState, useMemo } from 'react';\nimport { GratitudeDto } from '@client/api';\n\ndayjs.extend(relativeTime);\n\nconst { Text, Paragraph } = Typography;\n\ntype Props = {\n  feedback?: GratitudeDto[];\n  showCount: number;\n};\n\nexport function GratitudeList({ feedback, showCount }: Props) {\n  const feedbackPartial = feedback?.slice(0, showCount);\n  const expansionNeeded = Number(feedback?.length) > showCount;\n\n  const [feedbackToShow, setFeedbackToShow] = useState(feedbackPartial);\n  const [allFeedbackVisible, setAllFeedbackVisible] = useState(!expansionNeeded);\n\n  const showAllFeedback = () => {\n    setFeedbackToShow(feedback);\n    setAllFeedbackVisible(true);\n  };\n\n  const showFeedbackPartially = () => {\n    setFeedbackToShow(feedbackPartial);\n    setAllFeedbackVisible(false);\n  };\n\n  const dataSource = useMemo(\n    () =>\n      feedbackToShow?.map(feedback => {\n        const { comment, date } = feedback;\n        const feedbackDate = dayjs(date);\n        return {\n          comment,\n          dateFormatted: feedbackDate.format('YYYY-MM-DD HH:mm:ss'),\n          relativeDate: feedbackDate.fromNow().toLowerCase(),\n        };\n      }),\n    [feedbackToShow],\n  );\n\n  return feedback?.length && feedback.length > 0 ? (\n    <div>\n      <List\n        header={<Text strong>Recent Feedback</Text>}\n        dataSource={dataSource}\n        size=\"small\"\n        renderItem={({ dateFormatted, relativeDate, comment }, i) => (\n          <List.Item key={i}>\n            <Col flex={1} style={{ paddingRight: 16 }}>\n              <Paragraph italic ellipsis={{ rows: 2, expandable: true }}>\n                {comment}{' '}\n              </Paragraph>\n            </Col>\n            <Col style={{ minWidth: 100 }}>\n              <Tooltip title={dateFormatted}>\n                <span style={{ color: '#666' }}>{relativeDate}</span>\n              </Tooltip>\n            </Col>\n          </List.Item>\n        )}\n      />\n      {expansionNeeded &&\n        (allFeedbackVisible ? (\n          <Button className=\"no-print\" onClick={showFeedbackPartially}>\n            Show partially\n          </Button>\n        ) : (\n          <Button className=\"no-print\" onClick={showAllFeedback}>\n            Show all\n          </Button>\n        ))}\n    </div>\n  ) : null;\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/GratitudeSection/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { GratitudeDto } from '@client/api';\nimport { GratitudeSection } from './index';\n\nvi.mock('./GratitudeList', () => ({\n  GratitudeList: () => <div>Mock GratitudeList</div>,\n}));\n\ndescribe('GratitudeSection', () => {\n  test('should display nothing if data is empty', () => {\n    const { container } = render(<GratitudeSection data={[]} />);\n    expect(container).toBeEmptyDOMElement();\n  });\n\n  test('should display nothing if data is not provided', () => {\n    const { container } = render(<GratitudeSection data={null} />);\n    expect(container).toBeEmptyDOMElement();\n  });\n\n  test('should display gratitudes list if data is provided', () => {\n    const mockData = ['Feeback 1', 'Feedback 2', 'Feedback 3'] as unknown as GratitudeDto[];\n\n    render(<GratitudeSection data={mockData} />);\n\n    const totalCount = screen.getByText(`Total count: ${mockData.length}`);\n    const gratitudeList = screen.getByText('Mock GratitudeList');\n\n    expect(totalCount).toBeInTheDocument();\n    expect(gratitudeList).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/GratitudeSection/index.tsx",
    "content": "import { Typography } from 'antd';\nimport { BaseSection } from '../BaseSection';\nimport { GratitudeList } from './GratitudeList';\nimport { GratitudeDto } from '@client/api';\n\nconst { Text } = Typography;\n\ntype Props = {\n  data: GratitudeDto[] | null;\n};\n\nexport const GratitudeSection = ({ data }: Props) => {\n  if (!data?.length) {\n    return null;\n  }\n\n  return (\n    <BaseSection title=\"Gratitude\">\n      <Text>Total count: {data!.length}</Text>\n      <br />\n      <GratitudeList feedback={data} showCount={5} />\n    </BaseSection>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/PersonalSection/PersonalSection.module.css",
    "content": ".title {\n  font-size: 12px;\n  color: #eee;\n  text-transform: uppercase;\n}\n\n.value {\n  padding-bottom: 16px;\n  font-weight: bold;\n  word-break: break-all;\n}\n\n@media print {\n  .title,\n  .value {\n    color: #000;\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/PersonalSection/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { UserData } from '@client/modules/Opportunities/models';\nimport { getPersonalToRender } from '@client/modules/Opportunities/data/getPersonalToRender';\nimport { PersonalSection } from './index';\n\nvi.mock('@client/modules/Opportunities/data/getPersonalToRender');\n\ndescribe('PersonalSection', () => {\n  test('should display nothing if user data is not provided', () => {\n    const { container } = render(<PersonalSection user={null} />);\n    expect(container).toBeEmptyDOMElement();\n  });\n\n  test('should display personal data if provided', () => {\n    const mockData = [\n      {\n        title: 'Title 1',\n        value: 'Value 1',\n      },\n      {\n        title: 'Title 2',\n        value: 'Value 2',\n      },\n      {\n        title: 'Title 3',\n        value: 'Value 3',\n      },\n    ];\n    vi.mocked(getPersonalToRender).mockReturnValue(mockData);\n\n    render(<PersonalSection user={{} as UserData} />);\n\n    mockData.forEach(({ title, value }) => {\n      const titleElement = screen.getByText(title);\n      const valueElement = screen.getByText(value);\n\n      expect(titleElement).toBeInTheDocument();\n      expect(valueElement).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/PersonalSection/index.tsx",
    "content": "import { UserData } from '@client/modules/Opportunities/models';\nimport { Fragment } from 'react';\nimport { getPersonalToRender } from '@client/modules/Opportunities/data/getPersonalToRender';\n\nimport styles from './PersonalSection.module.css';\n\ntype Props = {\n  user: UserData | null;\n};\n\nexport function PersonalSection({ user }: Props) {\n  if (user == null) {\n    return null;\n  }\n  const data = getPersonalToRender(user);\n\n  return (\n    <div>\n      <div>\n        {data.map(({ title, value }) => (\n          <Fragment key={title}>\n            <div className={styles.title}>{title}</div>\n            <div className={styles.value}>{value}</div>\n          </Fragment>\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/index.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { ResumeDto } from '@client/api';\nimport { ExpirationState } from '@client/modules/Opportunities/constants';\nimport { useViewData, useExpiration } from '@client/modules/Opportunities/hooks';\nimport { ViewCV } from './index';\n\nvi.mock('@client/modules/Opportunities/components/ExpirationTooltip', () => ({\n  ExpirationTooltip: () => <div>ExpirationTooltip</div>,\n}));\n\nvi.mock('./AboutSection', () => ({\n  AboutSection: () => <div>AboutSection</div>,\n}));\n\nvi.mock('./ContactsSection', () => ({\n  ContactsSection: () => <div>ContactsSection</div>,\n}));\n\nvi.mock('./CoursesSection', () => ({\n  CoursesSection: () => <div>CoursesSection</div>,\n}));\n\nvi.mock('./FeedbackSection', () => ({\n  FeedbackSection: () => <div>FeedbackSection</div>,\n}));\n\nvi.mock('./GratitudeSection', () => ({\n  GratitudeSection: () => <div>GratitudeSection</div>,\n}));\n\nvi.mock('./PersonalSection', () => ({\n  PersonalSection: () => <div>PersonalSection</div>,\n}));\n\nvi.mock('../NameTitle', () => ({\n  NameTitle: () => <div>NameTitle</div>,\n}));\n\nvi.mock('../PublicLink', () => ({\n  PublicLink: ({ url }: { url: string }) => <div>PublicLink {url}</div>,\n}));\n\nvi.mock('./ActionButtons', () => ({\n  ActionButtons: () => <div>ActionButtons</div>,\n}));\n\nvi.mock('@client/modules/Opportunities/hooks');\n\n/*\n    Preerequisities:\n    1. Mock usViewData hook\n    2. Mock use expiration\n\n    Test cases:\n    1. If loading is true, then LoadingScreen should be displayed\n    2.Loading = false + public mode = true => PublicLink should be displayed\n    3.Loading = false + public mode = false => ActionButtons should be displayed\n\n    Mock ExpirationTooltip\n    Mock NameTitle\n    Mock PersonalSection\n    Mock ContactsSection\n    Mock AboutSection\n    Mock CoursesSection\n    Mock FeedbackSection\n    Mock GratitudeSection\n    4.Loading = false + userData => Expiration\n*/\n\nconst mockUuid = '13791ec3-83b9-44ce-95c5-f06837a71966';\n\ndescribe('ViewCV', () => {\n  test('should display loading screeen if loading is true', () => {\n    vi.mocked(useViewData).mockReturnValue({ loading: true });\n    vi.mocked(useExpiration).mockReturnValue({\n      expirationState: ExpirationState.NotExpired,\n      expirationDateFormatted: '2021-01-01',\n    });\n\n    render(<ViewCV initialData={{} as ResumeDto} />);\n\n    const loadingScreen = screen.getByText('Loading...');\n\n    expect(loadingScreen).toBeInTheDocument();\n  });\n\n  test('should display public link in public mode', () => {\n    vi.mocked(useViewData).mockReturnValue({ loading: false, uuid: mockUuid });\n    vi.mocked(useExpiration).mockReturnValue({\n      expirationState: ExpirationState.NotExpired,\n      expirationDateFormatted: '2021-01-01',\n    });\n\n    render(<ViewCV initialData={{} as ResumeDto} publicMode={true} />);\n\n    const publicLink = screen.getByText(`PublicLink ${window.location.origin}/cv/${mockUuid}`);\n    const actionButtons = screen.queryByText('ActionButtons');\n\n    expect(publicLink).toBeInTheDocument();\n    expect(actionButtons).not.toBeInTheDocument();\n  });\n\n  test('should display action buttons in non public mode', () => {\n    vi.mocked(useViewData).mockReturnValue({ loading: false, uuid: mockUuid });\n    vi.mocked(useExpiration).mockReturnValue({\n      expirationState: ExpirationState.NotExpired,\n      expirationDateFormatted: '2021-01-01',\n    });\n\n    render(<ViewCV initialData={{} as ResumeDto} publicMode={false} />);\n\n    const publicLink = screen.queryByText(`PublicLink ${window.location.origin}/cv/${mockUuid}`);\n    const actionButtons = screen.getByText('ActionButtons');\n\n    expect(publicLink).not.toBeInTheDocument();\n    expect(actionButtons).toBeInTheDocument();\n  });\n\n  test('should not display userData-related content if userData is provided', () => {\n    vi.mocked(useViewData).mockReturnValue({ loading: false, uuid: mockUuid, userData: {} });\n    vi.mocked(useExpiration).mockReturnValue({\n      expirationState: ExpirationState.NotExpired,\n      expirationDateFormatted: '2021-01-01',\n    });\n\n    render(<ViewCV initialData={{} as ResumeDto} publicMode={false} />);\n\n    const expiration = screen.queryByText('ExpirationTooltip');\n    const nameTitle = screen.queryByText('NameTitle');\n    const personalSection = screen.queryByText('PersonalSection');\n    const contactsSection = screen.queryByText('ContactsSection');\n    const aboutSection = screen.queryByText('AboutSection');\n    const coursesSection = screen.queryByText('CoursesSection');\n    const feedbackSection = screen.queryByText('FeedbackSection');\n    const gratitudeSection = screen.queryByText('GratitudeSection');\n\n    expect(expiration).toBeInTheDocument();\n    expect(nameTitle).toBeInTheDocument();\n    expect(personalSection).toBeInTheDocument();\n    expect(contactsSection).toBeInTheDocument();\n    expect(aboutSection).toBeInTheDocument();\n    expect(coursesSection).toBeInTheDocument();\n    expect(feedbackSection).toBeInTheDocument();\n    expect(gratitudeSection).toBeInTheDocument();\n  });\n\n  test('should not display userData-related content if userData is not provided', () => {\n    vi.mocked(useViewData).mockReturnValue({ loading: false, uuid: mockUuid, userData: null });\n    vi.mocked(useExpiration).mockReturnValue({\n      expirationState: ExpirationState.NotExpired,\n      expirationDateFormatted: '2021-01-01',\n    });\n\n    render(<ViewCV initialData={{} as ResumeDto} publicMode={false} />);\n\n    const expiration = screen.queryByText('Expiration');\n    const nameTitle = screen.queryByText('NameTitle');\n    const personalSection = screen.queryByText('PersonalSection');\n    const contactsSection = screen.queryByText('ContactsSection');\n    const aboutSection = screen.queryByText('AboutSection');\n    const coursesSection = screen.queryByText('CoursesSection');\n    const feedbackSection = screen.queryByText('FeedbackSection');\n    const gratitudeSection = screen.queryByText('GratitudeSection');\n\n    expect(expiration).not.toBeInTheDocument();\n    expect(nameTitle).not.toBeInTheDocument();\n    expect(personalSection).not.toBeInTheDocument();\n    expect(contactsSection).not.toBeInTheDocument();\n    expect(aboutSection).not.toBeInTheDocument();\n    expect(coursesSection).not.toBeInTheDocument();\n    expect(feedbackSection).not.toBeInTheDocument();\n    expect(gratitudeSection).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/components/ViewCv/index.tsx",
    "content": "import { Col, Divider, Flex, Row, theme } from 'antd';\nimport { useEffect, useState } from 'react';\nimport { ResumeDto } from '@client/api';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport { useExpiration, useViewData } from '@client/modules/Opportunities/hooks';\nimport { ExpirationTooltip } from '@client/modules/Opportunities/components/ExpirationTooltip';\nimport { ExpirationState } from '@client/modules/Opportunities/constants';\nimport { AboutSection } from './AboutSection';\nimport { ContactsSection } from './ContactsSection';\nimport { CoursesSection } from './CoursesSection';\nimport { FeedbackSection } from './FeedbackSection';\nimport { GratitudeSection } from './GratitudeSection';\nimport { PersonalSection } from './PersonalSection';\nimport { NameTitle } from '../NameTitle';\nimport { PublicLink } from '../PublicLink';\nimport { ActionButtons } from './ActionButtons';\n\ntype Props = {\n  initialData: ResumeDto;\n  onRemoveConsent?: () => void;\n  switchView?: () => void;\n  publicMode?: boolean;\n};\n\nexport const ViewCV = ({ initialData, publicMode, onRemoveConsent, switchView }: Props) => {\n  const { loading, uuid, userData, contacts, courses, feedbacks, gratitudes, expires } = useViewData({ initialData });\n  const { expirationState, expirationDateFormatted } = useExpiration(expires as unknown as string);\n  const [url, setUrl] = useState('');\n\n  useEffect(() => {\n    setUrl(`${window.location.origin}/cv/${uuid}`);\n  }, [uuid]);\n\n  const { token } = theme.useToken();\n\n  return (\n    <LoadingScreen show={loading}>\n      {publicMode ? (\n        <PublicLink url={url} />\n      ) : (\n        <ActionButtons\n          onRemoveConsent={onRemoveConsent}\n          switchView={switchView}\n          url={url}\n          isExpired={expirationState === ExpirationState.Expired}\n        />\n      )}\n      {userData && (\n        <Row className=\"print-no-padding\" style={{ minHeight: '100vh', minWidth: '300px', padding: 10 }}>\n          <Col xl={8} lg={8} md={10} sm={24} xs={24} className=\"cv-sidebar\">\n            <Row justify=\"space-between\">\n              <ExpirationTooltip\n                publicMode={publicMode}\n                expirationDate={expirationDateFormatted}\n                expirationState={expirationState}\n              />\n            </Row>\n            <Row>\n              <NameTitle userData={userData} />\n            </Row>\n            <Divider\n              style={{\n                margin: '8px 0',\n                backgroundColor: token.colorBgContainer,\n              }}\n            />\n            <Row gutter={24}>\n              <Col xs={12} sm={12} md={24} lg={24} style={{ marginTop: 16 }}>\n                <PersonalSection user={userData} />\n              </Col>\n              <Divider\n                style={{\n                  margin: '8px 0',\n                  backgroundColor: token.colorBgContainer,\n                }}\n              />\n              <Col xs={12} sm={12} md={24} lg={24} style={{ marginTop: 16 }}>\n                <ContactsSection contacts={contacts} />\n              </Col>\n            </Row>\n          </Col>\n          <Col xl={16} lg={16} md={14} sm={24} xs={24}>\n            <Flex vertical gap=\"small\">\n              <AboutSection notes={userData.notes} />\n              <CoursesSection visibleCourses={initialData.visibleCourses} courses={courses ?? []} />\n              <FeedbackSection data={feedbacks} />\n              <GratitudeSection data={gratitudes} />\n            </Flex>\n          </Col>\n        </Row>\n      )}\n    </LoadingScreen>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/constants.ts",
    "content": "export const enum ExpirationState {\n  Expired,\n  NearlyExpired,\n  NotExpired,\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/data/__snapshots__/getPersonalToRender.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`getPersonalToRender > should correctly handle missing values 1`] = `\n[\n  {\n    \"title\": \"English\",\n    \"value\": null,\n  },\n  {\n    \"title\": \"Self Introduction\",\n    \"value\": \"Not Available\",\n  },\n  {\n    \"title\": \"Ready to work full time\",\n    \"value\": \"No\",\n  },\n  {\n    \"title\": \"Ready to work from\",\n    \"value\": null,\n  },\n  {\n    \"title\": \"Military service status\",\n    \"value\": \"Unknown\",\n  },\n  {\n    \"title\": \"Locations\",\n    \"value\": \"Unknown\",\n  },\n]\n`;\n\nexports[`getPersonalToRender > should return correct data in case all values are present 1`] = `\n[\n  {\n    \"title\": \"English\",\n    \"value\": \"B1\",\n  },\n  {\n    \"title\": \"Self Introduction\",\n    \"value\": <a\n      className=\"rs-link\"\n      href=\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\"\n      rel=\"nofollow\"\n      target=\"_blank\"\n    >\n      https://www.youtube.com/watch?v=dQw4w9WgXcQ\n    </a>,\n  },\n  {\n    \"title\": \"Ready to work full time\",\n    \"value\": \"Yes\",\n  },\n  {\n    \"title\": \"Ready to work from\",\n    \"value\": \"2022-09-24\",\n  },\n  {\n    \"title\": \"Military service status\",\n    \"value\": \"Not liable\",\n  },\n  {\n    \"title\": \"Locations\",\n    \"value\": \"Minsk Belarus\",\n  },\n]\n`;\n"
  },
  {
    "path": "client/src/modules/Opportunities/data/getContactsToRender.test.tsx",
    "content": "import { getContactsToRender } from './getContactsToRender';\n\nconst mockContacts = {\n  phone: '1',\n  email: null,\n  skype: '3',\n  telegram: null,\n  linkedin: null,\n  githubUsername: '6',\n  website: '7',\n};\n\ndescribe('getContactsToRender', () => {\n  test('should return empty array if no contacts', () => {\n    const view = getContactsToRender(null);\n    expect(view).toStrictEqual([]);\n  });\n\n  test('should return contacts to render', () => {\n    const view = getContactsToRender(mockContacts);\n    expect(view).toStrictEqual([\n      ['phone', '1'],\n      ['skype', '3'],\n      ['githubUsername', '6'],\n      ['website', '7'],\n    ]);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/data/getContactsToRender.ts",
    "content": "import { Contacts } from '../models';\n\ntype EntryOf<T extends object> = { [K in keyof T]: [K, T[K]] }[keyof T];\n\nexport const getContactsToRender = (contacts: Contacts | null) => {\n  if (!contacts) {\n    return [];\n  }\n  const contactsEntries = Object.entries(contacts);\n  return contactsEntries.filter((contact): contact is EntryOf<Contacts> => contact[1] !== null);\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/data/getPersonalToRender.test.tsx",
    "content": "import { UserData } from '../models';\nimport { getPersonalToRender } from './getPersonalToRender';\n\ndescribe('getPersonalToRender', () => {\n  it('should return correct data in case all values are present', () => {\n    const mockUserData = {\n      englishLevel: 'B1',\n      fullTime: true,\n      locations: 'Minsk Belarus',\n      militaryService: 'notLiable',\n      selfIntroLink: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',\n      startFrom: '2022-09-24',\n    } as unknown as UserData;\n\n    const view = getPersonalToRender(mockUserData);\n    expect(view).toMatchSnapshot();\n  });\n\n  it('should correctly handle missing values', () => {\n    const mockUserData = {\n      englishLevel: null,\n      fullTime: false,\n      locations: null,\n      militaryService: null,\n      selfIntroLink: null,\n      startFrom: null,\n    } as unknown as UserData;\n\n    const view = getPersonalToRender(mockUserData);\n    expect(view).toMatchSnapshot();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/data/getPersonalToRender.tsx",
    "content": "import { UserData } from '../models';\nimport capitalize from 'lodash/capitalize';\nimport { ResumeDtoMilitaryServiceEnum } from '@client/api';\n\nexport function getPersonalToRender(user: UserData) {\n  const { selfIntroLink } = user;\n  return [\n    {\n      title: 'English',\n      value: user.englishLevel,\n    },\n    {\n      title: 'Self Introduction',\n      value: selfIntroLink ? (\n        <a className=\"rs-link\" target=\"_blank\" rel=\"nofollow\" href={selfIntroLink}>\n          {selfIntroLink}\n        </a>\n      ) : (\n        'Not Available'\n      ),\n    },\n    {\n      title: 'Ready to work full time',\n      value: user.fullTime ? 'Yes' : 'No',\n    },\n    {\n      title: 'Ready to work from',\n      value: user.startFrom,\n    },\n    {\n      title: 'Military service status',\n      value: user.militaryService ? capitalize(militaryServiceDictionary[user.militaryService]) : 'Unknown',\n    },\n    {\n      title: 'Locations',\n      value: user.locations ? user.locations.split(',').join(', ') : 'Unknown',\n    },\n  ];\n}\n\nconst militaryServiceDictionary: {\n  [key in ResumeDtoMilitaryServiceEnum]: string;\n} = {\n  served: 'served',\n  notLiable: 'not liable',\n  liable: 'liable',\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/hooks/index.ts",
    "content": "export { useExpiration } from './useExpiration';\nexport { useResumeData } from './useResumeData';\nexport { useViewData } from './useViewData';\n"
  },
  {
    "path": "client/src/modules/Opportunities/hooks/useExpiration.test.tsx",
    "content": "import { renderHook } from '@testing-library/react';\nimport { ExpirationState } from '../constants';\nimport { useExpiration } from './useExpiration';\n\nconst mockCurrentTime = 1664564110455;\n\ndescribe('useExpiration', () => {\n  beforeAll(() => {\n    vi.useFakeTimers().setSystemTime(mockCurrentTime);\n  });\n\n  afterAll(() => {\n    vi.useRealTimers();\n  });\n\n  it('should correctly return NotExpired status', () => {\n    const addition = 30 * 24 * 60 * 60 * 1000;\n    const mockExpiresIn30Days = String(mockCurrentTime + addition);\n    const { result } = renderHook(() => useExpiration(mockExpiresIn30Days));\n    expect(result.current).toStrictEqual({\n      expirationDateFormatted: '2022-10-30',\n      expirationState: ExpirationState.NotExpired,\n    });\n  });\n\n  it('should correctly return NearlyExpired status', () => {\n    const addition = 1 * 24 * 60 * 60 * 1000;\n    const mockExpiresIn1Day = String(mockCurrentTime + addition);\n    const { result } = renderHook(() => useExpiration(mockExpiresIn1Day));\n    expect(result.current).toStrictEqual({\n      expirationDateFormatted: '2022-10-01',\n      expirationState: ExpirationState.NearlyExpired,\n    });\n  });\n\n  it('should correctly return Expired status', () => {\n    const addition = 1 * 24 * 60 * 60 * 1000;\n    const mockExpiresIn1DayBefore = String(mockCurrentTime - addition);\n    const { result } = renderHook(() => useExpiration(mockExpiresIn1DayBefore));\n    expect(result.current).toStrictEqual({\n      expirationDateFormatted: '2022-09-29',\n      expirationState: ExpirationState.Expired,\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/hooks/useExpiration.ts",
    "content": "import dayjs from 'dayjs';\nimport duration from 'dayjs/plugin/duration';\nimport { ExpirationState } from '@client/modules/Opportunities/constants';\n\ndayjs.extend(duration);\n\nexport const useExpiration = (expires: string) => {\n  const expirationDate = dayjs(Number(expires));\n  const expirationDateFormatted = expirationDate.format('YYYY-MM-DD');\n  const diff = expirationDate.diff(Date.now());\n  const daysLeft = dayjs.duration(diff).asDays();\n  const expirationState =\n    daysLeft < 0 ? ExpirationState.Expired : daysLeft < 2 ? ExpirationState.NearlyExpired : ExpirationState.NotExpired;\n\n  return {\n    expirationDateFormatted,\n    expirationState,\n  };\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/hooks/useResumeData.test.tsx",
    "content": "import { renderHook, waitFor } from '@testing-library/react';\nimport { OpportunitiesApi, ResumeDto } from '@client/api';\nimport { useResumeData } from './useResumeData';\nimport { AxiosResponse } from 'axios';\n\nconst mockGithubId = 'test';\nconst mockActualTime = 1664564110455;\nconst mockResumeData = {\n  data: 'test',\n};\n\ndescribe('useResumeData', () => {\n  it('should return resume data', async () => {\n    vi.spyOn(OpportunitiesApi.prototype, 'getResume').mockImplementation(() =>\n      Promise.resolve({\n        status: 200,\n        statusText: 'OK',\n        headers: {},\n        config: {},\n        data: mockResumeData as unknown as ResumeDto,\n      } as AxiosResponse<ResumeDto>),\n    );\n    const { result } = renderHook(() => useResumeData({ githubId: mockGithubId, actualTime: mockActualTime }));\n    await waitFor(() => {\n      expect(result.current[0]).toBe(mockResumeData);\n    });\n  });\n\n  it('should return null in case of 404 error', async () => {\n    vi.spyOn(OpportunitiesApi.prototype, 'getResume').mockImplementation(() =>\n      Promise.reject({\n        response: {\n          status: 404,\n          statusText: 'Not Found',\n          headers: {},\n          config: {},\n        },\n      }),\n    );\n    const { result } = renderHook(() => useResumeData({ githubId: mockGithubId, actualTime: mockActualTime }));\n    await waitFor(() => {\n      expect(result.current[0]).toBe(null);\n    });\n  });\n\n  it('should throw error in case of unexpected error', async () => {\n    const mockErrorResponse = {\n      response: {\n        status: 500,\n        statusText: 'Internal Server Error',\n        headers: {},\n        config: {},\n      },\n    };\n    vi.spyOn(OpportunitiesApi.prototype, 'getResume').mockImplementation(() => Promise.reject(mockErrorResponse));\n    const { result } = renderHook(() => useResumeData({ githubId: mockGithubId, actualTime: mockActualTime }));\n    await waitFor(() => {\n      expect(result.current[1]).toBe(mockErrorResponse);\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/hooks/useResumeData.tsx",
    "content": "import { OpportunitiesApi } from '@client/api';\nimport { AxiosError } from 'axios';\nimport { useAsync } from 'react-use';\n\ntype Props = {\n  githubId: string;\n  actualTime: number;\n};\n\nconst opportunitiesService = new OpportunitiesApi();\n\nexport function useResumeData({ githubId, actualTime }: Props) {\n  const fetchData = useAsync(async () => {\n    try {\n      const { data } = await opportunitiesService.getResume(githubId);\n      return data;\n    } catch (err) {\n      const error = err as AxiosError;\n      if (error.response?.status === 404) {\n        return null;\n      }\n      throw err;\n    }\n  }, [actualTime]);\n\n  return [fetchData.value, fetchData.error, fetchData.loading] as const;\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/hooks/useViewData.test.tsx",
    "content": "import { renderHook, waitFor } from '@testing-library/react';\nimport { ResumeDtoEnglishLevelEnum, ResumeDto, ResumeDtoMilitaryServiceEnum } from '@client/api';\nimport { useViewData } from './useViewData';\n\nconst mockResume = {\n  id: 12345,\n  uuid: '0882dc34-415d-4fa2-919d-37cab831db3a',\n  avatarLink: 'https://example.com/avatar.png',\n  desiredPosition: 'Senior Cookie Manager',\n  email: 'example@gmail.com',\n  englishLevel: ResumeDtoEnglishLevelEnum.A2,\n  expires: '1666626604224',\n  fullTime: true,\n  githubUsername: null,\n  linkedin: null,\n  locations: 'Minsk Belarus',\n  militaryService: ResumeDtoMilitaryServiceEnum.NotLiable,\n  name: 'John Doe',\n  notes: null,\n  phone: '+1111111111111',\n  selfIntroLink: null,\n  skype: null,\n  startFrom: '2022-09-24',\n  telegram: null,\n  website: null,\n  visibleCourses: [11, 21, 53],\n  gratitudes: [\n    {\n      date: '2022-06-13T18:55:18.761Z',\n      comment: 'Большое спасибо за замечательный фидьбек!',\n    },\n  ],\n  courses: [],\n  feedbacks: [],\n} as unknown as ResumeDto;\n\ndescribe('useViewData', () => {\n  test('should split data if provided', async () => {\n    const { result } = renderHook(() => useViewData({ initialData: mockResume }));\n\n    const expected = {\n      contacts: {\n        email: mockResume.email,\n        githubUsername: mockResume.githubUsername,\n        linkedin: mockResume.linkedin,\n        phone: mockResume.phone,\n        skype: mockResume.skype,\n        telegram: mockResume.telegram,\n        website: mockResume.website,\n      },\n      courses: mockResume.courses,\n      expires: mockResume.expires,\n      feedbacks: mockResume.feedbacks,\n      gratitudes: mockResume.gratitudes,\n      loading: false,\n      setExpires: expect.any(Function),\n      userData: {\n        avatarLink: mockResume.avatarLink,\n        desiredPosition: mockResume.desiredPosition,\n        englishLevel: mockResume.englishLevel,\n        fullTime: mockResume.fullTime,\n        locations: mockResume.locations,\n        militaryService: mockResume.militaryService,\n        name: mockResume.name,\n        notes: mockResume.notes,\n        selfIntroLink: mockResume.selfIntroLink,\n        startFrom: mockResume.startFrom,\n      },\n      uuid: mockResume.uuid,\n    };\n\n    await waitFor(() => expect(result.current).toStrictEqual(expected));\n  });\n\n  test('should return empty data if not provided', async () => {\n    const { result } = renderHook(() => useViewData({}));\n\n    await waitFor(() => {\n      expect(result.current).toStrictEqual({\n        contacts: null,\n        courses: [],\n        expires: null,\n        feedbacks: [],\n        gratitudes: [],\n        loading: true,\n        setExpires: expect.any(Function),\n        userData: null,\n        uuid: null,\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/hooks/useViewData.tsx",
    "content": "import { FeedbackDto, GratitudeDto, ResumeCourseDto, ResumeDto } from '@client/api';\nimport { useCallback, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { Contacts, UserData } from '../models';\n\ntype Props = {\n  initialData?: ResumeDto;\n};\n\nexport function useViewData({ initialData: resume }: Props) {\n  const [loading, setLoading] = useState<boolean>(false);\n  const [contacts, setContacts] = useState<Contacts | null>(null);\n  const [userData, setUserData] = useState<UserData | null>(null);\n  const [courses, setCourses] = useState<ResumeCourseDto[]>([]);\n  const [gratitudes, setGratitudes] = useState<GratitudeDto[]>([]);\n  const [feedbacks, setFeedbacks] = useState<FeedbackDto[]>([]);\n  const [expires, setExpires] = useState<number | null>(null);\n  const [uuid, setUuid] = useState<string | null>(null);\n\n  const fetchData = useCallback(async () => {\n    setLoading(true);\n\n    const { data } = { data: resume };\n\n    if (!data) {\n      return;\n    }\n\n    const {\n      notes,\n      name,\n      selfIntroLink,\n      startFrom,\n      militaryService,\n      avatarLink,\n      desiredPosition,\n      englishLevel,\n      email,\n      githubUsername,\n      linkedin,\n      locations,\n      phone,\n      skype,\n      telegram,\n      website,\n      fullTime,\n      courses,\n      gratitudes,\n      feedbacks,\n      uuid,\n      expires,\n    } = data;\n\n    const userData = {\n      notes,\n      name,\n      selfIntroLink,\n      militaryService: militaryService,\n      avatarLink,\n      locations,\n      desiredPosition,\n      englishLevel,\n      startFrom,\n      fullTime,\n    };\n\n    const contactsList = {\n      email,\n      githubUsername,\n      linkedin,\n      phone,\n      skype,\n      telegram,\n      website,\n    };\n\n    setContacts(contactsList);\n    setUserData(userData);\n    setCourses(courses);\n    setGratitudes(gratitudes);\n    setFeedbacks(feedbacks);\n    setExpires(expires);\n    setUuid(uuid);\n    setLoading(false);\n  }, []);\n\n  useAsync(fetchData, []);\n\n  return { userData, loading, contacts, courses, feedbacks, gratitudes, expires, uuid, setExpires };\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/models.ts",
    "content": "import { ResumeDtoEnglishLevelEnum, ResumeDtoMilitaryServiceEnum } from '@client/api';\nimport { Dayjs } from 'dayjs';\n\nexport interface CourseData {\n  locationName: string;\n  courseFullName: string;\n  isExpelled: boolean;\n  certificateId: string | null;\n  isCourseCompleted: boolean;\n  totalScore: number;\n  rank: number | null;\n  mentor: {\n    githubId: string;\n    name: string;\n  };\n}\n\nexport type VisibleCourses = number[];\n\nexport interface CourseDataShortened {\n  courseId: number;\n  courseFullName: string;\n}\n\nexport interface VisibleCoursesFormData {\n  [id: string]: boolean;\n}\n\nexport type ContactType = 'phone' | 'email' | 'skype' | 'telegram' | 'linkedin' | 'githubUsername' | 'website';\n\nexport interface UserData {\n  avatarLink: string | null;\n  name: string | null;\n  desiredPosition: string | null;\n  selfIntroLink: string | null;\n  englishLevel: ResumeDtoEnglishLevelEnum | null;\n  militaryService: ResumeDtoMilitaryServiceEnum | null;\n  notes: string | null;\n  startFrom: string | null;\n  fullTime: boolean;\n  locations: string | null;\n}\n\nexport type Contacts = {\n  [key in ContactType]: string | null;\n};\n\nexport interface FieldData {\n  name: string[];\n  value: unknown;\n  touched: boolean;\n  validating: boolean;\n  errors: string[];\n}\n\nexport interface AllUserCVData extends Omit<UserData, 'uuid'>, Contacts {\n  visibleCourses: VisibleCourses;\n}\n\nexport interface UserDataToSubmit extends Omit<UserData, 'startFrom'> {\n  startFrom: Dayjs;\n  visibleCourses: VisibleCourses;\n}\n\nexport interface AllDataToSubmit extends UserDataToSubmit, Contacts {}\n\nexport interface EditCVData extends AllUserCVData {\n  expires: number | null;\n  courses: CourseDataShortened[];\n  visibleCourses: VisibleCourses;\n}\n\nexport interface GetFullCVData extends AllUserCVData {\n  expires: number | null;\n  courses: CVStudentStats[];\n  feedback: CVFeedback[];\n}\n\nexport interface CVStudentStats {\n  locationName: string;\n  courseFullName: string;\n  certificateId: string | null;\n  isCourseCompleted: boolean;\n  totalScore: number;\n  rank: number | null;\n  mentor: {\n    githubId: string;\n    name: string;\n  };\n}\n\nexport interface JobSeekerStudentStats extends CVStudentStats {\n  courseName: string;\n}\n\nexport interface JobSeekerData {\n  name: string | null;\n  desiredPosition: string | null;\n  githubId: string;\n  englishLevel: ResumeDtoEnglishLevelEnum | null;\n  fullTime: boolean;\n  locations: string | null;\n  startFrom: string | null;\n  courses: JobSeekerStudentStats[];\n  feedback: JobSeekerFeedback[];\n  expires: number;\n  isHidden: boolean;\n}\n\nexport interface CVFeedback {\n  comment: string;\n  feedbackDate: string;\n}\n\nexport interface JobSeekerFeedback {\n  badgeId: string;\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/pages/EditPage/index.tsx",
    "content": "import { useContext, useEffect, useState } from 'react';\nimport { Layout, Modal, Typography } from 'antd';\nimport { AxiosError } from 'axios';\nimport dayjs from 'dayjs';\nimport Head from 'next/head';\nimport { OpportunitiesApi, ResumeDto } from '@client/api';\nimport { Header } from '@client/shared/components/Header';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport { useLoading } from '@client/components/useLoading';\nimport { SessionContext } from '@client/modules/Course/contexts';\nimport { EditViewCv } from '@client/modules/Opportunities/components/EditViewCv';\n\nconst { Content } = Layout;\nconst { Text, Paragraph } = Typography;\n\nconst service = new OpportunitiesApi();\n\nexport function EditPage() {\n  const { githubId } = useContext(SessionContext);\n  const [loading, withLoading] = useLoading(false);\n  const [editMode, setEditMode] = useState<boolean>(false);\n  const [consent, setConsent] = useState<boolean>(false);\n  const [resume, setResume] = useState<ResumeDto | null>(null);\n  const [modal, contextHolder] = Modal.useModal();\n\n  const switchView = () => setEditMode(!editMode);\n\n  useEffect(() => {\n    getData();\n  }, [editMode]);\n\n  const getData = withLoading(async () => {\n    const { data } = await service.getConsent();\n    if (data.consent) {\n      try {\n        const { data } = await service.getResume(githubId);\n        setResume(data);\n      } catch (err) {\n        const error = err as AxiosError;\n        if (error.response?.status === 404) {\n          setResume(null);\n          return;\n        }\n        throw err;\n      }\n    }\n    setConsent(data.consent);\n  });\n\n  const handleDeleteConsent = withLoading(async () => {\n    const { data } = await service.deleteConsent();\n    await getData();\n    return data;\n  });\n\n  const showCreationModal = (validUntilTimestamp: number) => {\n    const validUntil = dayjs(validUntilTimestamp).format('YY-MM-DD');\n\n    const title = <Text strong>Your CV is now public until {validUntil} (for 1 month period from now on)</Text>;\n    const content = (\n      <>\n        <Paragraph>You need to renew your CV every month.</Paragraph>\n        <Paragraph>\n          {' '}\n          Otherwise it will not be shown to other RS App users.{' '}\n          <a href=\"https://docs.app.rs.school/#/platform/cv\" target=\"_blank\" rel=\"noreferrer\">\n            Learn more about CV\n          </a>\n        </Paragraph>\n      </>\n    );\n\n    modal.info({\n      title,\n      content,\n      okText: 'Got it',\n    });\n  };\n\n  const handleCreateConsent = withLoading(async () => {\n    const { data } = await service.createConsent();\n    await getData();\n    showCreationModal(data.expires);\n    setEditMode(true);\n  });\n\n  return (\n    <>\n      {contextHolder}\n      <Head>\n        <link href=\"https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;700&display=swap\" rel=\"stylesheet\" />\n      </Head>\n      <LoadingScreen show={loading}>\n        <Header title=\"My CV\" />\n        <Content className=\"print-no-padding ubuntu-font\" style={{ maxWidth: 960, margin: 'auto' }}>\n          <EditViewCv\n            githubId={githubId}\n            consent={consent}\n            data={resume}\n            editMode={editMode || resume == null}\n            switchView={switchView}\n            onRemoveConsent={handleDeleteConsent}\n            onCreateConsent={handleCreateConsent}\n            onUpdateResume={() => getData()}\n          />\n        </Content>\n      </LoadingScreen>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/pages/PublicPage/getServerSideProps.ts",
    "content": "import { GetServerSidePropsContext } from 'next';\nimport { OpportunitiesApi } from '@client/api';\nimport { BASE_PATH } from '@client/api/base';\nimport { Configuration } from '@client/api/configuration';\nimport { AxiosRequestConfig } from 'axios';\n\nconst rsHost = process.env.RS_HOST || '';\n\nfunction getNestJsServerAxiosProps(token?: string): Partial<AxiosRequestConfig> {\n  return {\n    baseURL: rsHost ? rsHost + BASE_PATH : undefined,\n    headers: token ? { Authorization: `Bearer ${token}` } : undefined,\n  };\n}\n\nfunction getApiConfiguration(token?: string): Configuration {\n  const props = getNestJsServerAxiosProps(token);\n  return new Configuration({ basePath: props.baseURL, baseOptions: props });\n}\n\nconst opportunitiesApi = new OpportunitiesApi(getApiConfiguration());\n\nexport const getServerSideProps = async (ctx: GetServerSidePropsContext) => {\n  const uuid = ctx.params?.uuid as string;\n  try {\n    const { data } = await opportunitiesApi.getPublicResume(uuid);\n    return {\n      props: { data },\n    };\n  } catch (err) {\n    console.error(err);\n\n    return {\n      redirect: {\n        destination: '/',\n        permanent: false,\n      },\n    };\n  }\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/pages/PublicPage/index.tsx",
    "content": "import { Layout } from 'antd';\nimport { ResumeDto } from '@client/api';\nimport { ViewCV } from '@client/modules/Opportunities/components/ViewCv';\nimport Head from 'next/head';\nimport { PropsWithChildren } from 'react';\n\ntype Props = { data: ResumeDto };\n\nexport function PublicPage({ data }: PropsWithChildren<Props>) {\n  const title = `CV / ${data.name ?? data.githubUsername ?? '(Empty)'} / RS School`;\n  return (\n    <>\n      <Head>\n        <title>{title}</title>\n        <meta property=\"og:title\" content={title} />\n        <meta property=\"og:type\" content=\"website\" />\n        <meta property=\"og:url\" content={`https://app.rs.school/cv/${data.uuid}`} />\n        <meta\n          property=\"og:image\"\n          content={data.avatarLink || 'https://app.rs.school/static/images/logo-rsschool3.png'}\n        />\n        <link href=\"https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;700&display=swap\" rel=\"stylesheet\" />\n      </Head>\n      <Layout>\n        <Layout.Content className=\"print-no-padding ubuntu-font\" style={{ maxWidth: 960, margin: 'auto' }}>\n          <ViewCV publicMode initialData={data} />\n        </Layout.Content>\n      </Layout>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Opportunities/transformers/index.ts",
    "content": "export { splitDataForForms } from './splitDataForForms';\nexport { transformFieldsData } from './transformFieldsData';\nexport { transformInitialCvData } from './transformInitialCvData';\n"
  },
  {
    "path": "client/src/modules/Opportunities/transformers/splitDataForForms.test.ts",
    "content": "import { ResumeDtoEnglishLevelEnum, ResumeDtoMilitaryServiceEnum } from '@client/api';\nimport { AllUserCVData } from '../models';\nimport { splitDataForForms } from './splitDataForForms';\n\nconst mockInitialData: AllUserCVData = {\n  avatarLink: 'https://example.com/avatar.png',\n  desiredPosition: 'Senior Cookie Manager',\n  email: 'example@gmail.com',\n  englishLevel: ResumeDtoEnglishLevelEnum.A2,\n  fullTime: true,\n  githubUsername: null,\n  linkedin: null,\n  locations: 'Minsk Belarus',\n  militaryService: ResumeDtoMilitaryServiceEnum.NotLiable,\n  name: 'John Doe',\n  notes: null,\n  phone: '+1111111111111',\n  selfIntroLink: null,\n  skype: null,\n  startFrom: '2022-09-24',\n  telegram: null,\n  website: null,\n  visibleCourses: [11, 21, 53],\n};\n\ndescribe('splitDataForForms', () => {\n  it('should split data for forms', () => {\n    const { userData, contacts, visibleCourses } = splitDataForForms(mockInitialData);\n\n    expect(userData).toEqual({\n      selfIntroLink: mockInitialData.selfIntroLink,\n      militaryService: mockInitialData.militaryService,\n      avatarLink: mockInitialData.avatarLink,\n      desiredPosition: mockInitialData.desiredPosition,\n      englishLevel: mockInitialData.englishLevel,\n      locations: mockInitialData.locations,\n      name: mockInitialData.name,\n      notes: mockInitialData.notes,\n      startFrom: mockInitialData.startFrom,\n      fullTime: mockInitialData.fullTime,\n    });\n\n    expect(contacts).toEqual({\n      phone: mockInitialData.phone,\n      email: mockInitialData.email,\n      skype: mockInitialData.skype,\n      telegram: mockInitialData.telegram,\n      linkedin: mockInitialData.linkedin,\n      githubUsername: mockInitialData.githubUsername,\n      website: mockInitialData.website,\n    });\n\n    expect(visibleCourses).toEqual(mockInitialData.visibleCourses);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/transformers/splitDataForForms.ts",
    "content": "import { Contacts, UserData, AllUserCVData } from '@client/modules/Opportunities/models';\n\nexport const splitDataForForms = (data: AllUserCVData) => {\n  const userData: Omit<UserData, 'uuid'> = {\n    selfIntroLink: data.selfIntroLink,\n    militaryService: data.militaryService,\n    avatarLink: data.avatarLink,\n    desiredPosition: data.desiredPosition,\n    englishLevel: data.englishLevel,\n    locations: data.locations,\n    name: data.name,\n    notes: data.notes,\n    startFrom: data.startFrom,\n    fullTime: data.fullTime,\n  };\n\n  const contacts: Contacts = {\n    phone: data.phone,\n    email: data.email,\n    skype: data.skype,\n    telegram: data.telegram,\n    linkedin: data.linkedin,\n    githubUsername: data.githubUsername,\n    website: data.website,\n  };\n\n  return {\n    userData,\n    contacts,\n    visibleCourses: data.visibleCourses,\n  };\n};\n"
  },
  {
    "path": "client/src/modules/Opportunities/transformers/transformFieldsData.test.ts",
    "content": "import { ResumeDtoEnglishLevelEnum, ResumeDtoMilitaryServiceEnum } from '@client/api';\nimport dayjs from 'dayjs';\nimport { transformFieldsData } from './transformFieldsData';\n\nconst mockFieldsData = {\n  selfIntroLink: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',\n  militaryService: ResumeDtoMilitaryServiceEnum.NotLiable,\n  avatarLink: 'https://example.com/avatar.png',\n  desiredPosition: 'Senior Cookie Manager',\n  englishLevel: ResumeDtoEnglishLevelEnum.A2,\n  locations: 'Minsk Belarus, Some Other, Some Another, Willbe Removed',\n  name: 'John Doe',\n  notes: 'Some notes',\n  startFrom: dayjs('2022-09-24'),\n  fullTime: true,\n  phone: '+1111111111111',\n  email: 'example@gmail.com',\n  skype: 'example',\n  telegram: 'example',\n  linkedin: 'example',\n  githubUsername: 'example',\n  website: 'https://example.com',\n  visibleCourses: [11, 21, 53],\n};\n\ndescribe('transformFieldsData', () => {\n  it('should correctly tranform missing data', () => {\n    const transformedData = transformFieldsData(mockFieldsData);\n\n    expect(transformedData).toStrictEqual({\n      selfIntroLink: mockFieldsData.selfIntroLink,\n      militaryService: mockFieldsData.militaryService,\n      avatarLink: mockFieldsData.avatarLink,\n      desiredPosition: mockFieldsData.desiredPosition,\n      englishLevel: mockFieldsData.englishLevel,\n      locations: 'Minsk Belarus,Some Other,Some Another',\n      name: mockFieldsData.name,\n      notes: mockFieldsData.notes,\n      startFrom: '2022-09-24',\n      fullTime: mockFieldsData.fullTime,\n      phone: mockFieldsData.phone,\n      email: mockFieldsData.email,\n      skype: mockFieldsData.skype,\n      telegram: mockFieldsData.telegram,\n      linkedin: mockFieldsData.linkedin,\n      githubUsername: mockFieldsData.githubUsername,\n      website: mockFieldsData.website,\n      visibleCourses: mockFieldsData.visibleCourses,\n    });\n  });\n\n  it('should transform fields data', () => {\n    const transformedData = transformFieldsData({\n      ...mockFieldsData,\n      selfIntroLink: '\\n',\n      avatarLink: null,\n      desiredPosition: '   ',\n      name: '',\n      notes: null,\n      phone: null,\n      email: '',\n      skype: '',\n      telegram: null,\n      linkedin: '',\n      locations: null,\n      githubUsername: '  ',\n      website: '',\n    });\n\n    expect(transformedData).toStrictEqual({\n      selfIntroLink: null,\n      militaryService: mockFieldsData.militaryService,\n      avatarLink: null,\n      desiredPosition: null,\n      englishLevel: mockFieldsData.englishLevel,\n      locations: null,\n      name: null,\n      notes: null,\n      startFrom: '2022-09-24',\n      fullTime: mockFieldsData.fullTime,\n      phone: null,\n      email: null,\n      skype: null,\n      telegram: null,\n      linkedin: null,\n      githubUsername: null,\n      website: null,\n      visibleCourses: mockFieldsData.visibleCourses,\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/transformers/transformFieldsData.ts",
    "content": "import dayjs from 'dayjs';\nimport { AllDataToSubmit } from '@client/modules/Opportunities/models';\n\nconst LOCATIONS_COUNT = 3;\n\nconst getTopLocations = (locationsRaw: string | null, length: number) =>\n  locationsRaw\n    ? locationsRaw\n        .split(',')\n        .slice(0, length)\n        .map(location => location.trim())\n        .join(',')\n    : null;\n\nconst nullifyConditional = (str?: string | null) => str?.trim() || null;\n\nexport const transformFieldsData = (data: AllDataToSubmit) => ({\n  selfIntroLink: nullifyConditional(data.selfIntroLink),\n  militaryService: data.militaryService,\n  avatarLink: nullifyConditional(data.avatarLink),\n  desiredPosition: nullifyConditional(data.desiredPosition),\n  englishLevel: data.englishLevel,\n  name: nullifyConditional(data.name),\n  notes: nullifyConditional(data.notes),\n  phone: nullifyConditional(data.phone),\n  email: nullifyConditional(data.email),\n  skype: nullifyConditional(data.skype),\n  telegram: nullifyConditional(data.telegram),\n  linkedin: nullifyConditional(data.linkedin),\n  locations: getTopLocations(data.locations, LOCATIONS_COUNT),\n  githubUsername: nullifyConditional(data.githubUsername),\n  website: nullifyConditional(data.website),\n  startFrom: data.startFrom && dayjs(data.startFrom).format('YYYY-MM-DD'),\n  fullTime: data.fullTime,\n  visibleCourses: data.visibleCourses,\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/transformers/transformInitialCvData.test.ts",
    "content": "import { ResumeDtoEnglishLevelEnum, ResumeDto, ResumeDtoMilitaryServiceEnum } from '@client/api';\nimport mapValues from 'lodash/mapValues';\nimport { transformInitialCvData } from './transformInitialCvData';\n\nconst mockInitialData = {\n  id: 12345,\n  uuid: '0882dc34-415d-4fa2-919d-37cab831db3a',\n  avatarLink: 'https://example.com/avatar.png',\n  desiredPosition: 'Senior Cookie Manager',\n  email: 'example@gmail.com',\n  englishLevel: ResumeDtoEnglishLevelEnum.A2,\n  expires: '1666626604224',\n  fullTime: true,\n  githubUsername: 'some-github-username',\n  linkedin: 'linkedin',\n  locations: 'Minsk Belarus',\n  militaryService: ResumeDtoMilitaryServiceEnum.NotLiable,\n  name: 'John Doe',\n  notes: 'Lalala some info about myself',\n  phone: '+1111111111111',\n  selfIntroLink: 'https://example.com/self-intro.mp4',\n  skype: 'skype',\n  startFrom: '2022-09-24',\n  telegram: 'telegram',\n  website: 'https://example.com',\n  visibleCourses: [11, 21, 53],\n  gratitudes: [0, 1, 2],\n  courses: [3, 4, 5],\n  feedbacks: [6, 7, 8],\n} as unknown as ResumeDto;\n\ndescribe('transformInitialCvData', () => {\n  it('should return correct data', () => {\n    const { userData, contacts, courses, visibleCourses } = transformInitialCvData(mockInitialData);\n\n    expect(userData).toEqual({\n      notes: mockInitialData.notes,\n      name: mockInitialData.name,\n      selfIntroLink: mockInitialData.selfIntroLink,\n      militaryService: mockInitialData.militaryService,\n      avatarLink: mockInitialData.avatarLink,\n      desiredPosition: mockInitialData.desiredPosition,\n      englishLevel: mockInitialData.englishLevel,\n      locations: mockInitialData.locations,\n      startFrom: mockInitialData.startFrom,\n      fullTime: mockInitialData.fullTime,\n    });\n\n    expect(contacts).toEqual({\n      email: mockInitialData.email,\n      githubUsername: mockInitialData.githubUsername,\n      linkedin: mockInitialData.linkedin,\n      phone: mockInitialData.phone,\n      skype: mockInitialData.skype,\n      telegram: mockInitialData.telegram,\n      website: mockInitialData.website,\n    });\n\n    expect(courses).toEqual(mockInitialData.courses);\n    expect(visibleCourses).toEqual(mockInitialData.visibleCourses);\n  });\n\n  it('should correctly handle absence of data', () => {\n    const { userData, contacts, courses, visibleCourses } = transformInitialCvData(null);\n\n    expect(userData).toEqual({\n      notes: null,\n      name: null,\n      selfIntroLink: null,\n      militaryService: null,\n      avatarLink: null,\n      desiredPosition: null,\n      englishLevel: null,\n      locations: null,\n      startFrom: null,\n      fullTime: false,\n    });\n\n    expect(contacts).toEqual({\n      email: null,\n      githubUsername: null,\n      linkedin: null,\n      phone: null,\n      skype: null,\n      telegram: null,\n      website: null,\n    });\n\n    expect(courses).toEqual([]);\n    expect(visibleCourses).toEqual([]);\n  });\n\n  it('should correctly handle mising values', () => {\n    const { userData, contacts, courses, visibleCourses } = transformInitialCvData(\n      mapValues(mockInitialData, () => undefined) as unknown as ResumeDto,\n    );\n\n    expect(userData).toEqual({\n      notes: null,\n      name: null,\n      selfIntroLink: null,\n      militaryService: null,\n      avatarLink: null,\n      desiredPosition: null,\n      englishLevel: null,\n      locations: null,\n      startFrom: null,\n      fullTime: false,\n    });\n\n    expect(contacts).toEqual({\n      email: null,\n      githubUsername: null,\n      linkedin: null,\n      phone: null,\n      skype: null,\n      telegram: null,\n      website: null,\n    });\n\n    expect(courses).toEqual([]);\n    expect(visibleCourses).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Opportunities/transformers/transformInitialCvData.ts",
    "content": "import { ResumeDto } from '@client/api';\n\nexport const transformInitialCvData = (data: ResumeDto | null) => {\n  const {\n    notes,\n    name,\n    selfIntroLink,\n    militaryService,\n    avatarLink,\n    desiredPosition,\n    englishLevel,\n    email,\n    githubUsername,\n    linkedin,\n    locations,\n    phone,\n    skype,\n    telegram,\n    website,\n    startFrom,\n    fullTime,\n    courses = [],\n    visibleCourses = [],\n  } = data ?? {};\n\n  const userData = {\n    notes: notes ?? null,\n    name: name ?? null,\n    selfIntroLink: selfIntroLink ?? null,\n    militaryService: militaryService ?? null,\n    avatarLink: avatarLink ?? null,\n    desiredPosition: desiredPosition ?? null,\n    englishLevel: englishLevel ?? null,\n    locations: locations ?? null,\n    startFrom: startFrom ?? null,\n    fullTime: !!fullTime,\n  };\n\n  const contacts = {\n    email: email ?? null,\n    githubUsername: githubUsername ?? null,\n    linkedin: linkedin ?? null,\n    phone: phone ?? null,\n    skype: skype ?? null,\n    telegram: telegram ?? null,\n    website: website ?? null,\n  };\n\n  return {\n    contacts,\n    userData,\n    visibleCourses,\n    courses,\n  };\n};\n"
  },
  {
    "path": "client/src/modules/Profile/components/MentorEndorsement/MentorEndorsement.tsx",
    "content": "import { Alert, Input, Modal, Spin, Typography } from 'antd';\nimport { useMemo } from 'react';\nimport { ProfileApi } from '@client/api';\nimport { useAsync } from 'react-use';\nimport isNull from 'lodash/isNull';\nimport isObject from 'lodash/isObject';\nimport omitBy from 'lodash/omitBy';\n\nexport interface Props {\n  open: boolean;\n  githubId: string;\n  onClose: () => void;\n}\n\nconst api = new ProfileApi();\n\nexport function MentorEndorsement(props: Props) {\n  const { value, loading, error } = useAsync(async () => {\n    if (props.open) {\n      const { data } = await api.getEndorsement(props.githubId);\n      return data;\n    }\n  }, [props.githubId, props.open]);\n\n  const data = useMemo(() => (value?.data ? cleanData(value.data) : null), [value?.data]);\n\n  return (\n    <Modal\n      width={640}\n      onCancel={props.onClose}\n      cancelButtonProps={{ hidden: true }}\n      onOk={props.onClose}\n      open={props.open}\n    >\n      <Spin spinning={loading}>\n        <div style={{ minHeight: 320, paddingTop: 32 }}>\n          {error ? (\n            <Alert\n              closable={false}\n              message=\"Error occurred while generating endorsment\"\n              description={error.message}\n              type=\"error\"\n            />\n          ) : null}\n\n          {value ? (\n            <>\n              <Typography.Title level={4}>Generated Text</Typography.Title>\n              <Typography.Paragraph style={{ fontSize: 13 }} copyable={{ text: value?.summary }}>\n                {value?.summary.split('\\n').map(i => (\n                  <>\n                    {i}\n                    <br />\n                  </>\n                ))}\n              </Typography.Paragraph>\n            </>\n          ) : null}\n        </div>\n\n        {data ? (\n          <div>\n            <Typography.Title level={4}>Data Model</Typography.Title>\n            <Input.TextArea rows={12} readOnly value={JSON.stringify(data, null, 4)} />\n          </div>\n        ) : null}\n      </Spin>\n    </Modal>\n  );\n}\n\nfunction removeNull(obj: object) {\n  return omitBy(obj, isNull);\n}\n\nfunction cleanData(obj: object) {\n  const cleanedObj = removeNull(obj);\n\n  // Recursively clean nested objects\n  for (const key in cleanedObj) {\n    if (isObject(cleanedObj[key])) {\n      cleanedObj[key] = cleanData(cleanedObj[key]);\n    }\n  }\n\n  return cleanedObj;\n}\n"
  },
  {
    "path": "client/src/modules/Profile/components/MentorEndorsement/index.tsx",
    "content": "export { MentorEndorsement } from './MentorEndorsement';\n"
  },
  {
    "path": "client/src/modules/Prompts/components/PromptModal.tsx",
    "content": "import { Form, Input, InputNumber, message, Modal } from 'antd';\nimport { useEffect } from 'react';\nimport { PromptDto, PromptsApi } from '@client/api';\n\ntype Props = {\n  open: boolean;\n  onCancel: () => void;\n  loadData: () => Promise<void>;\n  data?: PromptDto;\n};\nconst disciplineService = new PromptsApi();\n\nexport function PromptModal({ open, onCancel, loadData, data }: Props) {\n  const [form] = Form.useForm();\n\n  useEffect(() => form.resetFields, [open]);\n\n  const initialValues = data ?? { temperature: 0.5 };\n\n  const submitForm = async () => {\n    try {\n      const value = await form.validateFields();\n\n      if (data) {\n        await disciplineService.updatePrompt(data.id, value);\n      } else {\n        await disciplineService.createPrompt(value);\n      }\n\n      await loadData();\n      onCancel();\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    }\n  };\n\n  return (\n    <Modal width={600} title={data ? 'Edit prompt' : 'Add prompt'} open={open} onCancel={onCancel} onOk={submitForm}>\n      <Form layout=\"vertical\" form={form} initialValues={initialValues}>\n        <Form.Item key=\"type\" name=\"type\" label=\"Type\" rules={[{ required: true }]}>\n          <Input />\n        </Form.Item>\n        <Form.Item key=\"temperature\" name=\"temperature\" label=\"Temperature\" rules={[{ required: true }]}>\n          <InputNumber step={0.1} min={0} max={1} />\n        </Form.Item>\n        <Form.Item key=\"text\" name=\"text\" label=\"Text\" rules={[{ required: true }]}>\n          <Input.TextArea rows={16} />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Prompts/components/PromptTable.tsx",
    "content": "import { DeleteOutlined, EditOutlined } from '@ant-design/icons';\nimport { Button, Space, Table } from 'antd';\nimport { PromptDto } from '@client/api';\n\nconst { Column } = Table;\n\ntype Props = {\n  data: PromptDto[];\n  handleUpdate: (record: PromptDto) => void;\n  handleDelete: (record: PromptDto) => Promise<void>;\n};\n\nexport const PromptTable = ({ data, handleDelete, handleUpdate }: Props) => {\n  return (\n    <Table dataSource={data} rowKey={'name'}>\n      <Column title=\"Type\" dataIndex=\"type\" key=\"type\" />\n      <Column\n        title=\"Actions\"\n        key=\"action\"\n        width={100}\n        render={record => (\n          <Space size=\"middle\">\n            <Button key={'edit'} onClick={() => handleUpdate(record)} size=\"small\">\n              <EditOutlined size={8} />\n            </Button>\n            <Button key={'delete'} onClick={() => handleDelete(record)} size=\"small\" danger>\n              <DeleteOutlined size={8} />\n            </Button>\n          </Space>\n        )}\n      />\n    </Table>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Prompts/pages/PromptPage.tsx",
    "content": "import { Button, Layout, message } from 'antd';\nimport { PromptDto, PromptsApi } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { useModalForm } from '@client/hooks';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useCallback, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { PromptModal } from '../components/PromptModal';\nimport { PromptTable } from '../components/PromptTable';\n\nconst api = new PromptsApi();\n\nexport const PromptsPage = () => {\n  const { courses } = useActiveCourseContext();\n\n  const [prompts, setPrompts] = useState([] as PromptDto[]);\n  const { open, formData, toggle } = useModalForm<PromptDto>();\n\n  const [loading, setLoading] = useState(false);\n\n  const loadDisciplines = useCallback(async () => {\n    try {\n      setLoading(true);\n      const { data } = await api.getPrompts();\n      setPrompts(data);\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    } finally {\n      setLoading(false);\n    }\n  }, []);\n\n  const handleDelete = async (record: PromptDto) => {\n    await api.deletePrompt(record.id);\n    await loadDisciplines();\n  };\n\n  const handleModalShowUpdate = (record: PromptDto) => {\n    toggle(record);\n  };\n\n  useAsync(loadDisciplines, []);\n\n  return (\n    <AdminPageLayout title=\"Manage Prompts\" loading={loading} courses={courses}>\n      <Layout.Content style={{ margin: 8 }}>\n        <Button type=\"primary\" onClick={() => toggle()} style={{ marginBottom: '25px' }}>\n          Add Prompt\n        </Button>\n        <PromptTable data={prompts || []} handleUpdate={handleModalShowUpdate} handleDelete={handleDelete} />\n      </Layout.Content>\n\n      <PromptModal open={open} onCancel={() => toggle()} loadData={loadDisciplines} data={formData} />\n    </AdminPageLayout>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/AdditionalInfo/AdditionalInfo.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { Form } from 'antd';\nimport { UpdateUserDtoLanguagesEnum } from '@client/api';\nimport { LABELS } from '@client/modules/Registry/constants';\nimport { Course } from '@client/services/models';\nimport { AdditionalInfo } from './AdditionalInfo';\n\nconst courses = [\n  {\n    id: 1,\n    name: 'test',\n    startDate: new Date().toUTCString(),\n    planned: true,\n  },\n] as Course[];\n\nconst mockValues = {\n  preferedCourses: [1],\n  languagesMentoring: [UpdateUserDtoLanguagesEnum.En],\n  dataProcessing: 1,\n  aboutMyself: \"I'm Groot\",\n};\n\ntype Values = typeof mockValues;\n\nconst previousHandler = vi.fn();\nconst submitHandler = vi.fn();\nconst submitFailedHandler = vi.fn();\n\nconst renderAdditionalInfo = (values: Values) =>\n  render(\n    <Form initialValues={values} onFinish={submitHandler} onFinishFailed={submitFailedHandler}>\n      <AdditionalInfo courses={courses} onPrevious={previousHandler} />\n    </Form>,\n  );\n\ndescribe('AdditionalInfo', () => {\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  const user = userEvent.setup();\n\n  test.each`\n    label\n    ${LABELS.courses}\n    ${LABELS.aboutYourself}\n  `('should render field with $label label', async ({ label }) => {\n    renderAdditionalInfo(mockValues);\n\n    const field = await screen.findByText(label);\n    expect(field).toBeInTheDocument();\n  });\n\n  test('should render data processing checkbox', async () => {\n    renderAdditionalInfo(mockValues);\n\n    const checkbox = await screen.findByRole('checkbox');\n    expect(checkbox).toBeInTheDocument();\n  });\n\n  test('should render Previous button', async () => {\n    renderAdditionalInfo(mockValues);\n\n    const button = await screen.findByRole('button', { name: /previous/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  test('should render Submit button', async () => {\n    renderAdditionalInfo(mockValues);\n\n    const button = await screen.findByRole('button', { name: /submit/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  test('should call only submitHandler', async () => {\n    renderAdditionalInfo(mockValues);\n\n    const button = await screen.findByRole('button', { name: /submit/i });\n\n    await user.click(button);\n\n    expect(submitHandler).toHaveBeenCalled();\n    expect(submitFailedHandler).not.toHaveBeenCalled();\n  });\n\n  test('should call only submitFailedHandler', async () => {\n    renderAdditionalInfo({ ...mockValues, dataProcessing: 0 });\n\n    const button = await screen.findByRole('button', { name: /submit/i });\n\n    await user.click(button);\n\n    expect(submitHandler).not.toHaveBeenCalled();\n    expect(submitFailedHandler).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/AdditionalInfo/AdditionalInfo.tsx",
    "content": "import { Form, Select, Input, Typography } from 'antd';\nimport { CARD_TITLES, LABELS, PLACEHOLDERS, WIDE_FORM_ITEM_LAYOUT } from '@client/modules/Registry/constants';\nimport { Course } from '@client/services/models';\nimport {\n  CourseLabel,\n  DataProcessingCheckbox,\n  FormButtons,\n  FormCard,\n  LanguagesMentoring,\n} from '@client/modules/Registry/components';\n\ntype Props = {\n  courses: Course[];\n  onPrevious: () => void;\n};\n\nconst { Title } = Typography;\n\nconst formItemLayout = WIDE_FORM_ITEM_LAYOUT();\n\nexport function AdditionalInfo({ courses, onPrevious }: Props) {\n  return (\n    <FormCard\n      title={\n        <Title level={5} style={{ marginBottom: 0 }}>\n          {CARD_TITLES.additionalInfo}\n        </Title>\n      }\n    >\n      <Form.Item {...formItemLayout} name=\"preferedCourses\" label={LABELS.courses}>\n        <Select\n          mode=\"multiple\"\n          placeholder={PLACEHOLDERS.courses}\n          options={courses.map(course => ({\n            label: <CourseLabel course={course} />,\n            value: course.id,\n          }))}\n        />\n      </Form.Item>\n      <LanguagesMentoring />\n      <Form.Item {...formItemLayout} name=\"aboutMyself\" label={LABELS.aboutYourself}>\n        <Input.TextArea rows={6} placeholder={PLACEHOLDERS.aboutYourself} />\n      </Form.Item>\n      <DataProcessingCheckbox />\n      <FormButtons onPrevious={onPrevious} />\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/ContactInfo/ContactInfo.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { ERROR_MESSAGES, LABELS, PLACEHOLDERS, RSSCHOOL_BOT_LINK } from '@client/modules/Registry/constants';\nimport { ContactInfo } from './ContactInfo';\n\nconst mockValues = {\n  contactsTelegram: 'telegram',\n  contactsSkype: 'skype',\n  contactsWhatsApp: 'whatsapp',\n  contactsEmail: 'test@test.com',\n  contactsPhone: '+123456789',\n  contactsNotes: 'notes',\n};\n\ntype Values = typeof mockValues | Record<string, unknown>;\n\nconst renderContactInfo = (values: Values = mockValues) =>\n  render(\n    <Form role=\"form\" initialValues={values}>\n      <ContactInfo />\n    </Form>,\n  );\n\ndescribe('ContactInfo', () => {\n  test.each(Object.values(mockValues).map(value => ({ value })))(\n    'should render form item with $value value',\n    async ({ value }) => {\n      renderContactInfo();\n\n      const item = await screen.findByDisplayValue(value);\n      expect(item).toBeInTheDocument();\n    },\n  );\n\n  test('should render Continue button', async () => {\n    renderContactInfo();\n\n    const button = await screen.findByRole('button', { name: /continue/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  test('should render Telegram bot link', async () => {\n    renderContactInfo();\n\n    const link = await screen.findByRole('link');\n    expect(link).toBeInTheDocument();\n    expect(link).toHaveAttribute('href', RSSCHOOL_BOT_LINK);\n  });\n\n  test.each`\n    label\n    ${LABELS.telegram}\n    ${LABELS.skype}\n    ${LABELS.whatsApp}\n    ${LABELS.email}\n    ${LABELS.phone}\n    ${LABELS.notes}\n  `('should render field with $label label', async ({ label }) => {\n    renderContactInfo();\n\n    const fieldLabel = await screen.findByLabelText(label);\n    expect(fieldLabel).toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder\n    ${PLACEHOLDERS.telegram}\n    ${PLACEHOLDERS.skype}\n    ${PLACEHOLDERS.whatsApp}\n    ${PLACEHOLDERS.email}\n    ${PLACEHOLDERS.phone}\n    ${PLACEHOLDERS.notes}\n  `('should render field with $placeholder placeholder', async ({ placeholder }) => {\n    renderContactInfo();\n\n    const fieldPlaceholder = await screen.findByPlaceholderText(placeholder);\n    expect(fieldPlaceholder).toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder           | message\n    ${PLACEHOLDERS.email} | ${ERROR_MESSAGES.email}\n    ${PLACEHOLDERS.phone} | ${ERROR_MESSAGES.phone}\n  `('should not render $message error message on valid input', async ({ placeholder, message }) => {\n    renderContactInfo();\n\n    const input = await screen.findByPlaceholderText(placeholder);\n    const errorMessage = screen.queryByText(message);\n    expect(input).toBeInTheDocument();\n    expect(errorMessage).not.toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder           | value          | message\n    ${PLACEHOLDERS.email} | ${'test'}      | ${ERROR_MESSAGES.email}\n    ${PLACEHOLDERS.phone} | ${String(123)} | ${ERROR_MESSAGES.phone}\n  `('should render $message error message on invalid input', async ({ placeholder, value, message }) => {\n    renderContactInfo();\n\n    const input = await screen.findByPlaceholderText(placeholder);\n\n    fireEvent.change(input, {\n      target: {\n        value,\n      },\n    });\n\n    expect(input).toHaveValue(value);\n\n    const errorMessage = await screen.findByText(message);\n    expect(errorMessage).toBeInTheDocument();\n  });\n\n  test('should render error messages only on required fields', async () => {\n    renderContactInfo({});\n\n    const form = screen.getByRole('form');\n    fireEvent.submit(form);\n\n    const errorEmail = await screen.findByText(ERROR_MESSAGES.email);\n\n    expect(errorEmail).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/ContactInfo/ContactInfo.tsx",
    "content": "import { Alert, Form, Input, Space, Typography } from 'antd';\nimport {\n  CARD_TITLES,\n  ERROR_MESSAGES,\n  LABELS,\n  PLACEHOLDERS,\n  RSSCHOOL_BOT_LINK,\n} from '@client/modules/Registry/constants';\nimport { emailPattern, phonePattern } from '@client/services/validators';\nimport { FormButtons, FormCard } from '@client/modules/Registry/components';\n\nconst { Title, Text } = Typography;\n\nconst CardTitle = (\n  <Space direction=\"vertical\" size={0}>\n    <Title level={5}>{CARD_TITLES.contactInfo}</Title>\n    <Text type=\"secondary\" style={{ whiteSpace: 'normal' }}>\n      Information will be shown to students so they can contact you. Indicate only the data that you're willing to share\n    </Text>\n  </Space>\n);\n\nexport function ContactInfo() {\n  return (\n    <FormCard title={CardTitle}>\n      <Form.Item\n        label={LABELS.telegram}\n        name=\"contactsTelegram\"\n        extra={\n          <Alert\n            style={{ marginTop: 12 }}\n            type=\"info\"\n            message={\n              <span>\n                Subscribe to our{' '}\n                <a href={RSSCHOOL_BOT_LINK} target=\"_blank\">\n                  Telegram-bot\n                </a>{' '}\n                to keep in touch with us.\n              </span>\n            }\n          />\n        }\n      >\n        <Input placeholder={PLACEHOLDERS.telegram} />\n      </Form.Item>\n      <Form.Item label={LABELS.skype} name=\"contactsSkype\">\n        <Input placeholder={PLACEHOLDERS.skype} />\n      </Form.Item>\n      <Form.Item label={LABELS.whatsApp} name=\"contactsWhatsApp\">\n        <Input placeholder={PLACEHOLDERS.whatsApp} />\n      </Form.Item>\n      <Form.Item\n        label={LABELS.email}\n        name=\"contactsEmail\"\n        rules={[{ required: true, pattern: emailPattern, message: ERROR_MESSAGES.email }]}\n      >\n        <Input placeholder={PLACEHOLDERS.email} />\n      </Form.Item>\n      <Form.Item\n        label={LABELS.phone}\n        name=\"contactsPhone\"\n        rules={[{ pattern: phonePattern, message: ERROR_MESSAGES.phone }]}\n      >\n        <Input placeholder={PLACEHOLDERS.phone} />\n      </Form.Item>\n      <Form.Item label={LABELS.notes} name=\"contactsNotes\">\n        <Input.TextArea rows={4} placeholder={PLACEHOLDERS.notes} />\n      </Form.Item>\n      <FormButtons submitTitle=\"Continue\" />\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/CourseDetails/CourseDetails.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { CourseDto } from '@client/api';\nimport { LABELS, PLACEHOLDERS } from '@client/modules/Registry/constants';\nimport { CourseDetails } from './CourseDetails';\n\nconst renderCourseDetails = (courses: CourseDto[] = []) =>\n  render(\n    <Form initialValues={courses.length ? { courseId: courses[0]?.id } : undefined}>\n      <CourseDetails courses={courses} />\n    </Form>,\n  );\n\ndescribe('CourseDetail', () => {\n  test.each`\n    label\n    ${LABELS.course}\n    ${LABELS.languagesStudent}\n  `('should render field with $label label', async ({ label }) => {\n    renderCourseDetails();\n\n    const fieldLabel = await screen.findByLabelText(label);\n    expect(fieldLabel).toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder\n    ${PLACEHOLDERS.courses}\n    ${PLACEHOLDERS.languages}\n  `('should render field with $placeholder placeholder', async ({ placeholder }) => {\n    renderCourseDetails();\n\n    const fieldPlaceholder = await screen.findByText(placeholder);\n    expect(fieldPlaceholder).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/CourseDetails/CourseDetails.tsx",
    "content": "import { Form, Select, Typography } from 'antd';\nimport { CourseDto } from '@client/api';\nimport { CourseLabel, FormCard, LanguagesMentoring } from '@client/modules/Registry/components';\nimport { CARD_TITLES, LABELS, PLACEHOLDERS } from '@client/modules/Registry/constants';\n\nconst { Title } = Typography;\n\ntype Props = {\n  courses: CourseDto[];\n};\n\nexport function CourseDetails({ courses }: Props) {\n  return (\n    <FormCard\n      title={\n        <Title level={5} style={{ marginBottom: 0 }}>\n          {CARD_TITLES.courseDetails}\n        </Title>\n      }\n    >\n      <Form.Item label={LABELS.course} name=\"courseId\">\n        <Select\n          placeholder={PLACEHOLDERS.courses}\n          options={courses.map(course => ({\n            label: <CourseLabel course={course} isStudentForm />,\n            value: course.id,\n          }))}\n        />\n      </Form.Item>\n      <LanguagesMentoring isStudentForm />\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/Disciplines/Disciplines.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { DisciplineDto } from '@client/api';\nimport { LABELS } from '@client/modules/Registry/constants';\nimport { Disciplines } from './Disciplines';\n\nconst mockDisciplines = [\n  {\n    name: 'JS',\n  },\n  {\n    name: 'TS',\n  },\n] as DisciplineDto[];\n\ndescribe('Disciplines', () => {\n  test.each(mockDisciplines)('should render form item with $name value', async ({ name }) => {\n    render(\n      <Form>\n        <Disciplines disciplines={mockDisciplines} />\n      </Form>,\n    );\n\n    const item = await screen.findByDisplayValue(name);\n    expect(item).toBeInTheDocument();\n  });\n\n  test(`should render field with \"${LABELS.disciplines}\" label`, async () => {\n    render(\n      <Form>\n        <Disciplines disciplines={mockDisciplines} />\n      </Form>,\n    );\n\n    const fieldLabel = await screen.findByTitle(LABELS.disciplines);\n    expect(fieldLabel).toBeInTheDocument();\n  });\n\n  test(\"should render <Empty /> when there's no disciplines\", async () => {\n    render(\n      <Form>\n        <Disciplines disciplines={[]} />\n      </Form>,\n    );\n\n    const noData = await screen.findByText('No data', { selector: ':not(title)' });\n    expect(noData).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/Disciplines/Disciplines.tsx",
    "content": "import { Form, Checkbox, Row, Empty, Typography, Col } from 'antd';\nimport { DisciplineDto } from '@client/api';\nimport { CARD_TITLES, LABELS, VALIDATION_RULES, WIDE_FORM_ITEM_LAYOUT } from '@client/modules/Registry/constants';\nimport { FormCard } from '@client/modules/Registry/components';\n\ntype Props = {\n  disciplines: DisciplineDto[];\n};\n\nconst { Title } = Typography;\n\nconst formItemLayout = WIDE_FORM_ITEM_LAYOUT();\n\nexport function Disciplines({ disciplines }: Props) {\n  return (\n    <FormCard\n      title={\n        <Title level={5} style={{ marginBottom: 0 }}>\n          {CARD_TITLES.disciplines}\n        </Title>\n      }\n    >\n      {disciplines.length ? (\n        <Form.Item\n          {...formItemLayout}\n          name=\"technicalMentoring\"\n          label={LABELS.disciplines}\n          rules={VALIDATION_RULES}\n          required\n        >\n          <Checkbox.Group\n            style={{\n              paddingTop: '5px',\n            }}\n          >\n            <Row justify=\"space-between\" gutter={[0, 8]}>\n              {disciplines.map(({ name }) => (\n                <Col span={12} key={name}>\n                  <Checkbox value={name}>{name}</Checkbox>\n                </Col>\n              ))}\n            </Row>\n          </Checkbox.Group>\n        </Form.Item>\n      ) : (\n        <Empty />\n      )}\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/PersonalInfo/PersonalInfo.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { ERROR_MESSAGES, LABELS, PLACEHOLDERS } from '@client/modules/Registry/constants';\nimport { PersonalInfo } from './PersonalInfo';\nimport usePlacesAutocomplete from 'use-places-autocomplete';\n\nvi.mock('use-places-autocomplete');\n\nvi.mocked(usePlacesAutocomplete).mockImplementation(() => ({\n  value: null,\n  suggestions: {\n    data: {\n      map: vi.fn(),\n    },\n    loading: false,\n  },\n  setValue: vi.fn(),\n}));\n\nconst mockValues = {\n  firstName: 'John',\n  lastName: 'Doe',\n  location: null,\n  primaryEmail: 'test@test.com',\n  contactsEpamEmail: 'john_doe@epam.com',\n};\n\ntype Values = typeof mockValues | Record<string, unknown>;\n\nconst renderPersonalInfo = (values: Values = mockValues, isStudentForm?: boolean) =>\n  render(\n    <Form role=\"form\" initialValues={values}>\n      <PersonalInfo location={null} setLocation={vi.fn()} isStudentForm={isStudentForm} />\n    </Form>,\n  );\n\ndescribe('PersonalInfo', () => {\n  test.each(\n    Object.values(mockValues)\n      .filter(Boolean)\n      .map(value => ({ value })),\n  )('should render form item with $value value', async ({ value }) => {\n    renderPersonalInfo();\n\n    const item = await screen.findByDisplayValue(value as string);\n    expect(item).toBeInTheDocument();\n  });\n\n  test.each`\n    label\n    ${LABELS.firstName}\n    ${LABELS.lastName}\n    ${LABELS.primaryEmail}\n    ${LABELS.epamEmail}\n  `('should render field with $label label', async ({ label }) => {\n    renderPersonalInfo();\n\n    const fieldLabel = await screen.findByLabelText(label);\n    expect(fieldLabel).toBeInTheDocument();\n  });\n\n  test('should render field with location label', async () => {\n    renderPersonalInfo();\n    const fieldLabel = await screen.findByText(LABELS.location);\n    expect(fieldLabel).toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder\n    ${PLACEHOLDERS.firstName}\n    ${PLACEHOLDERS.lastName}\n    ${PLACEHOLDERS.email}\n    ${PLACEHOLDERS.epamEmail}\n  `('should render field with $placeholder placeholder', async ({ placeholder }) => {\n    renderPersonalInfo();\n\n    const fieldPlaceholder = await screen.findByPlaceholderText(placeholder);\n    expect(fieldPlaceholder).toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder               | message\n    ${PLACEHOLDERS.email}     | ${ERROR_MESSAGES.email}\n    ${PLACEHOLDERS.epamEmail} | ${ERROR_MESSAGES.epamEmail}\n    ${PLACEHOLDERS.firstName} | ${ERROR_MESSAGES.inEnglish('First name')}\n    ${PLACEHOLDERS.lastName}  | ${ERROR_MESSAGES.inEnglish('Last name')}\n  `('should not render $message error message on valid input', async ({ placeholder, message }) => {\n    renderPersonalInfo();\n\n    const input = await screen.findByPlaceholderText(placeholder);\n    const errorMessage = screen.queryByText(message);\n    expect(input).toBeInTheDocument();\n    expect(errorMessage).not.toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder               | value              | message\n    ${PLACEHOLDERS.email}     | ${'test'}          | ${ERROR_MESSAGES.email}\n    ${PLACEHOLDERS.epamEmail} | ${'test@epam.com'} | ${ERROR_MESSAGES.epamEmail}\n    ${PLACEHOLDERS.firstName} | ${'Róża'}          | ${ERROR_MESSAGES.inEnglish('First name')}\n    ${PLACEHOLDERS.lastName}  | ${'Wójcik'}        | ${ERROR_MESSAGES.inEnglish('Last name')}\n  `('should render $message error message on invalid input', async ({ placeholder, value, message }) => {\n    renderPersonalInfo();\n\n    const input = await screen.findByPlaceholderText(placeholder);\n\n    fireEvent.change(input, {\n      target: {\n        value,\n      },\n    });\n\n    expect(input).toHaveValue(value);\n\n    const errorMessage = await screen.findByText(message);\n    expect(errorMessage).toBeInTheDocument();\n  });\n\n  test('should render error messages only on required fields', async () => {\n    renderPersonalInfo({});\n\n    const form = screen.getByRole('form');\n    fireEvent.submit(form);\n\n    const errorFirstName = await screen.findByText(ERROR_MESSAGES.inEnglish('First name'));\n    const errorLocation = await screen.findByText(ERROR_MESSAGES.location);\n    const errorEmail = await screen.findByText(ERROR_MESSAGES.email);\n    const errorLastName = screen.queryByText(ERROR_MESSAGES.inEnglish('Last name'));\n    const errorEpamEmail = screen.queryByText(ERROR_MESSAGES.epamEmail);\n\n    expect(errorFirstName).toBeInTheDocument();\n    expect(errorLocation).toBeInTheDocument();\n    expect(errorEmail).toBeInTheDocument();\n    expect(errorLastName).toBeInTheDocument();\n    expect(errorEpamEmail).not.toBeInTheDocument();\n  });\n\n  test('should render data processing checkbox on student form', async () => {\n    renderPersonalInfo(mockValues, true);\n\n    const checkbox = await screen.findByRole('checkbox');\n    expect(checkbox).toBeInTheDocument();\n  });\n\n  test('should render Submit button on student form', async () => {\n    renderPersonalInfo(mockValues, true);\n\n    const button = await screen.findByRole('button', { name: /submit/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  test('should not render data processing checkbox on mentor form', () => {\n    renderPersonalInfo();\n\n    const checkbox = screen.queryByRole('checkbox');\n    expect(checkbox).not.toBeInTheDocument();\n  });\n\n  test('should not render Submit button on mentor form', () => {\n    renderPersonalInfo();\n\n    const button = screen.queryByRole('button', { name: /submit/i });\n    expect(button).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/PersonalInfo/PersonalInfo.tsx",
    "content": "import { Form, Input, Typography } from 'antd';\nimport { Dispatch, SetStateAction } from 'react';\nimport { Location } from '@common/models';\nimport { DataProcessingCheckbox, FormButtons, FormCard } from '@client/modules/Registry/components';\nimport { emailPattern, englishNamePattern, epamEmailPattern } from '@client/services/validators';\nimport {\n  CARD_TITLES,\n  ERROR_MESSAGES,\n  EXTRAS,\n  LABELS,\n  PLACEHOLDERS,\n  TOOLTIPS,\n} from '@client/modules/Registry/constants';\nimport { LocationSelect } from '@client/shared/components/Forms';\n\ntype Props = {\n  location: Location | null;\n  setLocation: Dispatch<SetStateAction<Location | null>>;\n  isStudentForm?: boolean;\n};\n\nconst { Title } = Typography;\n\nexport function PersonalInfo({ location, setLocation, isStudentForm }: Props) {\n  return (\n    <FormCard\n      title={\n        <Title level={5} style={{ marginBottom: 0 }}>\n          {CARD_TITLES.personalInfo}\n        </Title>\n      }\n    >\n      <Form.Item\n        label={LABELS.firstName}\n        name=\"firstName\"\n        rules={[{ required: true, pattern: englishNamePattern, message: ERROR_MESSAGES.inEnglish('First name') }]}\n        extra={EXTRAS.inEnglish}\n      >\n        <Input placeholder={PLACEHOLDERS.firstName} />\n      </Form.Item>\n      <Form.Item\n        label={LABELS.lastName}\n        name=\"lastName\"\n        rules={[{ required: true, pattern: englishNamePattern, message: ERROR_MESSAGES.inEnglish('Last name') }]}\n        extra={EXTRAS.inEnglish}\n      >\n        <Input placeholder={PLACEHOLDERS.lastName} />\n      </Form.Item>\n      <Form.Item\n        label={LABELS.location}\n        tooltip={isStudentForm ? TOOLTIPS.locationStudent : TOOLTIPS.locationMentor}\n        name=\"location\"\n        rules={[{ required: true, message: ERROR_MESSAGES.location }]}\n        valuePropName={'location'}\n      >\n        <LocationSelect onChange={setLocation} location={location} />\n      </Form.Item>\n      <Form.Item\n        label={LABELS.primaryEmail}\n        tooltip={TOOLTIPS.primaryEmail}\n        name=\"primaryEmail\"\n        rules={[{ required: true, pattern: emailPattern, message: ERROR_MESSAGES.email }]}\n      >\n        <Input placeholder={PLACEHOLDERS.email} />\n      </Form.Item>\n      <Form.Item\n        label={LABELS.epamEmail}\n        tooltip={TOOLTIPS.epamEmail}\n        name=\"contactsEpamEmail\"\n        rules={[{ pattern: epamEmailPattern, message: ERROR_MESSAGES.epamEmail }]}\n      >\n        <Input placeholder={PLACEHOLDERS.epamEmail} />\n      </Form.Item>\n      {isStudentForm ? (\n        <>\n          <DataProcessingCheckbox isStudentForm />\n          <FormButtons />\n        </>\n      ) : null}\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/Preferences/Preferences.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { LABELS } from '@client/modules/Registry/constants';\nimport { Preferences } from './Preferences';\n\nconst renderPreferences = () =>\n  render(\n    <Form>\n      <Preferences />\n    </Form>,\n  );\n\ndescribe('Preferences', () => {\n  test.each`\n    value\n    ${2}\n    ${'any'}\n  `('should render form item with $value value', async ({ value }) => {\n    renderPreferences();\n\n    const item = await screen.findByDisplayValue(value);\n    expect(item).toBeInTheDocument();\n  });\n\n  test.each`\n    label\n    ${LABELS.studentsCount}\n    ${LABELS.studentsLocation}\n  `('should render field with $label label', async ({ label }) => {\n    renderPreferences();\n\n    const fieldLabel = await screen.findByTitle(label);\n    expect(fieldLabel).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/Cards/Preferences/Preferences.tsx",
    "content": "import { Typography, Form, Radio } from 'antd';\nimport { CARD_TITLES, EXTRAS, LABELS, WIDE_FORM_ITEM_LAYOUT } from '@client/modules/Registry/constants';\nimport { FormCard } from '@client/modules/Registry/components';\nimport { MentorOptionsDtoPreferedStudentsLocationEnum } from '@client/api';\n\nconst { Title } = Typography;\n\nconst studentsLimits = [2, 3, 4, 5, 6];\nconst locations = [\n  {\n    value: MentorOptionsDtoPreferedStudentsLocationEnum.Any,\n    label: 'Anywhere',\n  },\n  {\n    value: MentorOptionsDtoPreferedStudentsLocationEnum.Country,\n    label: 'My country only',\n  },\n  {\n    value: MentorOptionsDtoPreferedStudentsLocationEnum.City,\n    label: 'My city only',\n  },\n];\n\nconst formItemLayout = WIDE_FORM_ITEM_LAYOUT();\n\nexport function Preferences() {\n  return (\n    <FormCard\n      title={\n        <Title level={5} style={{ marginBottom: 0 }}>\n          {CARD_TITLES.preferences}\n        </Title>\n      }\n    >\n      <Form.Item {...formItemLayout} name=\"maxStudentsLimit\" label={LABELS.studentsCount} extra={EXTRAS.readyToMentor}>\n        <Radio.Group>\n          {studentsLimits.map(elem => (\n            <Radio key={elem} value={elem}>\n              {elem}\n            </Radio>\n          ))}\n        </Radio.Group>\n      </Form.Item>\n      <Form.Item {...formItemLayout} name=\"preferedStudentsLocation\" label={LABELS.studentsLocation}>\n        <Radio.Group>\n          {locations.map(({ value, label }) => (\n            <Radio key={value} value={value}>\n              {label}\n            </Radio>\n          ))}\n        </Radio.Group>\n      </Form.Item>\n    </FormCard>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/CourseCertificateAlert/CourseCertificateAlert.tsx",
    "content": "import { Button, Result } from 'antd';\n\ntype CourseCertificateAlertProps = {\n  certificateDiscipline?: string;\n};\n\nexport function CourseCertificateAlert({ certificateDiscipline = 'any' }: CourseCertificateAlertProps) {\n  return (\n    <Result\n      status=\"info\"\n      icon={\n        <img\n          src=\"https://cdn.rs.school/sloths/cleaned/train.svg\"\n          alt=\"train icon\"\n          style={{ width: 100, height: 100 }}\n        />\n      }\n      title={`To register for this course, you need to already have ${certificateDiscipline} RS School certificate.`}\n      subTitle={`Complete ${certificateDiscipline} course to unlock access.`}\n      extra={\n        <Button type=\"primary\" href=\"/\">\n          Back to Home\n        </Button>\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/CourseLabel/CourseLabel.tsx",
    "content": "import { CourseDto } from '@client/api';\nimport { CourseIcon } from '@client/shared/components/Icons';\nimport { formatMonthFriendly } from '@client/services/formatter';\nimport { LABELS } from '@client/modules/Registry/constants';\n\ntype Props = {\n  course: CourseDto;\n  isStudentForm?: boolean;\n};\n\nexport function CourseLabel({ course, isStudentForm }: Props) {\n  const { discipline, name, startDate, personalMentoringStartDate, personalMentoringEndDate } = course;\n  const disciplineName = discipline?.name ? `${discipline.name}, ` : '';\n  const courseInfo = isStudentForm\n    ? ` ${name} (${disciplineName}${formatMonthFriendly(startDate)}) `\n    : ` ${name} (${LABELS.mentoring} ${\n        personalMentoringStartDate ? formatMonthFriendly(personalMentoringStartDate) : ''\n      }-${personalMentoringEndDate ? formatMonthFriendly(personalMentoringEndDate) : ''}) `;\n  return (\n    <>\n      <CourseIcon course={course} />\n      {courseInfo}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/DataProcessingCheckbox/DataProcessingCheckbox.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { Form } from 'antd';\nimport { ERROR_MESSAGES } from '@client/modules/Registry/constants';\nimport { DataProcessingCheckbox } from './DataProcessingCheckbox';\n\nenum Checkbox {\n  notChecked,\n  checked,\n}\n\nconst renderCheckbox = (checked = Checkbox.notChecked) =>\n  render(\n    <Form initialValues={{ dataProcessing: checked }}>\n      <DataProcessingCheckbox />\n    </Form>,\n  );\n\ndescribe('DataProcessingCheckbox', () => {\n  const user = userEvent.setup();\n\n  test('should render checkbox', async () => {\n    renderCheckbox();\n\n    const checkbox = await screen.findByRole('checkbox');\n    expect(checkbox).toBeInTheDocument();\n  });\n\n  test('should not render error message when checkbox is selected', async () => {\n    renderCheckbox(Checkbox.checked);\n\n    const checkbox = await screen.findByRole('checkbox');\n    const errorMessage = screen.queryByText(ERROR_MESSAGES.shouldAgree);\n    expect(checkbox).toBeChecked();\n    expect(errorMessage).not.toBeInTheDocument();\n  });\n\n  test('should render error message when checkbox is not selected', async () => {\n    renderCheckbox(Checkbox.checked);\n\n    const checkbox = await screen.findByRole('checkbox');\n\n    await user.click(checkbox);\n\n    const errorMessage = await screen.findByText(ERROR_MESSAGES.shouldAgree);\n    expect(checkbox).not.toBeChecked();\n    expect(errorMessage).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/DataProcessingCheckbox/DataProcessingCheckbox.tsx",
    "content": "import { Form, Checkbox, Typography } from 'antd';\nimport { ERROR_MESSAGES, DATA_PROCESSING_TEXT, TAIL_FORM_ITEM_LAYOUT } from '@client/modules/Registry/constants';\n\nconst { Text } = Typography;\n\ntype Props = {\n  isStudentForm?: boolean;\n};\n\nexport function DataProcessingCheckbox({ isStudentForm }: Props) {\n  return (\n    <Form.Item\n      {...TAIL_FORM_ITEM_LAYOUT(!isStudentForm)}\n      name=\"dataProcessing\"\n      valuePropName=\"checked\"\n      rules={[\n        {\n          validator: (_, value) => (value ? Promise.resolve() : Promise.reject(new Error(ERROR_MESSAGES.shouldAgree))),\n        },\n      ]}\n    >\n      <Checkbox>\n        <Text type=\"secondary\">{DATA_PROCESSING_TEXT}</Text>\n      </Checkbox>\n    </Form.Item>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/Footer/Footer.tsx",
    "content": "import { Col, Row, Typography } from 'antd';\nimport { GitHubLogoIcon, RSLogoIcon } from '@client/shared/components/Icons';\n\nconst { Text } = Typography;\nconst copyrights = `Copyright © The Rolling Scopes ${new Date().getFullYear()}`;\n\nexport function Footer() {\n  return (\n    <footer>\n      <Row style={{ maxWidth: 232, paddingBlock: 36 }} gutter={[0, 10]}>\n        <Col span={24}>\n          <Row justify=\"center\" align=\"middle\" style={{ height: 32, paddingInline: 5.5, gap: 24 }}>\n            <RSLogoIcon />\n            <GitHubLogoIcon />\n          </Row>\n        </Col>\n        <Col span={24}>\n          <Text type=\"secondary\">{copyrights}</Text>\n        </Col>\n      </Row>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormButtons/FormButtons.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { Form } from 'antd';\nimport { FormButtons } from './FormButtons';\n\ntype Props = {\n  onPrevious?: () => void;\n  submitTitle?: string;\n};\n\nconst previousHandler = vi.fn();\nconst submitHandler = vi.fn();\n\nconst renderFormButtons = ({ onPrevious, submitTitle }: Props = {}) =>\n  render(\n    <Form onFinish={submitHandler}>\n      <FormButtons onPrevious={onPrevious} submitTitle={submitTitle} />\n    </Form>,\n  );\n\ndescribe('FormButtons', () => {\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  const user = userEvent.setup();\n\n  test('should render only Submit button', () => {\n    renderFormButtons();\n\n    const submitButton = screen.queryByRole('button', { name: /submit/i });\n    const previousButton = screen.queryByRole('button', { name: /previous/i });\n    expect(submitButton).toBeInTheDocument();\n    expect(previousButton).not.toBeInTheDocument();\n  });\n\n  test('should render submit button with custom title', () => {\n    const submitTitle = 'Continue';\n    renderFormButtons({ submitTitle });\n\n    const submitButton = screen.queryByRole('button', { name: submitTitle });\n    expect(submitButton).toBeInTheDocument();\n  });\n\n  test('should render both buttons (submit & previous)', () => {\n    renderFormButtons({ onPrevious: previousHandler });\n\n    const submitButton = screen.queryByRole('button', { name: /submit/i });\n    const previousButton = screen.queryByRole('button', { name: /previous/i });\n    expect(submitButton).toBeInTheDocument();\n    expect(previousButton).toBeInTheDocument();\n  });\n\n  test('should call previousHandler', async () => {\n    renderFormButtons({ onPrevious: previousHandler });\n\n    const button = await screen.findByRole('button', { name: /previous/i });\n    expect(button).toBeInTheDocument();\n\n    await user.click(button);\n\n    expect(previousHandler).toHaveBeenCalled();\n  });\n\n  test('should call submitHandler', async () => {\n    renderFormButtons();\n\n    const button = await screen.findByRole('button', { name: /submit/i });\n    expect(button).toBeInTheDocument();\n\n    await user.click(button);\n\n    expect(submitHandler).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormButtons/FormButtons.tsx",
    "content": "import { Row, Col, Button, Form } from 'antd';\nimport { TAIL_FORM_ITEM_LAYOUT } from '@client/modules/Registry/constants';\n\ntype Props = {\n  onPrevious?: () => void;\n  submitTitle?: string;\n};\n\nexport function FormButtons({ onPrevious, submitTitle = 'Submit' }: Props) {\n  return (\n    <Form.Item {...TAIL_FORM_ITEM_LAYOUT(!!onPrevious)} style={{ marginBottom: 0 }}>\n      <Row justify=\"end\" className=\"buttons\" gutter={24}>\n        {onPrevious ? (\n          <Col>\n            <Button size=\"large\" type=\"default\" onClick={onPrevious}>\n              Previous\n            </Button>\n          </Col>\n        ) : null}\n        <Col>\n          <Button size=\"large\" type=\"primary\" htmlType=\"submit\">\n            {submitTitle}\n          </Button>\n        </Col>\n      </Row>\n    </Form.Item>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormCard/FormCard.tsx",
    "content": "import { Card } from 'antd';\nimport { ReactNode } from 'react';\n\ntype Props = {\n  title: ReactNode;\n  children?: ReactNode;\n};\n\nexport function FormCard({ title, children }: Props) {\n  return (\n    <Card title={title} bordered={false} headStyle={{ paddingBlock: 16 }}>\n      {children}\n    </Card>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormSections/DoneSection/DoneSection.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { DoneSection } from './DoneSection';\n\nconst renderDoneSection = (courseName?: string) => {\n  render(<DoneSection courseName={courseName} />);\n};\n\nconst courseName = 'test-course';\n\ndescribe('DoneSection', () => {\n  test('should render Continue link on student form', async () => {\n    renderDoneSection(courseName);\n\n    const link = await screen.findByRole('link', { name: /continue/i });\n    expect(link).toBeInTheDocument();\n    expect(link).toHaveAttribute('href', '/');\n  });\n\n  test('should not render Continue link on mentor form', async () => {\n    renderDoneSection();\n\n    const link = screen.queryByRole('link', { name: /continue/i });\n    expect(link).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormSections/DoneSection/DoneSection.tsx",
    "content": "import { Button, Card, Col, Row, Typography } from 'antd';\nimport Icon from '@ant-design/icons';\nimport type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';\nimport { SlothImage } from '@client/components/SlothImage';\nimport { SUCCESS_TEXT } from '@client/modules/Registry/constants';\n\ntype Props = {\n  courseName?: string;\n};\n\nconst { Title, Text } = Typography;\n\nexport function DoneSection({ courseName }: Props) {\n  const isStudentForm = !!courseName;\n  const isMentorForm = !isStudentForm;\n\n  return (\n    <Card>\n      <Row justify=\"center\" gutter={[0, 28]}>\n        <Col>\n          <SlothImage name={isStudentForm ? 'its-a-good-job' : 'slothzy'} />\n        </Col>\n        <Col span={24}>\n          <Row justify=\"center\" gutter={[0, 16]}>\n            <Col span={24}>\n              <Row justify=\"center\">\n                <Title level={3} style={{ marginBottom: 0 }}>\n                  Success\n                </Title>\n              </Row>\n            </Col>\n            {isMentorForm && (\n              <>\n                <Col span={24}>\n                  <Row justify=\"center\">\n                    <Title level={3} style={{ textTransform: 'uppercase', color: '#1890FF', marginBottom: 0 }}>\n                      but\n                    </Title>\n                  </Row>\n                </Col>\n                <Col span={24}>\n                  <Row justify=\"center\">\n                    <ArrowIcon />\n                  </Row>\n                </Col>\n              </>\n            )}\n            <Col span={24}>\n              <Row justify=\"center\">\n                <Text type=\"secondary\" style={{ maxWidth: '480px', textAlign: 'center' }}>\n                  {SUCCESS_TEXT(courseName)}\n                </Text>\n              </Row>\n            </Col>\n            <Col span={24}>\n              <Row justify=\"center\">\n                <Text type=\"secondary\">See you soon</Text>\n              </Row>\n            </Col>\n            {isStudentForm && (\n              <Col span={24}>\n                <Row justify=\"center\">\n                  <Button type=\"primary\" href=\"/\">\n                    Continue\n                  </Button>\n                </Row>\n              </Col>\n            )}\n          </Row>\n        </Col>\n      </Row>\n    </Card>\n  );\n}\n\nfunction svg() {\n  return (\n    <svg width=\"24\" height=\"65\" viewBox=\"0 0 24 65\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        d=\"M10.9393 63.7296C11.5251 64.3154 12.4749 64.3154 13.0607 63.7296L22.6066 54.1837C23.1924 53.5979 23.1924 52.6481 22.6066 52.0623C22.0208 51.4766 21.0711 51.4766 20.4853 52.0623L12 60.5476L3.51472 52.0623C2.92893 51.4766 1.97918 51.4766 1.3934 52.0623C0.807609 52.6481 0.807609 53.5979 1.3934 54.1837L10.9393 63.7296ZM10.5 -6.5567e-08L10.5 62.6689L13.5 62.6689L13.5 6.5567e-08L10.5 -6.5567e-08Z\"\n        fill=\"#1890FF\"\n      />\n    </svg>\n  );\n}\n\nfunction ArrowIcon(props: Partial<CustomIconComponentProps>) {\n  return <Icon component={svg} {...props} />;\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormSections/GeneralSection/GeneralSection.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { CourseDto } from '@client/api';\nimport { CARD_TITLES } from '@client/modules/Registry/constants';\nimport { GeneralSection } from './GeneralSection';\nimport usePlacesAutocomplete from 'use-places-autocomplete';\n\nvi.mock('use-places-autocomplete');\n\nvi.mocked(usePlacesAutocomplete).mockImplementation(() => ({\n  value: null,\n  suggestions: {\n    data: {\n      map: vi.fn(),\n    },\n    loading: false,\n  },\n  setValue: vi.fn(),\n}));\n\nconst renderGeneralSection = (courses?: CourseDto[]) => {\n  render(\n    <Form>\n      <GeneralSection location={null} setLocation={vi.fn()} courses={courses} />\n    </Form>,\n  );\n};\n\ndescribe('GeneralSection', () => {\n  test.each`\n    title\n    ${CARD_TITLES.personalInfo}\n    ${CARD_TITLES.contactInfo}\n  `('should render mentor form card with $title title', async ({ title }) => {\n    renderGeneralSection();\n\n    const card = await screen.findByRole('heading', { name: title });\n    expect(card).toBeInTheDocument();\n  });\n\n  test('should not render CourseDetails card on mentor form', async () => {\n    renderGeneralSection();\n\n    const card = screen.queryByRole('heading', { name: CARD_TITLES.courseDetails });\n    expect(card).not.toBeInTheDocument();\n  });\n\n  test.each`\n    title\n    ${CARD_TITLES.courseDetails}\n    ${CARD_TITLES.personalInfo}\n  `('should render student form card with $title title', async ({ title }) => {\n    renderGeneralSection([]);\n\n    const card = await screen.findByRole('heading', { name: title });\n    expect(card).toBeInTheDocument();\n  });\n\n  test('should not render ContactInfo card on student form', async () => {\n    renderGeneralSection([]);\n\n    const card = screen.queryByRole('heading', { name: CARD_TITLES.contactInfo });\n    expect(card).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormSections/GeneralSection/GeneralSection.tsx",
    "content": "import { Col, Row } from 'antd';\nimport { Dispatch, SetStateAction } from 'react';\nimport { Location } from '@common/models';\nimport { PersonalInfo, ContactInfo, CourseDetails } from '@client/modules/Registry/components';\nimport { CourseDto } from '@client/api';\n\ntype Props = {\n  location: Location | null;\n  setLocation: Dispatch<SetStateAction<Location | null>>;\n  courses?: CourseDto[];\n};\n\nexport function GeneralSection({ location, setLocation, courses }: Props) {\n  const isStudentForm = !!courses;\n  const isMentorForm = !isStudentForm;\n\n  return (\n    <Row justify=\"center\" gutter={[0, 24]}>\n      {isStudentForm && (\n        <Col span={24}>\n          <CourseDetails courses={courses} />\n        </Col>\n      )}\n      <Col span={24}>\n        <PersonalInfo setLocation={setLocation} location={location} isStudentForm={isStudentForm} />\n      </Col>\n      {isMentorForm && (\n        <Col span={24}>\n          <ContactInfo />\n        </Col>\n      )}\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormSections/MentorshipSection/MentorshipSection.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { CARD_TITLES } from '@client/modules/Registry/constants';\nimport { MentorshipSection } from './MentorshipSection';\n\nconst renderMentorshipSection = () => {\n  render(\n    <Form>\n      <MentorshipSection courses={[]} disciplines={[]} onPrevious={vi.fn()} />\n    </Form>,\n  );\n};\n\ndescribe('MentorshipSection', () => {\n  test.each`\n    title\n    ${CARD_TITLES.disciplines}\n    ${CARD_TITLES.preferences}\n    ${CARD_TITLES.additionalInfo}\n  `('should render card with $title title', async ({ title }) => {\n    renderMentorshipSection();\n\n    const card = await screen.findByRole('heading', { name: title });\n    expect(card).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/FormSections/MentorshipSection/MentorshipSection.tsx",
    "content": "import { DisciplineDto } from '@client/api';\nimport { Course } from '@client/services/models';\nimport { Preferences, Disciplines, AdditionalInfo } from '@client/modules/Registry/components';\nimport { Col, Row } from 'antd';\n\ntype Props = {\n  courses: Course[];\n  disciplines: DisciplineDto[];\n  onPrevious: () => void;\n};\n\nexport function MentorshipSection({ courses, disciplines, onPrevious }: Props) {\n  return (\n    <Row justify=\"center\" gutter={[0, 24]}>\n      <Col span={24}>\n        <Disciplines disciplines={disciplines} />\n      </Col>\n      <Col span={24}>\n        <Preferences />\n      </Col>\n      <Col span={24}>\n        <AdditionalInfo courses={courses} onPrevious={onPrevious} />\n      </Col>\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/Header/Header.tsx",
    "content": "import { Space, Typography } from 'antd';\nimport { ReactNode } from 'react';\n\nconst { Title, Text } = Typography;\n\ntype Props = {\n  title: ReactNode;\n};\n\nexport function Header({ title }: Props) {\n  return (\n    <Space direction=\"vertical\" align=\"center\" size={0} style={{ textAlign: 'center' }}>\n      <Title>{title}</Title>\n      <Text type=\"secondary\">Free courses from the developer community</Text>\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/LanguagesMentoring/LanguagesMentoring.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { UpdateUserDtoLanguagesEnum } from '@client/api';\nimport { getLanguageName } from '@client/components/SelectLanguages';\nimport { LABELS } from '@client/modules/Registry/constants';\nimport { LanguagesMentoring } from './LanguagesMentoring';\n\nconst mockValues = [UpdateUserDtoLanguagesEnum.En, UpdateUserDtoLanguagesEnum.Ru];\n\nconst renderLanguages = (isStudentForm = false) =>\n  render(\n    <Form initialValues={{ languagesMentoring: mockValues }}>\n      <LanguagesMentoring isStudentForm={isStudentForm} />\n    </Form>,\n  );\n\ndescribe('LanguagesMentoring', () => {\n  test(`should render field with \"${LABELS.languagesMentor}\" label on mentor form`, async () => {\n    renderLanguages();\n\n    const field = await screen.findByLabelText(LABELS.languagesMentor);\n    expect(field).toBeInTheDocument();\n  });\n\n  test(`should render field with \"${LABELS.languagesStudent}\" label on student form`, async () => {\n    renderLanguages(true);\n\n    const field = await screen.findByLabelText(LABELS.languagesStudent);\n    expect(field).toBeInTheDocument();\n  });\n\n  test.each`\n    value\n    ${getLanguageName(UpdateUserDtoLanguagesEnum.En)}\n    ${getLanguageName(UpdateUserDtoLanguagesEnum.Ru)}\n  `('should render pre-selected option with $value value', async ({ value }) => {\n    renderLanguages();\n\n    const option = await screen.findByText(value);\n    expect(option).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/LanguagesMentoring/LanguagesMentoring.tsx",
    "content": "import { Form } from 'antd';\nimport { SelectLanguages } from '@client/components/SelectLanguages';\nimport { LABELS, VALIDATION_RULES, WIDE_FORM_ITEM_LAYOUT } from '@client/modules/Registry/constants';\n\ntype Props = {\n  isStudentForm?: boolean;\n};\n\nexport function LanguagesMentoring({ isStudentForm }: Props) {\n  return (\n    <Form.Item\n      {...WIDE_FORM_ITEM_LAYOUT(isStudentForm)}\n      name=\"languagesMentoring\"\n      label={isStudentForm ? LABELS.languagesStudent : LABELS.languagesMentor}\n      rules={VALIDATION_RULES}\n      required\n    >\n      <SelectLanguages />\n    </Form.Item>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/NoCourses/NoCourses.tsx",
    "content": "import { MehTwoTone } from '@ant-design/icons';\nimport { Button, Result } from 'antd';\n\nexport function NoCourses() {\n  return (\n    <Result\n      status=\"info\"\n      icon={<MehTwoTone />}\n      title=\"There are no available courses.\"\n      subTitle=\"Please come back later.\"\n      extra={\n        <Button type=\"primary\" href=\"/\">\n          Back to Home\n        </Button>\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/RegistrationForm/RegistrationForm.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { FORM_TITLES } from '@client/modules/Registry/constants';\nimport { RegistrationForm } from './RegistrationForm';\n\nconst steps = [\n  {\n    title: 'general',\n    content: <span>general-content</span>,\n  },\n  {\n    title: 'done',\n    content: <span>done-content</span>,\n  },\n];\n\nconst FormWrapper = ({ type }: { type?: 'mentor' | 'student' }) => {\n  const [form] = Form.useForm();\n\n  return <RegistrationForm form={form} handleSubmit={vi.fn()} steps={steps} currentStep={0} type={type} />;\n};\n\nconst renderForm = (type?: 'mentor' | 'student') => {\n  render(<FormWrapper type={type} />);\n};\n\ndescribe('RegistrationForm', () => {\n  test('should render form', async () => {\n    renderForm();\n\n    const form = await screen.findByRole('form');\n    expect(form).toBeInTheDocument();\n  });\n\n  test('should render mentor form title', async () => {\n    renderForm();\n\n    const title = await screen.findByText(FORM_TITLES.mentorForm);\n    expect(title).toBeInTheDocument();\n  });\n\n  test('should render student form title', async () => {\n    renderForm('student');\n\n    const title = await screen.findByText(FORM_TITLES.studentForm);\n    expect(title).toBeInTheDocument();\n  });\n\n  test.each(steps)('should render step title', async ({ title }) => {\n    renderForm();\n\n    const stepTitle = await screen.findByText(title);\n    expect(stepTitle).toBeInTheDocument();\n  });\n\n  test('should render current step content', async () => {\n    renderForm();\n\n    const content = await screen.findByText(`${steps[0]?.title}-content`);\n    expect(content).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Registry/components/RegistrationForm/RegistrationForm.tsx",
    "content": "import { Col, Form, FormInstance, Row, Steps, theme } from 'antd';\nimport { Store } from 'antd/lib/form/interface';\nimport { useUpdate } from 'react-use';\nimport { Footer, Header } from '@client/modules/Registry/components';\nimport { DEFAULT_FORM_ITEM_LAYOUT, FORM_TITLES } from '@client/modules/Registry/constants';\nimport { useFormLayout } from '@client/modules/Registry/hooks';\nimport { useMemo } from 'react';\n\ntype Props = {\n  form: FormInstance;\n  handleSubmit: (values: any) => Promise<void>;\n  steps: {\n    title: string;\n    content: JSX.Element;\n  }[];\n  currentStep: number;\n  initialValues?: Store;\n  type?: 'mentor' | 'student';\n};\n\nexport function RegistrationForm({ form, handleSubmit, steps, currentStep, initialValues, type = 'mentor' }: Props) {\n  const update = useUpdate();\n  const { formLayout, isSmallScreen } = useFormLayout();\n  const stepItems = useMemo(\n    () => steps.map(({ title }) => ({ title: isSmallScreen ? null : title })),\n    [steps, isSmallScreen],\n  );\n  const title = type === 'mentor' ? FORM_TITLES.mentorForm : FORM_TITLES.studentForm;\n  const { token } = theme.useToken();\n\n  return (\n    <Row justify=\"center\" style={{ paddingBlock: 24 }}>\n      <Col xs={24} sm={20} md={24} lg={18} xxl={14}>\n        <Form\n          {...DEFAULT_FORM_ITEM_LAYOUT}\n          role=\"form\"\n          layout={formLayout}\n          form={form}\n          initialValues={initialValues}\n          onChange={update}\n          onFinish={handleSubmit}\n          onFinishFailed={({ errorFields: [errorField] }) => errorField && form.scrollToField(errorField.name)}\n        >\n          <Row justify=\"center\" gutter={[0, 24]}>\n            <Col>\n              <Header title={title} />\n            </Col>\n            <Col\n              span={24}\n              style={{\n                background: token.colorBgContainer,\n                borderRadius: 2,\n                paddingBlock: 16,\n                paddingInline: 60,\n              }}\n            >\n              <Steps current={currentStep} responsive={false} items={stepItems} />\n            </Col>\n            <Col span={24}>{steps[currentStep]?.content}</Col>\n            <Col span={24} flex=\"none\">\n              <Footer />\n            </Col>\n          </Row>\n        </Form>\n      </Col>\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/components/index.tsx",
    "content": "export * from './FormButtons/FormButtons';\nexport * from './FormCard/FormCard';\nexport * from './FormSections/DoneSection/DoneSection';\nexport * from './FormSections/GeneralSection/GeneralSection';\nexport * from './FormSections/MentorshipSection/MentorshipSection';\nexport * from './Cards/PersonalInfo/PersonalInfo';\nexport * from './Cards/AdditionalInfo/AdditionalInfo';\nexport * from './Cards/ContactInfo/ContactInfo';\nexport * from './Cards/Disciplines/Disciplines';\nexport * from './Cards/Preferences/Preferences';\nexport * from './Cards/CourseDetails/CourseDetails';\nexport * from './Footer/Footer';\nexport * from './DataProcessingCheckbox/DataProcessingCheckbox';\nexport * from './LanguagesMentoring/LanguagesMentoring';\nexport * from './CourseLabel/CourseLabel';\nexport * from './NoCourses/NoCourses';\nexport * from './Header/Header';\nexport * from './RegistrationForm/RegistrationForm';\nexport * from './CourseCertificateAlert/CourseCertificateAlert';\n"
  },
  {
    "path": "client/src/modules/Registry/constants/index.ts",
    "content": "import { Rule } from 'antd/lib/form';\n\nconst RSSCHOOL_BOT_LINK = 'https://t.me/rsschool_bot?start';\nconst DATA_PROCESSING_TEXT =\n  'I agree to the processing of my personal data contained in the application and sharing it with companies only for students employment purposes.';\nconst SUCCESS_TEXT = (courseName?: string) =>\n  courseName\n    ? `You have successfully registered for the ${courseName} course.`\n    : 'Before you start we need to consider your application and submit you to a course. It could take some time. We will send you next steps via an email on the address you provided during registration.';\n\nconst ERROR_MESSAGES = {\n  chooseAtLeastOne: 'Should choose at least one',\n  shouldAgree: 'Should agree to the data processing',\n  inEnglish: (prop: string) => `${prop} should be in English`,\n  email: 'Invalid email',\n  epamEmail: 'Please enter a valid EPAM email',\n  location: 'Please select city',\n  phone: 'Invalid phone number',\n  tryLater: 'An error occurred. Please try later',\n};\n\nconst TOOLTIPS = {\n  locationMentor: 'We need your location for understanding audience and use it for students distribution',\n  locationStudent: 'We need your location for understanding the audience and for mentor distribution.',\n  primaryEmail: 'No spam e-mails. Only for course purposes.',\n  epamEmail: 'If you are EPAM employee, please specify your email to avoid some manual processes later',\n};\n\nconst FORM_TITLES = {\n  mentorForm: 'Mentors registration',\n  studentForm: 'Welcome to RS School',\n};\n\nconst PLACEHOLDERS = {\n  firstName: 'John',\n  lastName: 'Doe',\n  email: 'user@example.com',\n  epamEmail: 'first_last@epam.com',\n  telegram: 'johnny',\n  skype: 'johnsmith',\n  whatsApp: 'johndoe',\n  phone: '+1234567890',\n  notes: 'Preferable time to contact, planned days off etc.',\n  courses: 'Select courses',\n  languages: 'Select languages',\n  aboutYourself: 'A couple words about yourself...',\n};\n\nconst EXTRAS = {\n  inEnglish: 'In English, as in passport',\n  readyToMentor: 'You are ready to mentor per course',\n};\n\nconst LABELS = {\n  firstName: 'First Name',\n  lastName: 'Last Name',\n  location: 'Location',\n  primaryEmail: 'Primary E-mail',\n  epamEmail: 'EPAM E-mail',\n  telegram: 'Telegram',\n  skype: 'Skype',\n  whatsApp: 'WhatsApp',\n  email: 'E-mail',\n  phone: 'Phone',\n  notes: 'Contact Notes',\n  courses: 'Preferred Courses',\n  languagesMentor: 'Languages you can mentor in',\n  languagesStudent: 'My Languages',\n  aboutYourself: 'About Yourself',\n  disciplines: 'You can mentor',\n  studentsCount: 'Count of students',\n  studentsLocation: 'Students location',\n  course: 'Course',\n  mentoring: 'Mentoring:',\n};\n\nconst CARD_TITLES = {\n  additionalInfo: 'Additional information',\n  contactInfo: 'Contact information',\n  courseDetails: 'Course details',\n  disciplines: 'Disciplines',\n  personalInfo: 'Personal information',\n  preferences: 'Preferences about students',\n};\n\nconst VALIDATION_RULES: Rule[] = [\n  {\n    validator: (_, value) => {\n      return value?.length ? Promise.resolve() : Promise.reject(new Error(ERROR_MESSAGES.chooseAtLeastOne));\n    },\n  },\n];\n\nconst TAIL_FORM_ITEM_LAYOUT = (isMentorshipSection: boolean) =>\n  isMentorshipSection\n    ? {\n        wrapperCol: {\n          xs: { span: 12, offset: 0 },\n          sm: { span: 24, offset: 0 },\n          md: { span: 10, offset: 8 },\n        },\n      }\n    : {\n        wrapperCol: {\n          xs: { span: 12, offset: 0 },\n          sm: { span: 24, offset: 0 },\n          md: { span: 16, offset: 6 },\n          xl: { span: 8, offset: 8 },\n        },\n      };\n\nconst WIDE_FORM_ITEM_LAYOUT = (isStudentForm = false) =>\n  isStudentForm\n    ? {}\n    : {\n        labelCol: {\n          sm: { span: 24, offset: 0 },\n          md: { span: 8, offset: 0 },\n        },\n        wrapperCol: {\n          sm: { span: 24, offset: 0 },\n          md: { span: 10, offset: 0 },\n        },\n      };\n\nconst DEFAULT_FORM_ITEM_LAYOUT = {\n  labelCol: {\n    sm: {},\n    md: { span: 6 },\n    xl: { span: 8 },\n  },\n  wrapperCol: {\n    sm: { span: 24 },\n    md: { span: 16 },\n    xl: { span: 8 },\n  },\n};\n\nexport {\n  RSSCHOOL_BOT_LINK,\n  DATA_PROCESSING_TEXT,\n  ERROR_MESSAGES,\n  TOOLTIPS,\n  FORM_TITLES,\n  PLACEHOLDERS,\n  EXTRAS,\n  LABELS,\n  CARD_TITLES,\n  VALIDATION_RULES,\n  DEFAULT_FORM_ITEM_LAYOUT,\n  WIDE_FORM_ITEM_LAYOUT,\n  TAIL_FORM_ITEM_LAYOUT,\n  SUCCESS_TEXT,\n};\n"
  },
  {
    "path": "client/src/modules/Registry/hooks/index.ts",
    "content": "export * from './useFormLayout/useFormLayout';\nexport * from './useMentorData/useMentorData';\nexport * from './useStudentData/useStudentData';\n"
  },
  {
    "path": "client/src/modules/Registry/hooks/useFormLayout/useFormLayout.ts",
    "content": "import { Grid } from 'antd';\nimport { FormLayout } from 'antd/lib/form/Form';\n\nconst { useBreakpoint } = Grid;\n\nexport function useFormLayout() {\n  const { xs, sm, md, lg, xl, xxl } = useBreakpoint();\n  const largeScreenSizes = [md, lg, xl, xxl];\n  const isSmallScreen = xs || (sm && !largeScreenSizes.some(Boolean));\n  const formLayout: FormLayout = isSmallScreen ? 'vertical' : 'horizontal';\n\n  return { formLayout, isSmallScreen: xs } as const;\n}\n"
  },
  {
    "path": "client/src/modules/Registry/hooks/useMentorData/useMentorData.tsx",
    "content": "import { Form, message } from 'antd';\nimport { useState, useCallback } from 'react';\nimport { useAsync } from 'react-use';\nimport {\n  CourseDto,\n  DisciplineDto,\n  DisciplinesApi,\n  MentorOptionsDtoPreferedStudentsLocationEnum,\n  ProfileApi,\n} from '@client/api';\nimport { Location } from '@common/models';\nimport { CdnService } from '@client/services/cdn';\nimport { Course } from '@client/services/models';\nimport { UserFull, UserService } from '@client/services/user';\nimport { GeneralSection, MentorshipSection, DoneSection } from '@client/modules/Registry/components';\nimport { ERROR_MESSAGES } from '@client/modules/Registry/constants';\n\nexport type FormData = ReturnType<typeof getInitialValues>;\n\nconst cdnService = new CdnService();\nconst userService = new UserService();\nconst disciplinesApi = new DisciplinesApi();\nconst profileApi = new ProfileApi();\n\nexport function useMentorData(courseAlias?: string | string[]) {\n  const [form] = Form.useForm<FormData>();\n  const [courses, setCourses] = useState<Course[]>([]);\n  const [currentStep, setCurrentSteps] = useState(0);\n  const [loading, setLoading] = useState(false);\n  const [disciplines, setDisciplines] = useState<DisciplineDto[]>([]);\n  const [resume, setResume] = useState<FormData | undefined>();\n  const [location, setLocation] = useState<Location | null>(null);\n\n  useAsync(async () => {\n    setLoading(true);\n    const [profile, courses, disciplinesData] = await Promise.all([\n      userService.getMyProfile(),\n      cdnService.getCourses(),\n      disciplinesApi.getDisciplines(),\n    ]);\n    const activeCourses = getActiveCourses(courses, courseAlias);\n    const preselectedCourseIds = courseAlias ? activeCourses.map(({ id }: Course) => id) : [];\n\n    setCourses(activeCourses);\n    setDisciplines(disciplinesData.data);\n    setLocation({\n      countryName: profile.countryName,\n      cityName: profile.cityName,\n    });\n    setResume(getInitialValues(profile, preselectedCourseIds));\n    setLoading(false);\n  }, []);\n\n  const onPrevious = () => {\n    setCurrentSteps(previousStep => previousStep - 1);\n  };\n\n  const handleSubmit = useCallback(\n    async (model: FormData) => {\n      const data = { ...resume, ...model };\n      setResume(data);\n      if (!currentStep) {\n        setCurrentSteps(previousStep => previousStep + 1);\n      } else {\n        setLoading(true);\n\n        const {\n          technicalMentoring,\n          preferedCourses,\n          preferedStudentsLocation,\n          maxStudentsLimit,\n          languagesMentoring,\n          location,\n          firstName,\n          lastName,\n          primaryEmail,\n          contactsEpamEmail,\n          contactsTelegram,\n          contactsSkype,\n          contactsWhatsApp,\n          contactsEmail,\n          contactsNotes,\n          contactsPhone,\n          aboutMyself,\n        } = data;\n\n        const registryModel = {\n          preferedCourses,\n          maxStudentsLimit,\n          preferedStudentsLocation,\n          languagesMentoring,\n          technicalMentoring,\n        };\n\n        const userModel = {\n          firstName,\n          lastName,\n          primaryEmail,\n          contactsEpamEmail,\n          contactsTelegram,\n          contactsSkype,\n          contactsWhatsApp,\n          contactsEmail,\n          contactsNotes,\n          contactsPhone,\n          aboutMyself,\n          cityName: location ? location.cityName : '',\n          countryName: location ? location.countryName : '',\n          languages: languagesMentoring,\n        };\n\n        const requests = [profileApi.updateUser(userModel), cdnService.registerMentor(registryModel)];\n\n        try {\n          await Promise.all(requests);\n          setCurrentSteps(previousStep => previousStep + 1);\n        } catch {\n          message.error(ERROR_MESSAGES.tryLater);\n        } finally {\n          setLoading(false);\n        }\n      }\n    },\n    [resume, currentStep],\n  );\n\n  const steps = [\n    { title: 'General', content: <GeneralSection location={location} setLocation={setLocation} /> },\n    {\n      title: 'Mentorship',\n      content: <MentorshipSection courses={courses} disciplines={disciplines} onPrevious={onPrevious} />,\n    },\n    { title: 'Done', content: <DoneSection /> },\n  ];\n\n  return {\n    handleSubmit,\n    resume,\n    loading,\n    currentStep,\n    steps,\n    form,\n  };\n}\n\nfunction getInitialValues(\n  {\n    countryName,\n    cityName,\n    languages,\n    firstName,\n    lastName,\n    primaryEmail,\n    contactsEpamEmail,\n    contactsTelegram,\n    contactsSkype,\n    contactsWhatsApp,\n    contactsEmail,\n    contactsNotes,\n    contactsPhone,\n    aboutMyself,\n  }: UserFull,\n  preselectedCourseIds: number[],\n) {\n  const location =\n    countryName &&\n    cityName &&\n    ({\n      countryName,\n      cityName,\n    } as Location | null);\n  return {\n    firstName,\n    lastName,\n    primaryEmail,\n    contactsEpamEmail,\n    contactsTelegram,\n    contactsSkype,\n    contactsWhatsApp,\n    contactsEmail,\n    contactsNotes,\n    contactsPhone,\n    aboutMyself,\n    location,\n    preferedCourses: preselectedCourseIds,\n    technicalMentoring: [],\n    languagesMentoring: languages ?? [],\n    preferedStudentsLocation:\n      MentorOptionsDtoPreferedStudentsLocationEnum.Any as MentorOptionsDtoPreferedStudentsLocationEnum,\n    maxStudentsLimit: 2,\n    dataProcessing: false,\n  };\n}\n\nfunction getActiveCourses(courses: CourseDto[], courseAlias?: string | string[]) {\n  if (!courseAlias) {\n    return courses\n      .filter(course => (course.planned || !course.completed) && !course.inviteOnly && course.personalMentoring)\n      .sort((a, b) => a.startDate.localeCompare(b.startDate));\n  }\n\n  if (Array.isArray(courseAlias)) {\n    return courses.filter((course: Course) => courseAlias.includes(course.alias));\n  }\n\n  return courses.filter((course: Course) => course.alias === courseAlias);\n}\n"
  },
  {
    "path": "client/src/modules/Registry/hooks/useStudentData/useStudentData.tsx",
    "content": "import { useAsync } from 'react-use';\nimport { Course } from '@client/services/models';\nimport { UserFull, UserService } from '@client/services/user';\nimport { StudentStats } from '@common/models';\nimport { useCallback, useEffect, useState } from 'react';\nimport { CdnService } from '@client/services/cdn';\nimport { GeneralSection, DoneSection } from '@client/modules/Registry/components';\nimport { Location } from '@common/models';\nimport { Form, Modal, theme, Typography } from 'antd';\nimport { useRouter } from 'next/router';\nimport { ExclamationCircleOutlined } from '@ant-design/icons';\nimport { DisciplinesApi, ProfileApi } from '@client/api';\nimport { TYPES } from '@client/configs/registry';\nimport { ERROR_MESSAGES } from '@client/modules/Registry/constants';\nimport { useMessage } from '@client/hooks';\n\nconst { Title, Text } = Typography;\n\ntype StudentFormData = ReturnType<typeof getInitialValues>;\n\ntype IdName = {\n  id: number;\n  name: string;\n};\n\nconst cdnService = new CdnService();\nconst profileApi = new ProfileApi();\nconst userService = new UserService();\nconst disciplinesApi = new DisciplinesApi();\n\nexport function useStudentData(githubId: string, courseAlias?: string) {\n  const { message } = useMessage();\n  const router = useRouter();\n  const [form] = Form.useForm<StudentFormData>();\n  const [registered, setRegistered] = useState<boolean | null>(null);\n  const [currentStep, setCurrentStep] = useState(0);\n  const [location, setLocation] = useState(null as Location | null);\n  const [loading, setLoading] = useState(false);\n  const [missingDisciplines, setMissingDisciplines] = useState('');\n  const [modal, modalContext] = Modal.useModal();\n\n  const { token } = theme.useToken();\n\n  const { value: student, loading: dataLoading } = useAsync(async () => {\n    const [profile, profileInfo, courses] = await Promise.all([\n      userService.getMyProfile(),\n      userService.getProfileInfo(githubId),\n      cdnService.getCourses(),\n    ]);\n\n    const registeredForCourses = enrolledOtherCourses(profileInfo?.studentStats, courses);\n\n    if (courseAlias) {\n      const currentCourse = courses.find(course => course.alias === courseAlias);\n      const value = registeredForCourses.some(({ id }) => id === currentCourse?.id);\n\n      if (currentCourse) {\n        const missingDisciplines = await getMissingDisciplines(currentCourse, profileInfo?.studentStats, courses);\n        setMissingDisciplines(missingDisciplines);\n      }\n\n      if (value) {\n        setRegistered(value);\n        return;\n      }\n    }\n\n    setRegistered(false);\n\n    const activeCourses = courseAlias\n      ? courses.filter(isCourseOpenForRegistryWithAlias(courseAlias))\n      : (\n          await Promise.all(\n            courses.filter(isCourseOpenForRegistry(registeredForCourses)).map(async course => ({\n              course,\n              hasMissingDisciplines: !!(await getMissingDisciplines(course, profileInfo?.studentStats, courses)),\n            })),\n          )\n        )\n          .filter(({ hasMissingDisciplines }) => !hasMissingDisciplines)\n          .map(({ course }) => course)\n          .sort(sortByStartDate);\n\n    form.setFieldsValue(getInitialValues(profile, activeCourses));\n\n    return {\n      profile,\n      registeredForCourses,\n      courses: activeCourses,\n    } as const;\n  }, [githubId, courseAlias]);\n\n  const handleSubmit = useCallback(\n    async (values: StudentFormData) => {\n      if (loading || dataLoading) {\n        return;\n      }\n\n      const { courseId, location, primaryEmail, contactsEpamEmail, firstName, lastName, languagesMentoring } = values;\n      const registryModel = { type: TYPES.STUDENT, courseId };\n      const userModel = {\n        cityName: location?.cityName ?? '',\n        countryName: location?.countryName ?? '',\n        primaryEmail,\n        contactsEpamEmail,\n        firstName,\n        lastName,\n        languages: languagesMentoring,\n      };\n\n      if (student?.registeredForCourses.length) {\n        modal.confirm({\n          icon: <ExclamationCircleOutlined size={16} style={{ color: token.colorInfo }} />,\n          title: (\n            <Title level={5} style={{ verticalAlign: 'middle' }}>\n              Course registration warning\n            </Title>\n          ),\n          content: (\n            <>\n              <Text>You are already registered for the following courses:</Text>\n              <ul>\n                {student.registeredForCourses.map(({ name }) => (\n                  <li key={name}>\n                    <Text>{name}</Text>\n                  </li>\n                ))}\n              </ul>\n              <Text style={{ fontWeight: 700 }}>\n                NOTE: We do not recommend studying at several courses at the same time.\n              </Text>\n            </>\n          ),\n          centered: true,\n          onOk: async () => {\n            await confirmRegistration();\n          },\n          okText: 'Register',\n          maskClosable: true,\n          autoFocusButton: 'cancel',\n        });\n      } else {\n        await confirmRegistration();\n      }\n\n      async function confirmRegistration() {\n        const requests = [profileApi.updateUser(userModel), cdnService.registerStudent(registryModel)];\n        setLoading(true);\n\n        try {\n          await Promise.all(requests);\n          setCurrentStep(previousStep => previousStep + 1);\n        } catch {\n          message.error(ERROR_MESSAGES.tryLater);\n        } finally {\n          setLoading(false);\n        }\n      }\n    },\n    [loading, dataLoading, student?.registeredForCourses],\n  );\n\n  function getCourseName() {\n    const course = student?.courses.find(course => course.id === form.getFieldValue('courseId'));\n    return course?.fullName;\n  }\n\n  const steps = [\n    {\n      title: 'General',\n      content: <GeneralSection location={location} setLocation={setLocation} courses={student?.courses} />,\n    },\n    { title: 'Done', content: <DoneSection courseName={getCourseName()} /> },\n  ];\n\n  useEffect(() => {\n    if (registered) {\n      message.success('You are already registered to the course. Redirecting to Home page in 5 seconds...');\n      setTimeout(() => router.push('/'), 5000);\n    }\n  }, [registered, router]);\n\n  useEffect(() => {\n    setLocation(\n      student?.profile ? { countryName: student.profile.countryName, cityName: student.profile.cityName } : null,\n    );\n  }, [student?.profile]);\n\n  return {\n    courses: student?.courses ?? [],\n    loading: dataLoading || loading,\n    registered,\n    steps,\n    currentStep,\n    form,\n    handleSubmit,\n    modalContext,\n    missingDisciplines,\n  } as const;\n}\n\nfunction getInitialValues(\n  { countryName, cityName, languages, firstName, lastName, primaryEmail, contactsEpamEmail }: Partial<UserFull> = {},\n  courses: Course[] = [],\n) {\n  const location: Location | null = countryName && cityName ? { countryName, cityName } : null;\n  return {\n    firstName,\n    lastName,\n    primaryEmail,\n    contactsEpamEmail,\n    languagesMentoring: languages,\n    courseId: courses[0]?.id,\n    location,\n  } as const;\n}\n\nfunction enrolledOtherCourses(studentStats: StudentStats[] | undefined, courses: Course[]) {\n  return (\n    studentStats?.reduce((acc, { isExpelled, isCourseCompleted, courseId }) => {\n      const course = courses?.find(el => el.id === courseId);\n      if (!isExpelled && !isCourseCompleted && course?.completed === false) {\n        acc.push({ id: courseId, name: `${course.name} (${getStatus(course)})` });\n      }\n      return acc;\n    }, [] as IdName[]) ?? []\n  );\n}\n\nfunction isCourseOpenForRegistry(registeredCourses: IdName[]) {\n  return (course: Course) => {\n    // invite only courses do not open for public registration\n    if (course.inviteOnly) {\n      return false;\n    }\n    return isCourseAvailableForRegistration(course, registeredCourses);\n  };\n}\n\nfunction isCourseOpenForRegistryWithAlias(courseAlias?: string) {\n  return (course: Course) => courseAlias === course.alias && isCourseAvailableForRegistration(course, []);\n}\n\nfunction isCourseAvailableForRegistration(course: Course, registeredCourses: IdName[]) {\n  if (course.completed || registeredCourses.some(({ id }) => id === course.id)) {\n    return false;\n  }\n\n  const isRegistrationActive = new Date(course.registrationEndDate || 0).getTime() > Date.now();\n  if (isRegistrationActive) {\n    return true;\n  }\n\n  if (course.planned) {\n    return true;\n  }\n\n  return false;\n}\n\nfunction sortByStartDate(a: Course, b: Course) {\n  return a.startDate.localeCompare(b.startDate);\n}\n\nfunction getStatus(course: Course) {\n  return course.planned ? 'Planned' : 'Active';\n}\n\nasync function getMissingDisciplines(\n  course: Course,\n  studentStats: StudentStats[] | undefined,\n  courses: Course[],\n): Promise<string> {\n  let missingDisciplines = '';\n\n  const disciplineIds = course.certificateDisciplines;\n  const { data = [] } =\n    disciplineIds && disciplineIds?.length\n      ? ((await disciplinesApi.getDisciplinesByIds({ ids: disciplineIds })) as { data: IdName[] })\n      : {};\n\n  const certifiedStudentCourseIds =\n    studentStats\n      ?.map(course => ({\n        courseId: course.courseId,\n        certificate: course.certificateId,\n      }))\n      .filter(item => item.certificate)\n      .map(item => item.courseId) || [];\n\n  if (!disciplineIds?.length) {\n    if (Array.isArray(disciplineIds) && !certifiedStudentCourseIds.length) {\n      missingDisciplines = 'any';\n    }\n  } else {\n    const studentCertifiedDisciplineNames = [\n      ...new Set(\n        courses\n          .filter(course => certifiedStudentCourseIds.includes(course.id))\n          .map(course => course.discipline?.name)\n          .filter(Boolean),\n      ),\n    ];\n\n    const requiredDisciplineNames = data.map(discipline => discipline.name);\n    const hasAllRequiredDisciplines = requiredDisciplineNames.every(name =>\n      studentCertifiedDisciplineNames.includes(name),\n    );\n\n    if (!hasAllRequiredDisciplines) {\n      missingDisciplines = requiredDisciplineNames\n        .filter(name => !studentCertifiedDisciplineNames.includes(name))\n        .join(', ');\n    }\n  }\n\n  return missingDisciplines;\n}\n"
  },
  {
    "path": "client/src/modules/Registry/pages/Mentor/Mentor.tsx",
    "content": "import { RegistrationPageLayout } from '@client/components/RegistrationPageLayout';\nimport { RegistrationForm } from '@client/modules/Registry/components';\nimport { useMentorData } from '@client/modules/Registry/hooks';\nimport { useRouter } from 'next/router';\n\nexport function MentorRegistry() {\n  const router = useRouter();\n  const { resume, loading, currentStep, steps, form, handleSubmit } = useMentorData(\n    router.query.course as string | undefined,\n  );\n\n  return (\n    <RegistrationPageLayout loading={loading}>\n      {resume ? (\n        <RegistrationForm\n          form={form}\n          handleSubmit={handleSubmit}\n          steps={steps}\n          currentStep={currentStep}\n          initialValues={resume}\n        />\n      ) : null}\n    </RegistrationPageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/pages/Student/Student.tsx",
    "content": "import { RegistrationPageLayout } from '@client/components/RegistrationPageLayout';\nimport { SessionContext } from '@client/modules/Course/contexts';\nimport { CourseCertificateAlert, NoCourses, RegistrationForm } from '@client/modules/Registry/components';\nimport { useStudentData } from '@client/modules/Registry/hooks';\nimport { useRouter } from 'next/router';\nimport { useContext } from 'react';\n\nexport function StudentRegistry() {\n  const session = useContext(SessionContext);\n  const router = useRouter();\n  const { courses, loading, registered, steps, currentStep, form, handleSubmit, modalContext, missingDisciplines } =\n    useStudentData(session.githubId, router.query.course as string | undefined);\n\n  let content: React.ReactNode;\n  if (loading || registered) {\n    content = null;\n  } else if (missingDisciplines && courses.length) {\n    content = <CourseCertificateAlert certificateDiscipline={missingDisciplines} />;\n  } else if (!courses.length) {\n    content = <NoCourses />;\n  } else {\n    content = (\n      <RegistrationForm\n        form={form}\n        handleSubmit={handleSubmit}\n        steps={steps}\n        currentStep={currentStep}\n        type=\"student\"\n      />\n    );\n  }\n\n  return (\n    <RegistrationPageLayout loading={loading}>\n      {modalContext}\n      {content}\n    </RegistrationPageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Registry/pages/index.ts",
    "content": "export * from './Mentor/Mentor';\nexport * from './Student/Student';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/AdditionalActions/AdditionalActions.test.tsx",
    "content": "import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { AdditionalActions, AdditionalActionsProps, MenuItemType } from '.';\nimport { SettingsButtons } from '../SettingsPanel';\nimport { buildMenuItem } from '../SettingsPanel/helpers';\nimport { buildExportLink, buildICalendarLink, setExportLink } from './helpers';\n\nwindow.prompt = vi.fn();\n\nvi.mock('./helpers', () => ({\n  buildExportLink: vi.fn(),\n  buildICalendarLink: vi.fn(),\n  setExportLink: vi.fn(),\n}));\n\nconst PROPS_MOCK: AdditionalActionsProps = {\n  menuItems: generateMenuItems(),\n  courseId: 1,\n  timezone: 'Region/Town',\n  calendarToken: 'calendar-token',\n  courseAlias: 'course-alias',\n  onCopyFromCourse: vi.fn(),\n};\n\ndescribe('AdditionalActions', () => {\n  it('should render menu items', async () => {\n    render(<AdditionalActions {...PROPS_MOCK} />);\n\n    const moreBtn = screen.getByRole('button', { name: /more/i });\n    fireEvent.click(moreBtn);\n\n    const menuItems = await screen.findAllByRole('menuitem');\n    expect(menuItems).toHaveLength(4);\n  });\n\n  it('should call onCopyFromCourse when \"Copy from\" action was clicked', async () => {\n    render(<AdditionalActions {...PROPS_MOCK} />);\n    const moreBtn = screen.getByRole('button', { name: /more/i });\n    fireEvent.click(moreBtn);\n\n    const copyBtn = await screen.findByRole('menuitem', { name: new RegExp(SettingsButtons.Copy, 'i') });\n    fireEvent.click(copyBtn);\n\n    await waitFor(() => {\n      expect(PROPS_MOCK.onCopyFromCourse).toHaveBeenCalled();\n    });\n  });\n\n  it('should call onCalendarCopyLink when \"Copy iCal Link\" action was clicked', async () => {\n    render(<AdditionalActions {...PROPS_MOCK} />);\n    const moreBtn = screen.getByRole('button', { name: /more/i });\n    fireEvent.click(moreBtn);\n\n    const calendarBtn = await screen.findByRole('menuitem', { name: new RegExp(SettingsButtons.CopyLink, 'i') });\n    fireEvent.click(calendarBtn);\n\n    await waitFor(() => {\n      expect(buildICalendarLink).toHaveBeenCalledWith(\n        PROPS_MOCK.courseId,\n        PROPS_MOCK.calendarToken,\n        PROPS_MOCK.timezone,\n      );\n    });\n  });\n\n  it('should call onExport when \"Export\" action was clicked', async () => {\n    render(<AdditionalActions {...PROPS_MOCK} />);\n    const moreBtn = screen.getByRole('button', { name: /more/i });\n    fireEvent.click(moreBtn);\n\n    const exportBtn = await screen.findByRole('menuitem', { name: new RegExp(SettingsButtons.Export, 'i') });\n    fireEvent.click(exportBtn);\n\n    expect(buildExportLink).toHaveBeenCalledWith(PROPS_MOCK.courseId, PROPS_MOCK.timezone);\n    expect(setExportLink).toHaveBeenCalled();\n  });\n});\n\nfunction generateMenuItems(): MenuItemType[] {\n  return [\n    buildMenuItem(SettingsButtons.CopyLink, <></>, true),\n    buildMenuItem(SettingsButtons.Download, <></>, true),\n    buildMenuItem(SettingsButtons.Export, <></>, true),\n    buildMenuItem(SettingsButtons.Copy, <></>, true),\n  ];\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/components/AdditionalActions/AdditionalActions.tsx",
    "content": "import { Button, Dropdown, Space } from 'antd';\nimport { DownOutlined } from '@ant-design/icons';\nimport type { MenuProps } from 'antd';\nimport { buildExportLink, buildICalendarLink, setExportLink } from './helpers';\nimport { SettingsButtons } from '../SettingsPanel';\nimport { useCopyToClipboard } from 'react-use';\nimport { useMessage } from '@client/hooks';\n\nexport type MenuItemType = Required<MenuProps>['items'][number];\ntype MenuItemClickHandler = Required<MenuProps>['onClick'];\n\nexport interface AdditionalActionsProps {\n  menuItems: MenuItemType[];\n  courseId: number;\n  timezone: string;\n  calendarToken: string;\n  courseAlias: string;\n  onCopyFromCourse: () => void;\n}\n\nconst AdditionalActions = ({\n  menuItems,\n  courseId,\n  timezone,\n  calendarToken,\n  courseAlias,\n  onCopyFromCourse,\n}: AdditionalActionsProps) => {\n  const { message } = useMessage();\n  const [, copyToClipboard] = useCopyToClipboard();\n\n  const onExport = () => {\n    setExportLink(buildExportLink(courseId, timezone));\n  };\n\n  const onCalendarDownload = () => {\n    if (calendarToken) {\n      const iCalLink = buildICalendarLink(courseId, calendarToken, timezone);\n\n      const link = document.createElement('a');\n      link.href = iCalLink;\n      link.target = '_blank';\n      link.setAttribute('download', `schedule-${courseAlias}.ics`);\n      document.body.appendChild(link);\n      link.click();\n      link.remove();\n    }\n  };\n\n  function onCalendarCopyLink() {\n    const link = buildICalendarLink(courseId, calendarToken, timezone);\n    copyToClipboard(`${window.document.location.origin}${link}`);\n    message.success('Copied to clipboard');\n  }\n\n  const handleMenuItemClick: MenuItemClickHandler = item => {\n    switch (item.key) {\n      case SettingsButtons.CopyLink:\n        onCalendarCopyLink();\n        break;\n      case SettingsButtons.Download:\n        onCalendarDownload();\n        break;\n      case SettingsButtons.Export:\n        onExport();\n        break;\n      case SettingsButtons.Copy:\n        onCopyFromCourse();\n        break;\n      default:\n        break;\n    }\n  };\n\n  return (\n    <Dropdown menu={{ items: menuItems, onClick: handleMenuItemClick }} trigger={['click']} placement=\"bottomRight\">\n      <Button data-testid={SettingsButtons.More}>\n        <Space>\n          {SettingsButtons.More}\n          <DownOutlined />\n        </Space>\n      </Button>\n    </Dropdown>\n  );\n};\n\nexport default AdditionalActions;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/AdditionalActions/helpers.ts",
    "content": "export const buildICalendarLink = (courseId: number, token: string, timezone: string) =>\n  `/api/v2/courses/${courseId}/icalendar/${token}?timezone=${encodeURIComponent(timezone || '')}`;\n\nexport const buildExportLink = (courseId: number, timezone: string) =>\n  `/api/course/${courseId}/schedule/csv/${timezone.replace('/', '_')}`;\n\nexport const setExportLink = (link: string) => {\n  window.location.href = link;\n};\n"
  },
  {
    "path": "client/src/modules/Schedule/components/AdditionalActions/index.tsx",
    "content": "export { default as AdditionalActions } from './AdditionalActions';\nexport { type AdditionalActionsProps, type MenuItemType } from './AdditionalActions';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/EventDetails/EventDetails.module.css",
    "content": ".container {\n  position: relative;\n  max-width: 1200px;\n  margin: 20px auto;\n  padding: 20px 10px;\n}\n\n.buttonClose {\n  position: absolute;\n  right: 10px;\n  top: 0;\n}\n\n.buttonEdit {\n  position: absolute;\n  left: 10px;\n  top: 0;\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/components/EventDetails/EventDetails.tsx",
    "content": "import { CloseOutlined, EditOutlined } from '@ant-design/icons';\nimport { Button, Col, Row, Tooltip, Typography } from 'antd';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { renderTag, tagsRenderer } from '@client/shared/components/Table';\nimport dayjs from 'dayjs';\nimport Link from 'next/link';\nimport { CourseEvent } from '@client/services/course';\n\nimport styles from './EventDetails.module.css';\n\nconst { Title, Text } = Typography;\n\ninterface EventDetailsProps {\n  eventData: CourseEvent;\n  alias: string;\n  isAdmin: boolean;\n  isPreview?: boolean;\n  onEdit?: (isTask?: boolean) => void;\n}\n\nexport function EventDetails({ eventData, alias, isAdmin, isPreview, onEdit }: EventDetailsProps) {\n  const { event, dateTime, place, organizer, special, duration } = eventData;\n\n  return (\n    <>\n      <div className={styles.container}>\n        <Row justify=\"center\" align=\"middle\" gutter={[40, 8]}>\n          <Col>\n            <Title>{event.name}</Title>\n          </Col>\n        </Row>\n\n        {dateTime && (\n          <Row justify=\"center\" align=\"middle\" gutter={[40, 8]}>\n            <Col>\n              <Title level={3}>{dayjs(dateTime).format('MMM Do YYYY HH:mm')}</Title>\n            </Col>\n          </Row>\n        )}\n\n        {event.type && (\n          <Row justify=\"center\" align=\"middle\" gutter={[24, 20]}>\n            <Col>{renderTag(event.type)}</Col>\n            {special && <Col>{!!special && tagsRenderer(special.split(','))}</Col>}\n          </Row>\n        )}\n\n        {organizer && organizer.githubId && (\n          <Tooltip title=\"Organizer\">\n            <Row justify=\"center\" align=\"middle\" gutter={[16, 16]}>\n              <Col>\n                <GithubUserLink value={organizer.githubId} />\n              </Col>\n            </Row>\n          </Tooltip>\n        )}\n\n        {event.descriptionUrl && (\n          <Row justify=\"center\" align=\"middle\" gutter={[16, 16]}>\n            <Col>\n              <Title level={3}>\n                <a href={event.descriptionUrl} target=\"_blank\">\n                  Event link\n                </a>\n              </Title>\n            </Col>\n          </Row>\n        )}\n\n        <Row justify=\"center\" align=\"middle\" gutter={[16, 16]}>\n          {duration && (\n            <Col>\n              <Text strong>{`Duration: ${duration} hours`}</Text>\n            </Col>\n          )}\n          {place && (\n            <Col>\n              <Text strong>Place: {place}</Text>\n            </Col>\n          )}\n        </Row>\n\n        {event.description && (\n          <Row justify=\"center\" align=\"middle\" gutter={[16, 16]}>\n            <Col>\n              <Tooltip title=\"Description\">\n                <Text>{event.description}</Text>\n              </Tooltip>\n            </Col>\n          </Row>\n        )}\n\n        {isAdmin && (\n          <div className={styles.buttonEdit}>\n            <Button icon={<EditOutlined />} onClick={() => onEdit && onEdit(false)} />\n          </div>\n        )}\n\n        {!isPreview && (\n          <div className={styles.buttonClose}>\n            <Link prefetch={false} href={`/course/schedule?course=${alias}`} legacyBehavior>\n              <a>\n                <Button icon={<CloseOutlined />} />\n              </a>\n            </Link>\n          </div>\n        )}\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/components/EventDetails/index.ts",
    "content": "export { EventDetails } from './EventDetails';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/FilteredTags/FilteredTags.test.tsx",
    "content": "import { fireEvent, render, screen, within } from '@testing-library/react';\nimport { FilteredTags } from './FilteredTags';\nimport { CourseScheduleItemDto, CourseScheduleItemDtoTagEnum as TagsEnum } from '@client/api';\nimport { TAG_NAME_MAP } from '@client/modules/Schedule/constants';\n\ndescribe('FilteredTags', () => {\n  const onTagCloseMock = vi.fn();\n  const onClearAllButtonClick = vi.fn();\n\n  it('should not render when tags were not provided', () => {\n    render(<FilteredTags tagFilters={[]} onTagClose={onTagCloseMock} onClearAllButtonClick={onClearAllButtonClick} />);\n\n    expect(screen.queryByText(/Type /)).not.toBeInTheDocument();\n  });\n\n  it.each`\n    tag\n    ${TagsEnum.Coding}\n    ${TagsEnum.CrossCheckReview}\n    ${TagsEnum.CrossCheckSubmit}\n    ${TagsEnum.Interview}\n    ${TagsEnum.Lecture}\n    ${TagsEnum.SelfStudy}\n    ${TagsEnum.Test}\n  `('should render tag \"$tag\"', ({ tag }: { tag: CourseScheduleItemDto['tag'] }) => {\n    render(\n      <FilteredTags tagFilters={[tag]} onTagClose={onTagCloseMock} onClearAllButtonClick={onClearAllButtonClick} />,\n    );\n\n    expect(screen.getByText(getTagLabel(tag))).toBeInTheDocument();\n  });\n\n  it('should render several tags', () => {\n    render(\n      <FilteredTags\n        tagFilters={[TagsEnum.Coding, TagsEnum.CrossCheckReview, TagsEnum.Interview]}\n        onTagClose={onTagCloseMock}\n        onClearAllButtonClick={onClearAllButtonClick}\n      />,\n    );\n\n    expect(screen.getByText(getTagLabel(TagsEnum.Coding))).toBeInTheDocument();\n    expect(screen.getByText(getTagLabel(TagsEnum.CrossCheckReview))).toBeInTheDocument();\n    expect(screen.getByText(getTagLabel(TagsEnum.Interview))).toBeInTheDocument();\n  });\n\n  it('should render \"Clear all\" button', () => {\n    render(\n      <FilteredTags\n        tagFilters={[TagsEnum.Coding, TagsEnum.CrossCheckReview, TagsEnum.Interview]}\n        onTagClose={onTagCloseMock}\n        onClearAllButtonClick={onClearAllButtonClick}\n      />,\n    );\n\n    expect(screen.getByText(/Clear all/)).toBeInTheDocument();\n  });\n\n  it('should remove selected tag when onTagClose was called', () => {\n    render(\n      <FilteredTags\n        tagFilters={[TagsEnum.Coding, TagsEnum.CrossCheckReview, TagsEnum.Interview]}\n        onTagClose={onTagCloseMock}\n        onClearAllButtonClick={onClearAllButtonClick}\n      />,\n    );\n    const interviewTag = screen.getByText(getTagLabel(TagsEnum.Interview));\n    const interviewCrossIcon = within(interviewTag).getByRole('img', { name: 'Close' });\n\n    fireEvent.click(interviewCrossIcon);\n\n    expect(onTagCloseMock).toHaveBeenCalledWith(TagsEnum.Interview);\n  });\n\n  it('should clear all tags when onClearAllButtonClick was called', () => {\n    render(\n      <FilteredTags\n        tagFilters={[TagsEnum.Coding, TagsEnum.CrossCheckReview, TagsEnum.Interview]}\n        onTagClose={onTagCloseMock}\n        onClearAllButtonClick={onClearAllButtonClick}\n      />,\n    );\n\n    const clearAllBtn = screen.getByText(/Clear all/);\n    fireEvent.click(clearAllBtn);\n\n    expect(onClearAllButtonClick).toHaveBeenCalled();\n  });\n});\n\nfunction getTagLabel(tag: CourseScheduleItemDto['tag']) {\n  return TAG_NAME_MAP[tag];\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/components/FilteredTags/FilteredTags.tsx",
    "content": "import { Button, Col, Row, Tag, theme } from 'antd';\nimport { FilterFilled } from '@ant-design/icons';\nimport { TAG_NAME_MAP } from '@client/modules/Schedule/constants';\nimport { CourseScheduleItemDto } from '@client/api';\n\ntype FilteredTagsProps = {\n  tagFilters: string[];\n  onTagClose: (tag: string) => void;\n  onClearAllButtonClick: () => void;\n  filterName?: string;\n};\n\nexport const FilteredTags = ({ tagFilters, onTagClose, onClearAllButtonClick, filterName = '' }: FilteredTagsProps) => {\n  const { token } = theme.useToken();\n  return (\n    <>\n      {tagFilters?.length > 0 ? (\n        <Row style={{ padding: 12, background: token.colorBgContainer, marginBottom: 4, borderRadius: 4 }}>\n          <Col flex=\"auto\">\n            <FilterFilled\n              style={{\n                color: token.colorTextLabel,\n                marginRight: 8,\n              }}\n            />\n            {tagFilters.map(tag => (\n              <Tag key={tag} closable onClose={() => onTagClose(tag)}>{`${filterName}${\n                TAG_NAME_MAP[tag as CourseScheduleItemDto['tag']] || tag\n              }`}</Tag>\n            ))}\n          </Col>\n          <Col flex=\"none\">\n            <Button size=\"small\" onClick={onClearAllButtonClick}>\n              Clear all\n            </Button>\n          </Col>\n        </Row>\n      ) : null}\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Schedule/components/FilteredTags/index.tsx",
    "content": "import { FilteredTags } from './FilteredTags';\n\nexport default FilteredTags;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/MobileItemCard/MobileItemCard.tsx",
    "content": "import { CourseScheduleItemDto } from '@client/api';\nimport { Row, Col, Typography } from 'antd';\nimport { SwapRightOutlined } from '@ant-design/icons';\nimport { coloredDateRenderer } from '@client/shared/components/Table';\nimport { renderTagWithStyle, statusRenderer } from '../TableView/renderers';\nimport Link from 'next/link';\nimport { ScheduleSettings } from '@client/modules/Schedule/hooks/useScheduleSettings';\nimport dayjs from 'dayjs';\n\nconst { Title } = Typography;\n\nexport const MobileItemCard = ({\n  item,\n  timezone,\n}: {\n  item: CourseScheduleItemDto;\n  timezone: ScheduleSettings['timezone'];\n}) => {\n  const timezoneOffset = `(UTC ${dayjs().tz(timezone).format('Z')})`;\n  return (\n    <div style={{ padding: '12px 0px', borderBottom: '1px groove' }}>\n      <Row gutter={12} wrap={false}>\n        <Col flex=\"1\">\n          <Link href={item.descriptionUrl ? item.descriptionUrl : ''} target=\"_blank\">\n            <Title level={5}>{item.name}</Title>\n          </Link>\n        </Col>\n        <Col flex=\"0 0 auto\">{renderTagWithStyle(item.tag)}</Col>\n      </Row>\n      <Row gutter={12} wrap={false}>\n        <Col flex=\"1\">{statusRenderer(item.status)}</Col>\n        <Col flex=\"0 0 auto\">\n          {coloredDateRenderer(timezone, 'MMM D HH:mm', 'start', 'Recommended date for studying')(item.startDate, item)}\n          {item.endDate && (\n            <>\n              {' '}\n              <SwapRightOutlined />{' '}\n            </>\n          )}\n          {item.endDate &&\n            coloredDateRenderer(timezone, 'MMM D HH:mm', 'end', 'Recommended date for studying')(item.endDate, item)}\n        </Col>\n      </Row>\n      <Row justify={'end'} color=\"secondary\">\n        {timezoneOffset}\n      </Row>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Schedule/components/MobileItemCard/index.tsx",
    "content": "export * from './MobileItemCard';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsDrawer/ChangeTagColors.tsx",
    "content": "import { BgColorsOutlined } from '@ant-design/icons';\nimport { ColorPicker, Space, Tag } from 'antd';\nimport { CourseScheduleItemDtoTagEnum } from '@client/api';\nimport { TAG_NAME_MAP } from '@client/modules/Schedule/constants';\nimport { getTagStyle } from '@client/modules/Schedule/utils';\nimport React from 'react';\nimport type { Color } from 'antd/es/color-picker';\nimport SettingsItem from '@client/components/SettingsItem';\n\ninterface ChangeTagColorProps {\n  tags: CourseScheduleItemDtoTagEnum[];\n  tagColors: Record<string, string>;\n  setTagColors: (value: Record<string, string>) => void;\n}\n\nconst ChangeTagColors: React.FC<ChangeTagColorProps> = ({ tagColors, setTagColors, tags }) => {\n  const changeColor = (colorState: Color, tag: CourseScheduleItemDtoTagEnum) =>\n    setTagColors({ ...tagColors, [tag]: colorState.toHexString() });\n\n  return (\n    <SettingsItem header=\"Change Tag Colors\" IconComponent={BgColorsOutlined}>\n      <Space direction=\"vertical\">\n        {tags.map(tag => (\n          <Space key={tag}>\n            <ColorPicker defaultValue={tagColors[tag]} onChange={value => changeColor(value, tag)} />\n            <Tag style={getTagStyle(tag, tagColors, { cursor: 'pointer' })}>{TAG_NAME_MAP[tag] ?? tag}</Tag>\n          </Space>\n        ))}\n      </Space>\n    </SettingsItem>\n  );\n};\n\nexport default ChangeTagColors;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsDrawer/SettingsDrawer.tsx",
    "content": "import { SettingOutlined } from '@ant-design/icons';\nimport { Button, Drawer, Tooltip } from 'antd';\nimport { useState } from 'react';\nimport { ScheduleSettings } from '@client/modules/Schedule/hooks/useScheduleSettings';\nimport ChangeTagColors from './ChangeTagColors';\nimport ShowTableColumns from './ShowTableColumns';\nimport TimeZone from './TimeZone';\nimport { CourseScheduleItemDtoTagEnum } from '@client/api';\n\ninterface SettingsDrawerProps {\n  settings: ScheduleSettings;\n  tags: CourseScheduleItemDtoTagEnum[];\n}\n\nconst TITLE = 'Schedule settings';\n\nexport function SettingsDrawer({ settings, tags }: SettingsDrawerProps) {\n  const [opened, setOpened] = useState(false);\n\n  const openDrawer = () => setOpened(true);\n  const closeDrawer = () => setOpened(false);\n\n  return (\n    <>\n      <Tooltip title=\"Table settings\" placement=\"left\">\n        <Button\n          data-testid=\"Settings\"\n          title=\"Settings\"\n          shape=\"circle\"\n          type=\"text\"\n          icon={<SettingOutlined style={{ fontSize: '2.5ch' }} />}\n          onClick={openDrawer}\n        />\n      </Tooltip>\n      <Drawer title={TITLE} placement=\"right\" closable onClose={closeDrawer} open={opened}>\n        <TimeZone timezone={settings.timezone} setTimezone={settings.setTimezone} />\n        <ShowTableColumns columnsHidden={settings.columnsHidden} setColumnsHidden={settings.setColumnsHidden} />\n        <ChangeTagColors tags={tags} tagColors={settings.tagColors} setTagColors={settings.setTagColors} />\n      </Drawer>\n    </>\n  );\n}\n\nexport default SettingsDrawer;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsDrawer/ShowTableColumns.tsx",
    "content": "import { Typography, Checkbox } from 'antd';\nimport { CheckboxChangeEvent } from 'antd/lib/checkbox';\nimport { useCallback } from 'react';\nimport { FilterOutlined } from '@ant-design/icons';\nimport { COLUMNS, CONFIGURABLE_COLUMNS } from '../../constants';\nimport SettingsItem from '@client/components/SettingsItem';\n\nconst { Text } = Typography;\n\nconst AVAILABLE_COLUMNS = COLUMNS.filter(column => CONFIGURABLE_COLUMNS.includes(column.key));\n\ninterface ShowTableColumnsProps {\n  columnsHidden: string[];\n  setColumnsHidden: (value: string[]) => void;\n}\n\nexport function ShowTableColumns({ columnsHidden, setColumnsHidden }: ShowTableColumnsProps) {\n  const toggleColumnCheckbox = useCallback(\n    ({ target: { checked, value: selectedColumn } }: CheckboxChangeEvent) => {\n      setColumnsHidden(\n        checked ? columnsHidden.filter(column => column !== selectedColumn) : [...columnsHidden, selectedColumn],\n      );\n    },\n    [columnsHidden, setColumnsHidden],\n  );\n\n  return (\n    <SettingsItem header=\"Table columns\" IconComponent={FilterOutlined}>\n      <div style={{ marginBottom: 10 }}>\n        <Text>Visible Columns</Text>\n      </div>\n      {AVAILABLE_COLUMNS.map(({ key, name }) => (\n        <div key={key} style={{ marginBottom: 10 }}>\n          <Checkbox\n            value={key}\n            checked={!columnsHidden.includes(key)}\n            onChange={toggleColumnCheckbox}\n            style={{ userSelect: 'none' }}\n          >\n            {name}\n          </Checkbox>\n        </div>\n      ))}\n    </SettingsItem>\n  );\n}\n\nexport default ShowTableColumns;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsDrawer/TimeZone.tsx",
    "content": "import { Typography, Select } from 'antd';\nimport { FieldTimeOutlined } from '@ant-design/icons';\nimport { ALL_TIMEZONES } from '@client/configs/timezones';\nimport SettingsItem from '@client/components/SettingsItem';\n\nconst { Paragraph, Title } = Typography;\nconst { Option } = Select;\n\ninterface TimeZoneProps {\n  timezone: string;\n  setTimezone: (value: string) => void;\n}\n\nexport function TimeZone({ timezone, setTimezone }: TimeZoneProps) {\n  return (\n    <SettingsItem header=\"Time zone\" IconComponent={FieldTimeOutlined}>\n      <Title level={5}>Time zone</Title>\n      <Paragraph>Manage region-specific options for the schedule.</Paragraph>\n      <Select\n        style={{ width: 200 }}\n        placeholder=\"Please select a timezone\"\n        filterOption={(input: string, option) => (option?.value as string)?.toLowerCase().includes(input.toLowerCase())}\n        defaultValue={timezone}\n        onChange={setTimezone}\n        showSearch\n      >\n        {ALL_TIMEZONES.map(timezone => (\n          <Option key={timezone} value={timezone}>\n            {/* there is no 'Europe / Kyiv' timezone at the moment */}\n            {timezone === 'Europe/Kiev' ? 'Europe/Kyiv' : timezone}\n          </Option>\n        ))}\n      </Select>\n    </SettingsItem>\n  );\n}\n\nexport default TimeZone;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsDrawer/index.ts",
    "content": "export { default as SettingsDrawer } from './SettingsDrawer';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsPanel/SettingsPanel.test.tsx",
    "content": "import { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { SettingsButtons, SettingsPanel, SettingsPanelProps } from '.';\n\nconst PROPS_MOCK: SettingsPanelProps = {\n  isCourseManager: true,\n  courseId: 1,\n  courseAlias: 'course-alias',\n  settings: {\n    timezone: 'Region/Town',\n  } as SettingsPanelProps['settings'],\n  calendarToken: 'calendar-token',\n  tags: [],\n  onCreateCourseTask: vi.fn(),\n  onCopyFromCourse: vi.fn(),\n  onCreateCourseEvent: vi.fn(),\n  refreshData: vi.fn(),\n};\n\ndescribe('SettingsPanel', () => {\n  it.each`\n    button\n    ${'Event'}\n    ${'Task'}\n    ${'Settings'}\n    ${'More'}\n  `('should render \"$button\" button', ({ button }: { button: string }) => {\n    render(<SettingsPanel {...PROPS_MOCK} />);\n\n    const settingsBtn = screen.getByTestId(button);\n\n    expect(settingsBtn).toBeInTheDocument();\n  });\n\n  it.each`\n    button\n    ${'Event'}\n    ${'Task'}\n  `('should not render \"$button\" button when user is not a course manager', ({ button }: { button: string }) => {\n    render(<SettingsPanel {...PROPS_MOCK} isCourseManager={false} />);\n\n    const moreBtn = screen.queryByText(button);\n\n    expect(moreBtn).not.toBeInTheDocument();\n  });\n\n  it('should not render \"More\" button when user is not a course manager and calendar token was not provided', () => {\n    render(<SettingsPanel {...PROPS_MOCK} isCourseManager={false} calendarToken={''} />);\n\n    const moreBtn = screen.queryByText(SettingsButtons.More);\n\n    expect(moreBtn).not.toBeInTheDocument();\n  });\n\n  it.each`\n    item                        | prop                 | condition\n    ${SettingsButtons.CopyLink} | ${'calendarToken'}   | ${'calendar token was not provided'}\n    ${SettingsButtons.Download} | ${'calendarToken'}   | ${'calendar token was not provided'}\n    ${SettingsButtons.Export}   | ${'isCourseManager'} | ${'user is not a course manager'}\n    ${SettingsButtons.Copy}     | ${'isCourseManager'} | ${'user is not a course manager'}\n  `(\n    'should not render additional action \"$item\" when $condition',\n    async ({ item, prop }: { item: string; prop: keyof SettingsPanelProps }) => {\n      const props = { ...PROPS_MOCK, [prop]: null };\n      render(<SettingsPanel {...props} />);\n\n      const moreBtn = screen.getByRole('button', { name: /more/i });\n      fireEvent.click(moreBtn);\n\n      await waitFor(() => {\n        const menuItem = screen.queryByRole('menuitem', { name: new RegExp(item, 'i') });\n        expect(menuItem).not.toBeInTheDocument();\n      });\n    },\n  );\n});\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsPanel/SettingsPanel.tsx",
    "content": "import { Button, Col, Row } from 'antd';\nimport {\n  CalendarOutlined,\n  CloudDownloadOutlined,\n  CopyOutlined,\n  FileExcelOutlined,\n  PlusOutlined,\n} from '@ant-design/icons';\nimport { CourseScheduleItemDtoTagEnum } from '@client/api';\nimport { ScheduleSettings } from '@client/modules/Schedule/hooks/useScheduleSettings';\nimport { useMemo } from 'react';\nimport { SettingsDrawer } from '../SettingsDrawer';\nimport { AdditionalActions } from '../AdditionalActions';\nimport { buildMenuItem } from './helpers';\n\nexport interface SettingsPanelProps {\n  isCourseManager: boolean;\n  courseId: number;\n  courseAlias: string;\n  settings: ScheduleSettings;\n  calendarToken: string;\n  mobileView?: boolean;\n  tags: CourseScheduleItemDtoTagEnum[];\n  refreshData: () => void;\n  onCreateCourseTask: () => void;\n  onCreateCourseEvent: () => void;\n  onCopyFromCourse: () => void;\n}\n\nexport enum SettingsButtons {\n  Event = 'Event',\n  Task = 'Task',\n  CopyLink = 'Copy iCal Link',\n  Download = 'Download iCal',\n  Export = 'Export Excel',\n  Copy = 'Copy from',\n  More = 'More',\n}\n\nexport function SettingsPanel({\n  isCourseManager,\n  onCreateCourseTask,\n  onCreateCourseEvent,\n  onCopyFromCourse,\n  courseId,\n  courseAlias,\n  settings,\n  calendarToken,\n  tags,\n  mobileView,\n}: SettingsPanelProps) {\n  const additionalMenuItems = useMemo(\n    () =>\n      [\n        buildMenuItem(SettingsButtons.CopyLink, <CalendarOutlined />, !!calendarToken),\n        buildMenuItem(SettingsButtons.Download, <CloudDownloadOutlined />, !!calendarToken),\n        buildMenuItem(SettingsButtons.Export, <FileExcelOutlined />, isCourseManager),\n        buildMenuItem(SettingsButtons.Copy, <CopyOutlined />, isCourseManager),\n      ].filter(Boolean),\n    [calendarToken, isCourseManager],\n  );\n\n  return (\n    <Row justify=\"end\" gutter={16}>\n      {isCourseManager && !mobileView ? (\n        <Col>\n          <Button\n            data-testid={SettingsButtons.Event}\n            type=\"primary\"\n            icon={<PlusOutlined />}\n            onClick={onCreateCourseEvent}\n          >\n            {SettingsButtons.Event}\n          </Button>\n        </Col>\n      ) : null}\n      {isCourseManager && !mobileView ? (\n        <Col>\n          <Button\n            data-testid={SettingsButtons.Task}\n            type=\"primary\"\n            icon={<PlusOutlined />}\n            onClick={onCreateCourseTask}\n          >\n            {SettingsButtons.Task}\n          </Button>\n        </Col>\n      ) : null}\n      {additionalMenuItems?.length !== 0 && (\n        <Col>\n          <AdditionalActions\n            menuItems={additionalMenuItems}\n            courseId={courseId}\n            timezone={settings.timezone}\n            calendarToken={calendarToken}\n            courseAlias={courseAlias}\n            onCopyFromCourse={onCopyFromCourse}\n          />\n        </Col>\n      )}\n      <Col>\n        <SettingsDrawer tags={tags} settings={settings} />\n      </Col>\n    </Row>\n  );\n}\n\nexport default SettingsPanel;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsPanel/helpers.ts",
    "content": "import { MenuItemType } from '../AdditionalActions';\n\nexport const buildMenuItem = (title: string, icon: React.ReactNode, isVisible: boolean): MenuItemType | null =>\n  isVisible\n    ? {\n        key: title,\n        label: title,\n        icon: icon,\n      }\n    : null;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/SettingsPanel/index.ts",
    "content": "export { default as SettingsPanel } from './SettingsPanel';\nexport { SettingsButtons, type SettingsPanelProps } from './SettingsPanel';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/StatusTabs/StatusTabs.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport StatusTabs, { Status } from './StatusTabs';\nimport { ALL_TAB_KEY, SCHEDULE_STATUSES } from '@client/modules/Schedule/constants';\nimport { CourseScheduleItemDtoStatusEnum } from '@client/api';\n\nconst StatusEnum = CourseScheduleItemDtoStatusEnum;\n\ndescribe('StatusTabs', () => {\n  const onTabChangeMock = vi.fn();\n\n  it('should render status tabs', () => {\n    const statuses = generateStatuses();\n\n    render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n    const expectedStatusCount = SCHEDULE_STATUSES.length + 1; // +1 is for 'All' tab\n    expect(screen.getAllByRole('tab')).toHaveLength(expectedStatusCount);\n  });\n\n  it('should render status tabs when statuses were not provided', () => {\n    render(<StatusTabs statuses={[]} onTabChange={onTabChangeMock} />);\n\n    const expectedStatusCount = SCHEDULE_STATUSES.length + 1; // +1 is for 'All' tab\n    expect(screen.getAllByRole('tab')).toHaveLength(expectedStatusCount);\n  });\n\n  it.each`\n    activeTab\n    ${undefined}\n    ${[]}\n    ${['missed']}\n  `(\n    'should render \"all\" tab selected by default when active tab is $activeTab',\n    ({ activeTab }: { activeTab: string }) => {\n      render(<StatusTabs activeTab={activeTab} statuses={[]} onTabChange={onTabChangeMock} />);\n\n      // Rule is disabled because the tab with text \"all\" is nested inside div with \"active\" class\n      // eslint-disable-next-line testing-library/no-node-access\n      const allTab = screen.getByText(/all/i).parentElement;\n      expect(allTab).toHaveClass('ant-tabs-tab-active');\n    },\n  );\n\n  it('should order tabs', () => {\n    const statuses = generateStatuses();\n\n    render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n    const [all, available, review, future, missed, done, registered, unAvailable, archived] =\n      screen.getAllByRole('tab');\n    expect(all).toHaveTextContent(new RegExp(ALL_TAB_KEY, 'i'));\n    expect(available).toHaveTextContent(new RegExp(StatusEnum.Available, 'i'));\n    expect(review).toHaveTextContent(new RegExp(StatusEnum.Review, 'i'));\n    expect(future).toHaveTextContent(new RegExp(StatusEnum.Future, 'i'));\n    expect(missed).toHaveTextContent(new RegExp(StatusEnum.Missed, 'i'));\n    expect(done).toHaveTextContent(new RegExp(StatusEnum.Done, 'i'));\n    expect(registered).toHaveTextContent(new RegExp(StatusEnum.Registered, 'i'));\n    expect(unAvailable).toHaveTextContent(new RegExp(StatusEnum.Unavailable, 'i'));\n    expect(archived).toHaveTextContent(new RegExp(StatusEnum.Archived, 'i'));\n  });\n\n  it.each`\n    status                  | count\n    ${StatusEnum.Done}      | ${2}\n    ${StatusEnum.Available} | ${3}\n    ${StatusEnum.Archived}  | ${4}\n    ${StatusEnum.Future}    | ${5}\n    ${StatusEnum.Review}    | ${6}\n  `(\n    'should render badge with count of $count for \"$status\" tab',\n    ({ status, count }: { status: string; count: number }) => {\n      const missedCount = 1;\n      const statuses = generateStatuses(undefined, { [StatusEnum.Missed]: missedCount, [status]: count });\n\n      render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n      expect(screen.getByText(missedCount)).toBeInTheDocument();\n      expect(screen.getByText(count)).toBeInTheDocument();\n    },\n  );\n\n  it('should render badge with total count for \"all\" tab', () => {\n    const missedCount = 1;\n    const doneCount = 2;\n    const reviewCount = 3;\n    const statuses = generateStatuses(undefined, {\n      [StatusEnum.Missed]: missedCount,\n      [StatusEnum.Done]: doneCount,\n      [StatusEnum.Review]: reviewCount,\n    });\n\n    render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n    const totalCount = missedCount + doneCount + reviewCount;\n    expect(screen.getByText(totalCount)).toBeInTheDocument();\n  });\n\n  describe('when active tab was changed', () => {\n    it('should call onTabChange with tab name \"all\"', () => {\n      const statuses = generateStatuses();\n      render(<StatusTabs activeTab={StatusEnum.Missed} statuses={statuses} onTabChange={onTabChangeMock} />);\n\n      const selectedTab = screen.getByText(new RegExp(ALL_TAB_KEY, 'i'));\n      fireEvent.click(selectedTab);\n\n      expect(onTabChangeMock).toHaveBeenCalledWith(ALL_TAB_KEY);\n    });\n\n    it.each`\n      tabName\n      ${StatusEnum.Missed}\n      ${StatusEnum.Done}\n      ${StatusEnum.Available}\n      ${StatusEnum.Archived}\n      ${StatusEnum.Future}\n      ${StatusEnum.Review}\n      ${StatusEnum.Registered}\n      ${StatusEnum.Unavailable}\n    `('should call onTabChange with tab name \"$tabName\"', ({ tabName }: { tabName: string }) => {\n      const statuses = generateStatuses(undefined, { [tabName]: 2 });\n      render(<StatusTabs statuses={statuses} onTabChange={onTabChangeMock} />);\n\n      const selectedTab = screen.getByText(new RegExp(`^${tabName}$`, 'i'));\n      fireEvent.click(selectedTab);\n\n      expect(onTabChangeMock).toHaveBeenCalledWith(tabName);\n    });\n  });\n});\n\nfunction generateStatuses(count = 3, statusTypeAndCount: Record<string, number> | null = null): Status[] {\n  if (statusTypeAndCount) {\n    const statuses: Status[] = [];\n\n    for (const statusType in statusTypeAndCount) {\n      if (Object.prototype.hasOwnProperty.call(statusTypeAndCount, statusType)) {\n        const statusCount = statusTypeAndCount[statusType];\n        statuses.push(...new Array(statusCount).fill(statusType));\n      }\n    }\n\n    return statuses;\n  }\n\n  return new Array(count).fill('').map(() => StatusEnum.Missed);\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/components/StatusTabs/StatusTabs.tsx",
    "content": "import { Col, Row, Tabs } from 'antd';\nimport { FC, useMemo, PropsWithChildren } from 'react';\nimport { ALL_TAB_KEY } from '@client/modules/Schedule/constants';\nimport { tabsRenderer } from './renderers';\n\nexport type Status = string;\n\ntype StatusTabsProps = PropsWithChildren & {\n  statuses: Status[];\n  activeTab?: string;\n  onTabChange: (tab: string) => void;\n  mobileView?: boolean;\n};\n\nconst StatusTabs: FC<StatusTabsProps> = ({ statuses, activeTab, onTabChange, children, mobileView }) => {\n  const tabs = useMemo(() => tabsRenderer(statuses, activeTab), [statuses, activeTab]);\n\n  const handleTabChange = (selectedTab: string) => {\n    onTabChange(selectedTab);\n  };\n\n  const getActiveTab = () => (!activeTab || Array.isArray(activeTab) ? ALL_TAB_KEY : activeTab);\n\n  return (\n    <Row gutter={mobileView ? 0 : 48}>\n      <Col span={24}>\n        <Tabs\n          tabBarStyle={{ marginBottom: 0 }}\n          activeKey={getActiveTab()}\n          items={tabs}\n          onChange={handleTabChange}\n          tabBarExtraContent={children}\n        />\n      </Col>\n    </Row>\n  );\n};\n\nexport default StatusTabs;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/StatusTabs/index.ts",
    "content": "export { default as StatusTabs } from './StatusTabs';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/StatusTabs/renderers.tsx",
    "content": "import { CourseScheduleItemDtoStatusEnum } from '@client/api';\nimport { ALL_TAB_KEY, ALL_TAB_LABEL, SCHEDULE_STATUSES } from '@client/modules/Schedule/constants';\nimport { Status } from './StatusTabs';\nimport { LabelItem, tabRenderer } from '@client/components/TabsWithCounter/renderers';\n\nconst tabsOrder = [\n  ALL_TAB_KEY,\n  CourseScheduleItemDtoStatusEnum.Available,\n  CourseScheduleItemDtoStatusEnum.Review,\n  CourseScheduleItemDtoStatusEnum.Future,\n  CourseScheduleItemDtoStatusEnum.Missed,\n  CourseScheduleItemDtoStatusEnum.Done,\n  CourseScheduleItemDtoStatusEnum.Registered,\n  CourseScheduleItemDtoStatusEnum.Unavailable,\n  CourseScheduleItemDtoStatusEnum.Archived,\n];\n\nexport const tabsRenderer = (statuses: Status[], activeTab?: string) => {\n  const initialItem = {\n    label: ALL_TAB_LABEL,\n    key: ALL_TAB_KEY,\n    count: statuses.length,\n  };\n\n  return SCHEDULE_STATUSES.reduce<LabelItem[]>(\n    (acc, current) => {\n      const { text, value } = current;\n\n      const newItem = {\n        label: text,\n        key: value,\n        count: statuses.filter(el => el === value).length,\n      };\n      return [...acc, newItem];\n    },\n    [initialItem],\n  )\n    .sort((prev, next) => tabsOrder.indexOf(prev.key) - tabsOrder.indexOf(next.key))\n    .map(item => tabRenderer(item, activeTab));\n};\n"
  },
  {
    "path": "client/src/modules/Schedule/components/TableView/TableView.test.tsx",
    "content": "import { fireEvent, render, screen, within } from '@testing-library/react';\nimport TableView from './TableView';\nimport * as ReactUse from 'react-use';\nimport { ALL_TAB_KEY, ColumnKey, ColumnName } from '@client/modules/Schedule/constants';\nimport {\n  CourseScheduleItemDto,\n  CourseScheduleItemDtoStatusEnum as StatusEnum,\n  CourseScheduleItemDtoTagEnum as TagsEnum,\n  CourseScheduleItemDtoTypeEnum,\n} from '@client/api';\nimport { ScheduleSettings } from '@client/modules/Schedule/hooks/useScheduleSettings';\n\nconst PROPS_SETTINGS_MOCK: ScheduleSettings = {\n  timezone: 'Europe/Moscow',\n  setTimezone: vi.fn(),\n  tagColors: {\n    coding: '#722ed1',\n    'cross-check': '#13c2c2',\n  },\n  setTagColors: vi.fn(),\n  columnsHidden: [],\n  setColumnsHidden: vi.fn(),\n  tagsHidden: [],\n  setTagsHidden: vi.fn(),\n};\n\ndescribe('TableView', () => {\n  it.each`\n    label\n    ${ColumnName.Status}\n    ${ColumnName.Name}\n    ${ColumnName.Type}\n    ${ColumnName.Organizer}\n    ${ColumnName.Weight}\n    ${ColumnName.Score}\n    ${'End Date (UTC +03:00)'}\n    ${'Start Date (UTC +03:00)'}\n  `('should render column \"$label\"', ({ label }: { label: string }) => {\n    render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);\n\n    expect(screen.getByText(label)).toBeInTheDocument();\n  });\n\n  it.each`\n    value\n    ${'Course Item 0'}\n    ${'2020-02-02 00:00'}\n    ${'2020-03-15 23:59'}\n    ${'×0.2'}\n    ${'20 / 100'}\n    ${'Missed'}\n    ${'Test'}\n  `('should render data field \"$value\"', ({ value }: { value: string }) => {\n    render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);\n\n    const [dataField] = screen.getAllByText(value);\n    expect(dataField).toBeInTheDocument();\n  });\n\n  it('should not render hidden columns', () => {\n    const propsWithHiddenColumn: ScheduleSettings = {\n      ...PROPS_SETTINGS_MOCK,\n      columnsHidden: [ColumnKey.Type],\n    };\n    render(<TableView settings={propsWithHiddenColumn} data={generateCourseData()} />);\n\n    expect(screen.queryByText('Type')).not.toBeInTheDocument();\n  });\n\n  describe('should filter data', () => {\n    it('by selected tag', () => {\n      vi.spyOn(ReactUse, 'useLocalStorage')\n        // Mock useLocalStorage for combinedFilter\n        .mockReturnValueOnce([{ types: [TagsEnum.Test], statuses: [], tags: [] }, vi.fn(), vi.fn()]);\n      const data = generateCourseData();\n\n      render(<TableView settings={PROPS_SETTINGS_MOCK} data={data} />);\n\n      expect(screen.getAllByText('Test')).toHaveLength(data.length);\n    });\n\n    it.each`\n      initialStatus\n      ${ALL_TAB_KEY}\n      ${[]}\n    `('by \"all\" status when initial status is $initialStatus', ({ initialStatus }: { initialStatus: string }) => {\n      const data = generateCourseData();\n      render(<TableView settings={PROPS_SETTINGS_MOCK} data={data} statusFilter={initialStatus} />);\n\n      const items = screen.getAllByText(/Course Item/i);\n      expect(items).toHaveLength(data.length);\n    });\n\n    it.each`\n      status\n      ${StatusEnum.Missed}\n      ${StatusEnum.Done}\n      ${StatusEnum.Available}\n      ${StatusEnum.Archived}\n      ${StatusEnum.Future}\n      ${StatusEnum.Review}\n    `('by \"$status\" status', ({ status }: { status: StatusEnum }) => {\n      const courseItemCount = 2;\n      const data = generateCourseData(courseItemCount, [status, status]);\n      render(<TableView settings={PROPS_SETTINGS_MOCK} data={data} statusFilter={status} />);\n\n      const items = screen.getAllByText(new RegExp(status, 'i'));\n      expect(items).toHaveLength(courseItemCount);\n    });\n\n    it.each`\n      field                   | searchQuery\n      ${ColumnName.Name}      | ${'Course Item 0'}\n      ${ColumnName.Organizer} | ${'organizer 0'}\n    `('by \"$field\" column search', async ({ field, searchQuery }: { field: string; searchQuery: string }) => {\n      const data = generateCourseData();\n      render(<TableView settings={PROPS_SETTINGS_MOCK} data={data} />);\n      // Check that all items rendered\n      expect(screen.getAllByText(/Course Item/)).toHaveLength(data.length);\n      // Find and click search button for column\n      const columnHeader = screen.getByRole('columnheader', { name: new RegExp(field, 'i') });\n      const searchButton = within(columnHeader).getByRole('button', { name: /search/i });\n      fireEvent.click(searchButton);\n      // Type search query inside search input\n      const searchInput = await screen.findByRole('textbox');\n      fireEvent.change(searchInput, { target: { value: searchQuery } });\n\n      // Apply search\n      const inputSearchBtn = screen.getByRole('button', { name: /search search/i });\n      fireEvent.click(inputSearchBtn);\n\n      // Find the line with search query and no others\n      const item = await screen.findByText(searchQuery);\n      expect(item).toBeInTheDocument();\n      expect(screen.queryByText(data[1]?.name ?? '')).not.toBeInTheDocument();\n    });\n  });\n\n  describe('should hide data', () => {\n    it.each`\n      selectedStatus          | hiddenStatus\n      ${StatusEnum.Missed}    | ${StatusEnum.Done}\n      ${StatusEnum.Done}      | ${StatusEnum.Available}\n      ${StatusEnum.Available} | ${StatusEnum.Archived}\n      ${StatusEnum.Archived}  | ${StatusEnum.Future}\n      ${StatusEnum.Future}    | ${StatusEnum.Review}\n      ${StatusEnum.Review}    | ${StatusEnum.Missed}\n    `(\n      'when \"$selectedStatus\" was selected and \"$hiddenStatus\" was filtered',\n      ({ selectedStatus, hiddenStatus }: { selectedStatus: StatusEnum; hiddenStatus: StatusEnum }) => {\n        const data = generateCourseData(2, [selectedStatus, hiddenStatus]);\n        render(<TableView settings={PROPS_SETTINGS_MOCK} data={data} statusFilter={selectedStatus} />);\n\n        const filteredItem = screen.queryByText(new RegExp(hiddenStatus, 'i'));\n        expect(filteredItem).not.toBeInTheDocument();\n      },\n    );\n  });\n\n  it.each`\n    tag\n    ${TagsEnum.Coding}\n    ${TagsEnum.Test}\n    ${TagsEnum.Interview}\n  `('should check filters in dropdown when tag \"$tag\" was selected', async ({ tag }: { tag: string }) => {\n    vi.spyOn(ReactUse, 'useLocalStorage')\n      // Mock useLocalStorage for combinedFilter\n      .mockReturnValueOnce([\n        { types: [TagsEnum.Coding, TagsEnum.Test, TagsEnum.Interview], statuses: [], tags: [] },\n        vi.fn(),\n        vi.fn(),\n      ]);\n    render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);\n\n    const [, tagFilterBtn] = screen.getAllByRole('button', { name: /filter/i });\n    if (tagFilterBtn) {\n      fireEvent.click(tagFilterBtn);\n    }\n\n    const filtersDropdown = await screen.findByRole('menu');\n    const menuItem = within(filtersDropdown).getByRole('menuitem', { name: new RegExp(tag, 'i') });\n    const checkbox = within(menuItem).getByRole('checkbox');\n    expect(checkbox).toBeChecked();\n  });\n\n  it('should not render filtered tags when tags is empty', () => {\n    vi.spyOn(ReactUse, 'useLocalStorage')\n      // Mock useLocalStorage for combinedFilter\n      .mockReturnValueOnce([{ tags: [] }, vi.fn(), vi.fn()]);\n    render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);\n\n    const tag = screen.queryByText(/Type: /);\n\n    expect(tag).not.toBeInTheDocument();\n  });\n\n  it('should remove tags when \"Clear all\" button was clicked', async () => {\n    const setFilterMock = vi.fn();\n    const types = [TagsEnum.Coding, TagsEnum.Test, TagsEnum.Interview];\n    vi.spyOn(ReactUse, 'useLocalStorage')\n      // Mock useLocalStorage for combinedFilter\n      .mockReturnValueOnce([\n        {\n          types,\n          statuses: [],\n          tags: types.map(t => ({ label: `${ColumnName.Type}: ${t}`, value: t, tagType: ColumnName.Type })),\n        },\n        setFilterMock,\n        vi.fn(),\n      ]);\n    render(<TableView settings={PROPS_SETTINGS_MOCK} data={generateCourseData()} />);\n\n    const clearAllBtn = screen.getByText(/Clear all/);\n    fireEvent.click(clearAllBtn);\n\n    expect(setFilterMock).toHaveBeenCalledWith({ types: [], statuses: [], tags: [] });\n  });\n});\n\nfunction generateCourseData(\n  count = 3,\n  statusMock: StatusEnum[] = [StatusEnum.Missed, StatusEnum.Archived, StatusEnum.Done],\n): CourseScheduleItemDto[] {\n  return new Array(count).fill({}).map((_, idx) => {\n    return {\n      id: idx,\n      type: CourseScheduleItemDtoTypeEnum.CourseTask,\n      name: `Course Item ${idx}`,\n      startDate: '2020-02-01T21:00:00.000Z',\n      endDate: '2020-03-15T20:59:00.000Z',\n      maxScore: idx + 100,\n      scoreWeight: 0.2,\n      organizer: {\n        id: idx,\n        name: '',\n        githubId: `organizer ${idx}`,\n      },\n      status: statusMock[idx] ?? StatusEnum.Done,\n      score: idx + 20,\n      tag: 'test',\n      descriptionUrl: '',\n      crossCheckEndDate: '2020-02-01T21:00:00.000Z',\n    };\n  });\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/components/TableView/TableView.tsx",
    "content": "import { Col, Form, Row, Table, message } from 'antd';\nimport { ColumnsType } from 'antd/lib/table';\nimport { CourseScheduleItemDto } from '@client/api';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport {\n  coloredDateRenderer,\n  dateSorter,\n  getColumnSearchProps,\n  renderTask,\n  scoreRenderer,\n  weightRenderer,\n} from '@client/shared/components/Table';\nimport FilteredTags from '@client/modules/Schedule/components/FilteredTags';\nimport {\n  ALL_TAB_KEY,\n  ColumnKey,\n  ColumnName,\n  CONFIGURABLE_COLUMNS,\n  LocalStorageKeys,\n  SCHEDULE_STATUSES,\n  TAG_NAME_MAP,\n  TAGS,\n} from '@client/modules/Schedule/constants';\nimport { ScheduleSettings } from '@client/modules/Schedule/hooks/useScheduleSettings';\nimport { useMemo, useState, useEffect } from 'react';\nimport { useLocalStorage } from 'react-use';\nimport dayjs from 'dayjs';\nimport { statusRenderer, renderTagWithStyle, renderStatusWithStyle } from './renderers';\nimport { FilterValue } from 'antd/lib/table/interface';\nimport utc from 'dayjs/plugin/utc';\nimport timezone from 'dayjs/plugin/timezone';\nimport { capitalize } from 'lodash';\nimport { MobileItemCard } from '../MobileItemCard';\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\nconst getColumns = ({\n  timezone,\n  tagColors,\n  combinedFilter,\n  filteredInfo,\n  currentTabKey,\n}: {\n  combinedFilter: CombinedFilter;\n  timezone: string;\n  tagColors: Record<string, string>;\n  filteredInfo: Record<string, FilterValue | null>;\n  currentTabKey: string;\n}): ColumnsType<CourseScheduleItemDto> => {\n  const timezoneOffset = `(UTC ${dayjs().tz(timezone).format('Z')})`;\n  const { types, statuses } = combinedFilter;\n  return [\n    {\n      key: ColumnKey.Status,\n      title: ColumnName.Status,\n      dataIndex: 'status',\n      render: statusRenderer,\n      ...(currentTabKey === ALL_TAB_KEY && {\n        filters: SCHEDULE_STATUSES.map(({ value }) => ({ text: renderStatusWithStyle(value), value })),\n        defaultFilteredValue: statuses,\n        filtered: statuses?.length > 0,\n        filteredValue: statuses || null,\n      }),\n    },\n    {\n      key: ColumnKey.Name,\n      title: ColumnName.Name,\n      dataIndex: 'name',\n      render: (name, item) => renderTask(name, item.descriptionUrl),\n      filteredValue: filteredInfo.name || null,\n      ...getColumnSearchProps('name'),\n    },\n    {\n      key: ColumnKey.Type,\n      title: ColumnName.Type,\n      dataIndex: 'tag',\n      render: (tag: CourseScheduleItemDto['tag']) => renderTagWithStyle(tag, tagColors),\n      filters: TAGS.map(status => ({ text: renderTagWithStyle(status.value, tagColors), value: status.value })),\n      defaultFilteredValue: types,\n      filtered: types?.length > 0,\n      filteredValue: types || null,\n    },\n    {\n      key: ColumnKey.StartDate,\n      title: (\n        <span>\n          {ColumnName.StartDate} {timezoneOffset}\n        </span>\n      ),\n      dataIndex: 'startDate',\n      render: coloredDateRenderer(timezone, 'YYYY-MM-DD HH:mm', 'start', 'Recommended date for studying'),\n      sorter: dateSorter('startDate'),\n      sortDirections: ['descend', 'ascend'],\n    },\n    {\n      key: ColumnKey.EndDate,\n      title: (\n        <span>\n          {ColumnName.EndDate} {timezoneOffset}\n        </span>\n      ),\n      dataIndex: 'endDate',\n      render: coloredDateRenderer(timezone, 'YYYY-MM-DD HH:mm', 'end', 'Recommended deadline'),\n      sorter: dateSorter('endDate'),\n      sortDirections: ['descend', 'ascend'],\n    },\n    {\n      key: ColumnKey.Organizer,\n      title: ColumnName.Organizer,\n      dataIndex: ['organizer', 'githubId'],\n      render: (value: string) => !!value && <GithubUserLink value={value} />,\n      filteredValue: filteredInfo.organizer || null,\n      ...getColumnSearchProps('organizer.githubId'),\n    },\n    {\n      key: ColumnKey.Weight,\n      title: ColumnName.Weight,\n      dataIndex: ['scoreWeight'],\n      render: weightRenderer,\n      align: 'right',\n    },\n    {\n      key: ColumnKey.Score,\n      title: ColumnName.Score,\n      render: scoreRenderer,\n      align: 'right',\n    },\n  ];\n};\n\nexport interface TableViewProps {\n  settings: ScheduleSettings;\n  data: CourseScheduleItemDto[];\n  statusFilter?: string;\n  mobileView?: boolean;\n}\n\nexport type CombinedFilter = {\n  types: string[];\n  statuses: string[];\n\n  tags?: FilterTag[];\n};\n\nexport type FilterTag = {\n  label: string;\n  value: string;\n  tagType: ColumnName.Type | ColumnName.Status;\n};\n\nconst hasStatusFilter = (statusFilter?: string, itemStatus?: string) =>\n  Array.isArray(statusFilter) || statusFilter === ALL_TAB_KEY || itemStatus === statusFilter;\n\nexport function TableView({ data, settings, statusFilter = ALL_TAB_KEY, mobileView }: TableViewProps) {\n  const [form] = Form.useForm();\n  const [filteredInfo, setFilteredInfo] = useState<Record<string, FilterValue | string[] | null>>({});\n  const [combinedFilter = { types: [], statuses: [], tags: [] }, setCombinedFilter] = useLocalStorage<CombinedFilter>(\n    LocalStorageKeys.Filters,\n  );\n\n  useEffect(() => {\n    if (statusFilter !== ALL_TAB_KEY && combinedFilter.statuses.length) {\n      const tags = combinedFilter.tags?.filter(({ tagType }) => tagType !== ColumnName.Status);\n      setCombinedFilter({ ...combinedFilter, statuses: [], tags });\n    }\n  }, [statusFilter]);\n\n  const filteredData = useMemo(() => {\n    return data\n      .filter(item => (hasStatusFilter(statusFilter, item.status) ? item : null))\n      .filter(\n        item =>\n          (combinedFilter?.types?.length ? combinedFilter.types.includes(item.tag) : true) &&\n          (combinedFilter?.statuses?.length ? combinedFilter.statuses.includes(item.status) : true),\n      );\n  }, [combinedFilter, data, statusFilter]);\n\n  const filteredColumns = useMemo(\n    () =>\n      getColumns({\n        tagColors: settings.tagColors,\n        timezone: settings.timezone,\n        combinedFilter,\n        filteredInfo,\n        currentTabKey: statusFilter,\n      }).filter(column => {\n        const key = (column.key as ColumnKey) ?? ColumnKey.Name;\n        return CONFIGURABLE_COLUMNS.includes(key) ? !settings.columnsHidden.includes(key) : true;\n      }),\n    [settings.columnsHidden, settings.timezone, settings.tagColors, statusFilter, combinedFilter],\n  );\n  const columns = filteredColumns as ColumnsType<CourseScheduleItemDto>;\n\n  const handleTagClose = (removedTagLabel: string) => {\n    const tags = combinedFilter.tags?.filter(({ label }) => label !== removedTagLabel);\n    const removedTag = combinedFilter.tags?.find(({ label }) => label === removedTagLabel);\n\n    switch (removedTag?.tagType) {\n      case ColumnName.Type:\n        setCombinedFilter({\n          ...combinedFilter,\n          types: combinedFilter.types.filter(tag => tag !== removedTag.value),\n          tags,\n        });\n        break;\n      case ColumnName.Status:\n        setCombinedFilter({\n          ...combinedFilter,\n          statuses: combinedFilter.statuses.filter(status => status !== removedTag.value),\n          tags,\n        });\n        break;\n      default:\n        message.error('Unknown tag');\n        break;\n    }\n  };\n\n  const handleClearAllButtonClick = () => {\n    setCombinedFilter({ types: [], statuses: [], tags: [] });\n  };\n\n  const handleTableChange = (_: unknown, filters: Record<ColumnKey, FilterValue | string[] | null>) => {\n    const combinedFilter: CombinedFilter = {\n      types: filters.type?.map(tag => tag.toString()) ?? [],\n      statuses: filters.status?.map(status => status.toString()) ?? [],\n    };\n\n    combinedFilter.tags = [\n      ...combinedFilter.types.map(\n        (tag: string): FilterTag => ({\n          label: `${ColumnName.Type}: ${TAG_NAME_MAP[tag as CourseScheduleItemDto['tag']]}`,\n          value: tag,\n          tagType: ColumnName.Type,\n        }),\n      ),\n      ...combinedFilter.statuses.map(\n        (status: string): FilterTag => ({\n          label: `${ColumnName.Status}: ${capitalize(status)}`,\n          value: status,\n          tagType: ColumnName.Status,\n        }),\n      ),\n    ];\n\n    setCombinedFilter(combinedFilter);\n    setFilteredInfo(filters);\n  };\n\n  const generateUniqueRowKey = ({ id, name, tag }: CourseScheduleItemDto) => [id, name, tag].join('|');\n\n  return !mobileView ? (\n    <Row style={{ padding: '24px 0 0' }} gutter={32}>\n      <Col span={24}>\n        <Form form={form} component={false}>\n          <FilteredTags\n            tagFilters={combinedFilter.tags?.map(({ label }) => label) ?? []}\n            onTagClose={handleTagClose}\n            onClearAllButtonClick={handleClearAllButtonClick}\n          />\n          <Table\n            locale={{\n              // disable default tooltips on sortable columns\n              triggerDesc: undefined,\n              triggerAsc: undefined,\n              cancelSort: undefined,\n            }}\n            onChange={handleTableChange}\n            pagination={false}\n            dataSource={filteredData}\n            rowKey={generateUniqueRowKey}\n            size=\"middle\"\n            columns={columns}\n          />\n        </Form>\n      </Col>\n    </Row>\n  ) : (\n    <>\n      {filteredData.map(item => (\n        <MobileItemCard item={item} key={generateUniqueRowKey(item)} timezone={settings.timezone} />\n      ))}\n    </>\n  );\n}\n\nexport default TableView;\n"
  },
  {
    "path": "client/src/modules/Schedule/components/TableView/index.ts",
    "content": "export { default as TableView } from './TableView';\n"
  },
  {
    "path": "client/src/modules/Schedule/components/TableView/renderers.tsx",
    "content": "import { Badge, Tag } from 'antd';\nimport { CourseScheduleItemDto, CourseScheduleItemDtoStatusEnum } from '@client/api';\nimport capitalize from 'lodash/capitalize';\nimport { DEFAULT_TAG_COLOR_MAP, TAG_NAME_MAP } from '@client/modules/Schedule/constants';\nimport { getStatusStyle, getTagStyle, getTaskStatusColor } from '@client/modules/Schedule/utils';\n\nexport function statusRenderer(value: CourseScheduleItemDtoStatusEnum) {\n  const label = capitalize(value);\n  const color = getTaskStatusColor(value);\n\n  return <Badge color={color} text={label} />;\n}\n\nexport function renderStatusWithStyle(statusName: CourseScheduleItemDtoStatusEnum) {\n  return (\n    <Tag style={getStatusStyle(statusName)} key={statusName}>\n      {capitalize(statusName)}\n    </Tag>\n  );\n}\n\nexport function renderTagWithStyle(tagName: CourseScheduleItemDto['tag'], tagColors = DEFAULT_TAG_COLOR_MAP) {\n  return (\n    <Tag style={getTagStyle(tagName, tagColors)} key={tagName}>\n      {TAG_NAME_MAP[tagName] || tagName}\n    </Tag>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/constants.ts",
    "content": "import { CourseScheduleItemDto, CourseScheduleItemDtoStatusEnum, CourseScheduleItemDtoTagEnum } from '@client/api';\n\nexport enum ColumnName {\n  Status = 'Status',\n  StartDate = 'Start Date',\n  EndDate = 'End Date',\n  Time = 'Time',\n  Type = 'Type',\n  Name = 'Task / Event',\n  Organizer = 'Organizer',\n  Score = 'Score / Max',\n  Weight = 'Weight',\n}\n\nexport enum ColumnKey {\n  Status = 'status',\n  StartDate = 'studentStartDate',\n  EndDate = 'studentEndDate',\n  Type = 'type',\n  Name = 'name',\n  Organizer = 'organizer',\n  Score = 'score',\n  Weight = 'scoreWeight',\n}\n\nexport const COLUMNS: { key: ColumnKey; name: ColumnName }[] = [\n  { key: ColumnKey.Status, name: ColumnName.Status },\n  { key: ColumnKey.Name, name: ColumnName.Name },\n  { key: ColumnKey.StartDate, name: ColumnName.StartDate },\n  { key: ColumnKey.EndDate, name: ColumnName.EndDate },\n  { key: ColumnKey.Type, name: ColumnName.Type },\n  { key: ColumnKey.Organizer, name: ColumnName.Organizer },\n  { key: ColumnKey.Weight, name: ColumnName.Weight },\n  { key: ColumnKey.Score, name: ColumnName.Score },\n];\n\nexport enum LocalStorageKeys {\n  ViewMode = 'scheduleViewMode',\n  Timezone = 'scheduleTimezone',\n  IsSplittedByWeek = 'scheduleIsSplitedByWeek',\n  TagColors = 'scheduleTagsColors',\n  ColumnsHidden = 'scheduleColumnsHidden',\n  EventTypesHidden = 'scheduleEventTypesHidden',\n  StatusFilter = 'scheduleStatusFilter',\n  Filters = 'scheduleFilters',\n}\n\nexport const TAG_NAME_MAP: Record<CourseScheduleItemDto['tag'], string> = {\n  coding: 'Coding',\n  interview: 'Interview',\n  test: 'Test',\n  'cross-check-submit': 'Cross-Check: Submit',\n  'cross-check-review': 'Cross-Check: Review',\n  'self-study': 'Self-study',\n  lecture: 'Lecture',\n  'team-distribution': 'Team Distribution',\n};\n\nexport const SCHEDULE_STATUSES = Object.keys(CourseScheduleItemDtoStatusEnum).map(key => ({\n  value: (CourseScheduleItemDtoStatusEnum as any)[key] as CourseScheduleItemDtoStatusEnum,\n  text: key,\n}));\n\nexport const TAGS = Object.values(CourseScheduleItemDtoTagEnum).map((value: CourseScheduleItemDtoTagEnum) => ({\n  value: value,\n  text: TAG_NAME_MAP[value],\n}));\n\nexport const CONFIGURABLE_COLUMNS = [\n  ColumnKey.StartDate,\n  ColumnKey.EndDate,\n  ColumnKey.Type,\n  ColumnKey.Organizer,\n  ColumnKey.Score,\n  ColumnKey.Weight,\n];\n\nexport const DEADLINE_COLOR = '#ff0000';\nexport const DEFAULT_COLOR = '#308e00';\n\nexport const DEFAULT_TAG_COLOR_MAP: Record<CourseScheduleItemDto['tag'], string> = {\n  coding: '#722ed1',\n  interview: '#1677ff',\n  test: '#faad14',\n  'cross-check-submit': '#13c2c2',\n  'cross-check-review': '#36A836',\n  'self-study': '#595959',\n  lecture: '#eb2f96',\n  'team-distribution': '#308e00',\n};\n\nexport const SPECIAL_TASK_TYPES = {\n  deadline: 'deadline',\n  test: 'test',\n};\n\nexport const SPECIAL_ENTITY_TAGS = [\n  'optional',\n  'live',\n  'record',\n  'js',\n  'node.js',\n  'react',\n  'angular',\n  'css',\n  'html',\n  'git',\n  'markdown',\n  'mobile',\n  'kotlin',\n  'aws',\n  'jupyter',\n];\n\nexport const CHECKER_TYPES = {\n  'auto-test': 'Auto-Test',\n  mentor: 'Mentor',\n  assigned: 'Cross-Mentor',\n  taskOwner: 'Task Owner',\n  crossCheck: 'Cross-Check',\n};\n\nexport const ALL_TAB_KEY = 'all';\nexport const ALL_TAB_LABEL = 'All';\n"
  },
  {
    "path": "client/src/modules/Schedule/hooks/useScheduleSettings.ts",
    "content": "import { useMemo } from 'react';\nimport { useLocalStorage } from 'react-use';\nimport { DEFAULT_TAG_COLOR_MAP, LocalStorageKeys } from '@client/modules/Schedule/constants';\n\nexport interface ScheduleSettings {\n  timezone: string;\n  setTimezone: (value: string) => void;\n  tagColors: Record<string, string>;\n  setTagColors: (value: Record<string, string>) => void;\n  columnsHidden: string[];\n  setColumnsHidden: (value: string[]) => void;\n  tagsHidden: string[];\n  setTagsHidden: (value: string[]) => void;\n}\n\nexport const useScheduleSettings = (): ScheduleSettings => {\n  const [tagColors = DEFAULT_TAG_COLOR_MAP, setTagColors] = useLocalStorage<Record<string, string>>(\n    LocalStorageKeys.TagColors,\n  );\n  const [columnsHidden = [], setColumnsHidden] = useLocalStorage<string[]>(LocalStorageKeys.ColumnsHidden);\n  const [tagsHidden = [], setTagsHidden] = useLocalStorage<string[]>(LocalStorageKeys.EventTypesHidden);\n  const [timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, setTimezone] = useLocalStorage<string>(\n    LocalStorageKeys.Timezone,\n  );\n\n  return useMemo(\n    () => ({\n      timezone,\n      setTimezone,\n      tagColors,\n      setTagColors,\n      columnsHidden,\n      setColumnsHidden,\n      tagsHidden: tagsHidden,\n      setTagsHidden: setTagsHidden,\n    }),\n    [timezone, tagColors, columnsHidden, tagsHidden],\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Schedule/index.ts",
    "content": "export { getTaskStatusColor } from './utils';\n"
  },
  {
    "path": "client/src/modules/Schedule/pages/SchedulePage/index.tsx",
    "content": "import { Alert, theme } from 'antd';\nimport {\n  CourseDto,\n  CourseEventDto,\n  CoursesScheduleApi,\n  CoursesScheduleIcalApi,\n  CoursesTasksApi,\n  CreateCourseTaskDto,\n} from '@client/api';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { isCourseManager } from '@client/domain/user';\nimport uniq from 'lodash/uniq';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CoursesListModal } from '@client/modules/CourseManagement/components/CoursesListModal';\nimport { CourseTaskModal } from '@client/modules/CourseManagement/components/CourseTaskModal';\nimport { CourseEventModal } from '@client/modules/CourseManagement/components/CourseEventModal';\nimport { SettingsPanel } from '@client/modules/Schedule/components/SettingsPanel';\nimport { TableView } from '@client/modules/Schedule/components/TableView';\nimport { StatusTabs } from '@client/modules/Schedule/components/StatusTabs';\nimport { useScheduleSettings } from '@client/modules/Schedule/hooks/useScheduleSettings';\nimport { useContext, useMemo, useState } from 'react';\nimport { useAsyncRetry, useLocalStorage, useMedia } from 'react-use';\nimport { ALL_TAB_KEY, LocalStorageKeys } from '@client/modules/Schedule/constants';\n\nconst courseScheduleApi = new CoursesScheduleApi();\nconst coursesScheduleIcalApi = new CoursesScheduleIcalApi();\n\nconst courseTaskApi = new CoursesTasksApi();\n\nexport function SchedulePage() {\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n\n  const mobileView = useMedia('(max-width: 768px)');\n\n  const [cipher, setCipher] = useState('');\n  const [courseTask, setCourseTask] = useState<null | Record<string, any>>(null);\n  const [courseEvent, setCourseEvent] = useState<Partial<CourseEventDto> | null>(null);\n  const [copyModal, setCopyModal] = useState<{ id?: number } | null>(null);\n  const [selectedTab, setSelectedTab] = useLocalStorage<string>(LocalStorageKeys.StatusFilter, ALL_TAB_KEY);\n  const isManager = useMemo(() => isCourseManager(session, course.id), [session, course.id]);\n  const settings = useScheduleSettings();\n\n  const handleSubmit = async (record: CreateCourseTaskDto) => {\n    await courseTaskApi.createCourseTask(course.id, record);\n    setCourseTask(null);\n    refreshData();\n  };\n\n  const handleEventSubmit = () => {\n    setCourseEvent(null);\n    refreshData();\n  };\n\n  const handleCopyFromSubmit = async (record: Pick<CourseDto, 'id'>) => {\n    await courseScheduleApi.copySchedule(course.id, { copyFromCourseId: record.id });\n    setCopyModal(null);\n    refreshData();\n  };\n\n  const handleCreateCourseTask = () => {\n    setCourseTask({});\n  };\n\n  const handleCreateCourseEvent = () => {\n    setCourseEvent({});\n  };\n\n  const {\n    retry: refreshData,\n    value: data = [],\n    loading,\n    error,\n  } = useAsyncRetry(async () => {\n    const [response, tokenResponse] = await Promise.all([\n      courseScheduleApi.getSchedule(course.id),\n      coursesScheduleIcalApi.getScheduleICalendarToken(course.id),\n    ]);\n    setCipher(tokenResponse.data.token);\n    return response.data ?? [];\n  }, [course.id]);\n\n  const eventTags = useMemo(() => uniq(data.map(item => item.tag)), [data]);\n  const statuses = useMemo(() => data.map(({ status }) => status), [data]);\n\n  const { token } = theme.useToken();\n\n  return (\n    <>\n      <PageLayout\n        loading={loading}\n        error={error}\n        title=\"Schedule\"\n        showCourseName\n        background={token.colorBgLayout}\n        withMargin={false}\n      >\n        <div style={!mobileView ? { margin: '16px 0 0', padding: '0 24px 24px' } : { margin: 0 }}>\n          <StatusTabs activeTab={selectedTab} statuses={statuses} onTabChange={setSelectedTab} mobileView={mobileView}>\n            {!mobileView && (\n              <SettingsPanel\n                onCreateCourseTask={handleCreateCourseTask}\n                onCreateCourseEvent={handleCreateCourseEvent}\n                onCopyFromCourse={() => setCopyModal({})}\n                isCourseManager={isManager}\n                courseId={course.id}\n                courseAlias={course.alias}\n                settings={settings}\n                calendarToken={cipher}\n                tags={eventTags}\n                refreshData={refreshData}\n                mobileView={mobileView}\n              />\n            )}\n          </StatusTabs>\n\n          <TableView settings={settings} data={data} statusFilter={selectedTab} mobileView={mobileView} />\n        </div>\n\n        {courseTask ? (\n          <CourseTaskModal data={courseTask} onSubmit={handleSubmit} onCancel={() => setCourseTask(null)} />\n        ) : null}\n        {courseEvent && (\n          <CourseEventModal\n            data={courseEvent}\n            onSubmit={handleEventSubmit}\n            onCancel={() => setCourseEvent(null)}\n            courseId={course.id}\n          />\n        )}\n        <CoursesListModal\n          okText=\"Copy\"\n          data={copyModal}\n          onSubmit={handleCopyFromSubmit}\n          onCancel={() => setCopyModal(null)}\n        >\n          <Alert\n            style={{ marginBottom: 16 }}\n            type=\"error\"\n            description=\"It will copy all tasks and events from selected couse to your course. The action is not reversible.\"\n          />\n        </CoursesListModal>\n      </PageLayout>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Schedule/utils.ts",
    "content": "import { CourseScheduleItemDtoStatusEnum } from '@client/api';\nimport { DEFAULT_COLOR } from '@client/modules/Schedule/constants';\nimport { CSSProperties } from 'react';\n\nexport const getTagStyle = (tagName: string, tagColors: Record<string, string> = {}, styles?: CSSProperties) => {\n  const tagColor = tagColors[tagName] || DEFAULT_COLOR;\n  return {\n    ...styles,\n    borderColor: tagColor,\n    color: tagColor,\n    backgroundColor: `${tagColor}10`,\n  };\n};\n\nexport function getTaskStatusColor(value: CourseScheduleItemDtoStatusEnum) {\n  switch (value) {\n    case CourseScheduleItemDtoStatusEnum.Done:\n      return '#52c41a';\n    case CourseScheduleItemDtoStatusEnum.Missed:\n      return '#ff4d4f';\n    case CourseScheduleItemDtoStatusEnum.Archived:\n      return '#d9d9d9';\n    case CourseScheduleItemDtoStatusEnum.Available:\n      return '#1677ff';\n    case CourseScheduleItemDtoStatusEnum.Future:\n      return '#13c2c2';\n    case CourseScheduleItemDtoStatusEnum.Review:\n      return '#722ed1';\n    case CourseScheduleItemDtoStatusEnum.Registered:\n      return '#73d13d';\n    case CourseScheduleItemDtoStatusEnum.Unavailable:\n      return '#faad14';\n    default:\n      return '#d9d9d9';\n  }\n}\n\nexport const getStatusStyle = (statusName: CourseScheduleItemDtoStatusEnum, styles?: CSSProperties) => {\n  const statusColor = getTaskStatusColor(statusName);\n  return {\n    ...styles,\n    borderColor: statusColor,\n    color: statusColor,\n    backgroundColor: `${statusColor}10`,\n  };\n};\n"
  },
  {
    "path": "client/src/modules/Score/components/ExportCsvButton/index.tsx",
    "content": "import { FileExcelOutlined } from '@ant-design/icons';\nimport { Button, Tooltip } from 'antd';\n\ntype Props = {\n  enabled?: boolean;\n  onClick: () => void;\n};\n\nexport function ExportCsvButton(props: Props) {\n  if (!props.enabled) {\n    return null;\n  }\n  return (\n    <Tooltip title=\"Export to CSV\" placement=\"left\">\n      <Button shape=\"circle\" type=\"text\" onClick={props.onClick}>\n        <FileExcelOutlined style={{ fontSize: '2.5ch', display: 'block' }} />\n      </Button>\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Score/components/ScoreTable/ScoreTableTabs.tsx",
    "content": "import { useCallback, useState } from 'react';\nimport { Button, Space, Tabs, TabsProps, Tooltip } from 'antd';\nimport { ScoreTable } from '@client/modules/Score/components/ScoreTable/index';\nimport { CoursePageProps } from '@client/services/models';\nimport { useRouter } from 'next/router';\nimport { getExportCsvUrl } from '@client/modules/Score/data/getExportCsvUrl';\nimport { isExportEnabled } from '@client/modules/Score/data/isExportEnabled';\nimport { UpdateAlert } from '@client/modules/Score/pages/ScorePage/UpdateAlert';\nimport { ExportCsvButton } from '@client/modules/Score/components/ExportCsvButton';\nimport { SettingOutlined } from '@ant-design/icons';\n\ntype Props = {\n  tabProps: CoursePageProps & {\n    onLoading: (value: boolean) => void;\n  };\n};\n\nexport const ScoreTableTabs = ({ tabProps }: Props) => {\n  const router = useRouter();\n  const { ['mentor.githubId']: mentor, cityName } = router.query;\n  const [isVisibleSetting, setIsVisibleSettings] = useState(false);\n\n  const { course, session } = tabProps;\n\n  const tabs: TabsProps['items'] = [\n    {\n      key: 'all',\n      label: 'All students',\n      children: (\n        <ScoreTable\n          {...tabProps}\n          activeOnly={false}\n          isVisibleSetting={isVisibleSetting}\n          setIsVisibleSettings={setIsVisibleSettings}\n        />\n      ),\n    },\n    {\n      key: 'active',\n      label: 'Active students',\n      children: (\n        <ScoreTable\n          {...tabProps}\n          activeOnly={true}\n          isVisibleSetting={isVisibleSetting}\n          setIsVisibleSettings={setIsVisibleSettings}\n        />\n      ),\n    },\n  ];\n\n  const handleExportCsv = useCallback(\n    () => (window.location.href = getExportCsvUrl(course?.id, cityName, mentor)),\n    [cityName, mentor, course?.id],\n  );\n\n  const csvEnabled = isExportEnabled({ course, session });\n\n  return (\n    <Tabs\n      tabBarStyle={{ marginBottom: 0 }}\n      items={tabs}\n      defaultActiveKey=\"active\"\n      tabBarExtraContent={\n        <Space style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>\n          <UpdateAlert />\n          <ExportCsvButton enabled={csvEnabled} onClick={handleExportCsv} />\n          <Tooltip title=\"Table settings\" placement=\"left\">\n            <Button shape=\"circle\" type=\"text\" title=\"Settings\" onClick={() => setIsVisibleSettings(true)}>\n              <SettingOutlined style={{ fontSize: '2.5ch', display: 'block' }} />\n            </Button>\n          </Tooltip>\n        </Space>\n      }\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Score/components/ScoreTable/Summary.tsx",
    "content": "import { Table } from 'antd';\nimport { ColumnType } from 'antd/lib/table';\nimport { ScoreStudentDto } from '@client/api';\nimport get from 'lodash/get';\n\ntype SummaryProps = {\n  visibleColumns: ColumnType<ScoreStudentDto>[];\n  studentScore: ScoreStudentDto;\n};\n\nexport const Summary = ({ visibleColumns, studentScore }: SummaryProps) => {\n  return (\n    <Table.Summary.Row>\n      {/* the table has a hidden first column */}\n      <Table.Summary.Cell index={0} />\n      {visibleColumns.map(({ dataIndex, render }, index) => {\n        const value = get(studentScore, dataIndex as string | string[], null);\n        return (\n          <Table.Summary.Cell key={index} index={index + 1}>\n            {render ? render(value, studentScore, index + 1) : value}\n          </Table.Summary.Cell>\n        );\n      })}\n    </Table.Summary.Row>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Score/components/ScoreTable/index.module.css",
    "content": ".tableScore :global(.rs-table-row-disabled) {\n  opacity: 0.25;\n}\n\n.tableScore :global(td),\n.tableScore :global(th) {\n  padding: 0 5px !important;\n  font-size: 11px;\n}\n\n.tableScore :global(td a) {\n  line-height: 24px;\n}\n\n.tableScore :global(.ant-table-body) {\n  min-height: 200px;\n}\n\n.tableScore {\n  margin: 1.5rem 0 0;\n}\n"
  },
  {
    "path": "client/src/modules/Score/components/ScoreTable/index.tsx",
    "content": "import { Table, TablePaginationConfig } from 'antd';\nimport { ColumnType, TableProps } from 'antd/lib/table';\nimport { FilterValue, SorterResult } from 'antd/lib/table/interface';\nimport { Store } from 'antd/lib/form/interface';\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useLocalStorage } from 'react-use';\nimport isUndefined from 'lodash/isUndefined';\nimport { useRouter } from 'next/router';\nimport { CoursesTasksApi, CourseTaskDto, ScoreStudentDto } from '@client/api';\nimport styles from './index.module.css';\nimport { getColumns } from '@client/modules/Score/data/getColumns';\nimport { getTaskColumns } from '@client/modules/Score/data/getTaskColumns';\nimport { useScorePaging } from '@client/modules/Score/hooks/useScorePaging';\nimport { SettingsDrawer } from '@client/modules/Score/components/SettingsDrawer';\nimport { CourseService } from '@client/services/course';\nimport { CoursePageProps } from '@client/services/models';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport { ScoreOrder, ScoreTableFilters } from '@client/modules/Score/hooks/types';\nimport useWindowDimensions from '@client/shared/hooks/useWindowDimensions';\nimport { Summary } from './Summary';\n\ntype Props = CoursePageProps & {\n  onLoading: (value: boolean) => void;\n  activeOnly: boolean;\n  isVisibleSetting: boolean;\n  setIsVisibleSettings: (value: boolean) => void;\n};\n\ntype TableScoreOrder = SorterResult<ScoreStudentDto> | SorterResult<ScoreStudentDto>[];\n\ninterface ScoreTableFiltersModified extends Omit<ScoreTableFilters, 'activeOnly'> {\n  activeOnly?: boolean;\n}\ntype StudentsState = {\n  content: ScoreStudentDto[];\n  pagination: IPaginationInfo;\n  filter: ScoreTableFilters;\n  order: ScoreOrder;\n};\n\nconst courseTasksApi = new CoursesTasksApi();\n\nexport function ScoreTable(props: Props) {\n  const router = useRouter();\n  const { width } = useWindowDimensions();\n  const { activeOnly, isVisibleSetting, setIsVisibleSettings } = props;\n  const { ['mentor.githubId']: mentor, cityName, githubId, name } = router.query;\n\n  const [columns, setColumns] = useState<ColumnType<ScoreStudentDto>[]>([]);\n  const [fixedColumn, setFixedColumn] = useState<boolean>(true);\n  const [courseTasks, setCourseTasks] = useState([] as CourseTaskDto[]);\n  const [loaded, setLoaded] = useState(false);\n  const [state, setState] = useState(['']);\n  const [students, setStudents] = useState<StudentsState>({\n    content: [],\n    pagination: { current: 1, pageSize: 100 },\n    filter: { activeOnly: true },\n    order: { field: 'rank', order: 'ascend' },\n  });\n  const [studentScore, setStudentScore] = useState<ScoreStudentDto | null>(null);\n\n  const recentlyAppliedFilters = useRef<null | Record<string, FilterValue | null>>(null);\n\n  const [notVisibleColumns = [], setNotVisibleColumns] = useLocalStorage<string[]>('notVisibleColumns');\n  const courseService = useMemo(() => new CourseService(props.course.id), []);\n  const [getPagedData] = useScorePaging(router, courseService, activeOnly);\n\n  const getCourseScore = async (\n    pagination: TablePaginationConfig,\n    filters: ScoreTableFiltersModified,\n    order: TableScoreOrder,\n  ) => {\n    if (!getPagedData) return;\n    const data = await getPagedData(pagination as IPaginationInfo, filters as ScoreTableFilters, order as ScoreOrder);\n    setStudents({ ...students, content: data.content, pagination: data.pagination });\n  };\n\n  const loadInitialData = useCallback(async () => {\n    try {\n      props.onLoading(true);\n      let filters = { activeOnly };\n\n      if (!isUndefined(cityName)) {\n        filters = { ...filters, cityName } as ScoreTableFilters;\n      }\n      if (!isUndefined(mentor)) {\n        filters = { ...filters, ['mentor.githubId']: mentor } as ScoreTableFilters;\n      }\n\n      if (!isUndefined(githubId)) {\n        filters = { ...filters, githubId } as ScoreTableFilters;\n      }\n      if (!isUndefined(name)) {\n        filters = { ...filters, name } as ScoreTableFilters;\n      }\n\n      const [courseScore, studentCourseScore, courseTasks] = await Promise.all([\n        courseService.getCourseScore(students.pagination, filters, students.order),\n        courseService.getStudentCourseScore(props.session?.githubId as string),\n        courseTasksApi.getCourseTasks(props.course.id),\n      ]);\n\n      const {\n        content,\n        pagination: { totalPages },\n      } = courseScore;\n\n      const {\n        pagination: { current: currentPage },\n      } = students;\n\n      if (currentPage > totalPages) {\n        const { content, pagination } = await courseService.getCourseScore(\n          { ...students.pagination, current: totalPages },\n          filters,\n          students.order,\n        );\n        setStudents({ ...students, content, pagination: { ...pagination, current: totalPages } });\n      } else {\n        setStudents({ ...students, content, pagination: courseScore.pagination });\n      }\n\n      const sortedTasks = courseTasks.data\n        .filter(task => !!task.studentEndDate || props.course.completed)\n        .map(task => ({\n          ...task,\n          isVisible: !notVisibleColumns.includes(String(task.id)),\n        }));\n      setStudentScore(studentCourseScore);\n      setCourseTasks(sortedTasks);\n      setColumns(\n        getColumns({\n          githubId,\n          name,\n          cityName,\n          mentor,\n          taskColumns: getTaskColumns(sortedTasks),\n        }),\n      );\n\n      setLoaded(true);\n    } finally {\n      props.onLoading(false);\n    }\n  }, [activeOnly]);\n\n  const getVisibleColumns = (columns: ColumnType<ScoreStudentDto>[]) =>\n    columns.filter(column => (column.dataIndex ? !notVisibleColumns.includes(String(column.dataIndex)) : true));\n\n  const handleModalCancel = () => {\n    setIsVisibleSettings(!isVisibleSetting);\n  };\n\n  const handleModalOk = (values: Store) => {\n    setNotVisibleColumns(Object.keys(values).filter((value: string) => values[value] === false));\n    setIsVisibleSettings(!isVisibleSetting);\n  };\n\n  useEffect(() => {\n    loadInitialData();\n  }, [activeOnly]);\n\n  useEffect(() => {\n    if (width < 400) {\n      setFixedColumn(false);\n      return;\n    }\n\n    setFixedColumn(true);\n  }, [width]);\n\n  useEffect(() => {\n    setColumns(prevColumns => {\n      const githubColumn = prevColumns.find(el => el.key === 'githubId');\n      if (githubColumn) {\n        githubColumn.fixed = fixedColumn ? 'left' : false;\n      }\n\n      return prevColumns;\n    });\n  }, [fixedColumn, loaded]);\n\n  if (!loaded) {\n    return null;\n  }\n\n  const handleChange: TableProps<ScoreStudentDto>['onChange'] = async (pagination, filters, sorter, { action }) => {\n    // Dirty hack to prevent sort request with old filters on Enter key in filter modal search input\n    // This is known issue please, see https://github.com/ant-design/ant-design/issues/37334\n    // TODO: Remove this hack after fix in antd\n    if (action === 'filter') {\n      recentlyAppliedFilters.current = filters;\n      setTimeout(() => (recentlyAppliedFilters.current = null), 50);\n    }\n    if (action === 'sort' && recentlyAppliedFilters.current) {\n      filters = recentlyAppliedFilters.current;\n    }\n\n    try {\n      props.onLoading(true);\n      const [studentCourseScore] = await Promise.all([\n        courseService.getStudentCourseScore(props.session?.githubId as string),\n        getCourseScore(pagination, filters, sorter),\n      ]);\n      setStudentScore(studentCourseScore);\n    } finally {\n      props.onLoading(false);\n    }\n  };\n\n  const visibleColumns = getVisibleColumns(columns);\n  const isSummaryShown = students.content.length > 1 && studentScore;\n\n  return (\n    <>\n      <Table<ScoreStudentDto>\n        className={styles.tableScore}\n        showHeader\n        scroll={{ x: getTableWidth(visibleColumns.length), y: 'calc(95vh - 320px)' }}\n        pagination={{ ...students.pagination, showTotal: total => `Total ${total} students` }}\n        rowKey=\"githubId\"\n        rowClassName={record => (!record.isActive ? 'rs-table-row-disabled' : '')}\n        dataSource={students.content}\n        summary={() =>\n          isSummaryShown && (\n            <Table.Summary fixed=\"top\">\n              <Summary studentScore={studentScore} visibleColumns={visibleColumns} />\n            </Table.Summary>\n          )\n        }\n        onChange={handleChange}\n        columns={visibleColumns}\n        rowSelection={{\n          selectedRowKeys: state,\n          onChange: (_, selectedRows) => {\n            setState(selectedRows.map(row => row.githubId));\n          },\n          type: 'radio',\n          columnWidth: 0,\n          renderCell: () => '',\n        }}\n        onRow={record => {\n          return {\n            onClick: () => {\n              setState([record.githubId]);\n            },\n          };\n        }}\n      />\n      <SettingsDrawer\n        courseTasks={courseTasks}\n        isVisible={isVisibleSetting}\n        onOk={handleModalOk}\n        onCancel={handleModalCancel}\n      />\n    </>\n  );\n}\n\nexport function getTableWidth(columnsCount: number) {\n  const columnWidth = 90;\n  // where 800 is approximate sum of basic columns (GitHub, Name, etc.)\n  const tableWidth = columnsCount * columnWidth;\n  return tableWidth;\n}\n"
  },
  {
    "path": "client/src/modules/Score/components/SettingsDrawer/index.tsx",
    "content": "import { Button, Checkbox, Drawer, Form, Space, theme } from 'antd';\nimport { Store } from 'antd/lib/form/interface';\nimport type { CourseTaskDto } from '@client/api';\nimport SettingsItem from '@client/components/SettingsItem';\nimport {\n  CheckSquareOutlined,\n  CloseCircleOutlined,\n  CloseSquareOutlined,\n  SaveOutlined,\n  TableOutlined,\n} from '@ant-design/icons';\n\ntype Props = {\n  courseTasks: (CourseTaskDto & { isVisible?: boolean })[];\n  isVisible: boolean;\n  onCancel: () => void;\n  onOk: (values: Store) => void;\n};\n\nexport function SettingsDrawer(props: Props) {\n  const { onCancel, onOk, courseTasks, isVisible } = props;\n  const [form] = Form.useForm();\n  const { token } = theme.useToken();\n\n  const onOkHandle = async () => {\n    const values = await form.validateFields().catch(() => null);\n    if (!values) {\n      return;\n    }\n    onOk(values);\n  };\n\n  const initialValues = courseTasks.reduce(\n    (acc, curr) => {\n      acc[curr.id] = curr.isVisible;\n      return acc;\n    },\n    {} as Record<string, boolean | undefined>,\n  );\n\n  const fillAllFields = (value: boolean) => {\n    const newValues: Record<string, boolean | undefined> = {};\n    courseTasks.forEach(task => {\n      newValues[task.id] = value;\n    });\n    form.setFieldsValue(newValues);\n  };\n\n  const actionConfigs = [\n    {\n      key: 'all',\n      title: 'Select all',\n      label: 'All',\n      onClick: () => fillAllFields(true),\n      icon: <CheckSquareOutlined style={{ color: token.colorInfo }} />,\n    },\n    {\n      key: 'none',\n      title: 'Deselect all',\n      label: 'None',\n      onClick: () => fillAllFields(false),\n      icon: <CloseSquareOutlined style={{ color: token.colorWarning }} />,\n    },\n    {\n      key: 'cancel',\n      title: 'Cancel',\n      label: 'Cancel',\n      onClick: onCancel,\n      icon: <CloseCircleOutlined style={{ color: token.colorError }} />,\n    },\n    {\n      key: 'save',\n      title: 'Save',\n      label: 'Save',\n      onClick: onOkHandle,\n      icon: <SaveOutlined style={{ color: token.colorSuccess }} />,\n    },\n  ] as const;\n\n  const actions = actionConfigs.map(({ key, title, label, onClick, icon }) => (\n    <Button key={key} type=\"text\" title={title} size=\"small\" onClick={onClick} icon={icon}>\n      {label}\n    </Button>\n  ));\n\n  return (\n    <Drawer title=\"Score settings\" open={isVisible} onClose={onCancel} styles={{ body: { containerType: 'size' } }}>\n      <SettingsItem header=\"Columns visibility\" IconComponent={TableOutlined} actions={actions}>\n        <Form form={form} initialValues={initialValues} layout=\"vertical\">\n          <Space style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>\n            {courseTasks.map(courseTask => (\n              <Form.Item\n                key={courseTask.id}\n                name={courseTask.id}\n                valuePropName=\"checked\"\n                style={{ marginBottom: 4, width: 200 }}\n              >\n                <Checkbox>{courseTask.name}</Checkbox>\n              </Form.Item>\n            ))}\n          </Space>\n        </Form>\n      </SettingsItem>\n    </Drawer>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Score/data/getColumns.tsx",
    "content": "import { ColumnType } from 'antd/es/table';\nimport { ScoreStudentDto } from '@client/api';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { dateRenderer, getColumnSearchProps } from '@client/shared/components/Table';\nimport isArray from 'lodash/isArray';\nimport Link from 'next/link';\n\ntype Props = {\n  taskColumns: Record<string, any>[];\n  githubId?: string | string[];\n  name?: string | string[];\n  cityName?: string | string[];\n  mentor?: string | string[];\n};\n\nconst getSearchProps = (key: string) => ({\n  ...getColumnSearchProps(key),\n  onFilter: undefined,\n});\n\nexport function getColumns(props: Props): ColumnType<ScoreStudentDto>[] {\n  const { githubId, name, cityName, mentor, taskColumns } = props;\n\n  return [\n    {\n      title: '#',\n      fixed: 'left',\n      dataIndex: 'rank',\n      key: 'rank',\n      width: 50,\n      sorter: 'rank',\n      render: (value: number) => (value >= 999999 ? 'New' : value),\n    },\n    {\n      title: 'GitHub',\n      fixed: 'left',\n      key: 'githubId',\n      dataIndex: 'githubId',\n      sorter: 'githubId',\n      defaultFilteredValue: githubId ? (isArray(githubId) ? githubId : [githubId]) : undefined,\n      width: 150,\n      render: (value: string) => (\n        <div>\n          <GithubAvatar githubId={value} size={24} />\n          &nbsp;\n          <a target=\"_blank\" href={`https://github.com/${value}`}>\n            {value}\n          </a>\n        </div>\n      ),\n      ...getSearchProps('githubId'),\n    },\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      width: 150,\n      sorter: 'name',\n      defaultFilteredValue: name ? (isArray(name) ? name : [name]) : undefined,\n      render: (value, record) => (\n        <Link prefetch={false} href={`/profile?githubId=${record.githubId}`}>\n          {value}\n        </Link>\n      ),\n      ...getSearchProps('name'),\n    },\n    {\n      title: 'City',\n      dataIndex: 'cityName',\n      width: 150,\n      sorter: 'cityName',\n      defaultFilteredValue: cityName ? (isArray(cityName) ? cityName : [cityName]) : undefined,\n      ...getSearchProps('cityName'),\n    },\n    {\n      title: 'Total',\n      dataIndex: 'totalScore',\n      width: 80,\n      sorter: 'totalScore',\n      render: (value: number) => <b>{value}</b>,\n    },\n    {\n      title: 'Cross-Check',\n      dataIndex: 'crossCheckScore',\n      width: 90,\n      sorter: 'crossCheckScore',\n      render: (value: number) => <b>{value}</b>,\n    },\n    ...taskColumns,\n    {\n      title: 'Change Date',\n      dataIndex: 'totalScoreChangeDate',\n      width: 80,\n      sorter: 'totalScoreChangeDate',\n      render: dateRenderer,\n    },\n    {\n      title: 'Last Commit Date',\n      dataIndex: 'repositoryLastActivityDate',\n      width: 80,\n      sorter: 'repositoryLastActivityDate',\n      render: dateRenderer,\n    },\n    {\n      title: 'Mentor',\n      dataIndex: ['mentor', 'githubId'],\n      width: 150,\n      sorter: 'mentor',\n      defaultFilteredValue: mentor ? (isArray(mentor) ? mentor : [mentor]) : undefined,\n      render: (value: string) => (\n        <Link prefetch={false} href={`/profile?githubId=${value}`}>\n          {value}\n        </Link>\n      ),\n      ...getSearchProps('mentor.githubId'),\n    },\n  ];\n}\n"
  },
  {
    "path": "client/src/modules/Score/data/getExportCsvUrl.ts",
    "content": "import { getQueryParams, getQueryString } from '@client/shared/utils/queryParams-utils';\n\nexport function getExportCsvUrl(courseId: number, cityName?: string | string[], mentor?: string | string[]) {\n  const queryParams = getQueryString(getQueryParams({ cityName, ['mentor.githubId']: mentor }));\n  const url = buildUrl(courseId, queryParams);\n  return url;\n}\n\nconst buildUrl = (id: number, params: string): string => {\n  return `/api/course/${id}/students/score/csv${params}`;\n};\n"
  },
  {
    "path": "client/src/modules/Score/data/getTaskColumns.tsx",
    "content": "import { ColumnType } from 'antd/lib/table';\nimport { Popover } from 'antd';\nimport { QuestionCircleOutlined } from '@ant-design/icons';\nimport { CourseTaskDto, ScoreStudentDto } from '@client/api';\nimport { dateTimeRenderer } from '@client/shared/components/Table';\n\nexport function getTaskColumns(courseTasks: CourseTaskDto[]) {\n  const columns = courseTasks.map(courseTask => {\n    const column: ColumnType<ScoreStudentDto> = {\n      dataIndex: courseTask.id.toString(),\n      title: () => {\n        const icon = (\n          <Popover\n            content={\n              <ul>\n                <li>Coefficient: {Number(courseTask.scoreWeight.toFixed(2))}</li>\n                <li>Deadline: {dateTimeRenderer(courseTask.studentEndDate)}</li>\n                <li>Max. score: {courseTask.maxScore}</li>\n              </ul>\n            }\n            trigger=\"click\"\n            overlayStyle={{ position: 'fixed', minWidth: 235 }}\n          >\n            <QuestionCircleOutlined title=\"Click for details\" />\n          </Popover>\n        );\n        return courseTask.descriptionUrl ? (\n          <>\n            <a className=\"table-header-link\" target=\"_blank\" href={courseTask.descriptionUrl}>\n              {courseTask.name}\n            </a>{' '}\n            {icon}\n          </>\n        ) : (\n          <div>\n            {courseTask.name} {icon}\n          </div>\n        );\n      },\n      width: 100,\n      className: 'align-right',\n      render: (_, d) => {\n        const currentTask = d.taskResults.find(taskResult => taskResult.courseTaskId === courseTask.id);\n        return currentTask ? <div>{currentTask.score}</div> : 0;\n      },\n    };\n    return column;\n  });\n  return columns;\n}\n"
  },
  {
    "path": "client/src/modules/Score/data/isExportEnabled.ts",
    "content": "import { ProfileCourseDto } from '@client/api';\nimport { Session } from '@client/components/withSession';\nimport { isCourseManager } from '@client/domain/user';\nimport { CourseRole } from '@client/services/models';\n\nexport function isExportEnabled({ session, course }: { session?: Session; course: ProfileCourseDto }) {\n  if (!session) {\n    return false;\n  }\n  const { isAdmin, isHirer, courses } = session;\n  const courseId = course.id;\n  const { roles } = courses?.[courseId] ?? { roles: [] };\n\n  return (\n    isAdmin ||\n    isHirer ||\n    isCourseManager(session, courseId) ||\n    roles?.includes(CourseRole.Manager) ||\n    roles?.includes(CourseRole.Supervisor)\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Score/hooks/types.ts",
    "content": "export type ScoreTableFilters = {\n  githubId?: string[];\n  name?: string[];\n  'mentor.githubId'?: string[];\n  cityName?: string[];\n  activeOnly: boolean;\n};\n\nexport type ScoreOrderField =\n  | 'rank'\n  | 'totalScore'\n  | 'crossCheckScore'\n  | 'githubId'\n  | 'name'\n  | 'cityName'\n  | 'mentor'\n  | 'totalScoreChangeDate'\n  | 'repositoryLastActivityDate';\n\nexport type ScoreOrder = {\n  field: ScoreOrderField;\n  order: 'ascend' | 'descend';\n  column?: { sorter: ScoreOrderField };\n};\n"
  },
  {
    "path": "client/src/modules/Score/hooks/useScorePaging.tsx",
    "content": "import { NextRouter } from 'next/router';\nimport { ParsedUrlQuery } from 'querystring';\nimport { useCallback } from 'react';\nimport { CourseService } from '@client/services/course';\nimport { getQueryParams } from '@client/shared/utils/queryParams-utils';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport { ScoreOrder, ScoreTableFilters } from './types';\n\nexport function useScorePaging(router: NextRouter, courseService: CourseService, activeOnly: boolean) {\n  const { githubId, name, ['mentor.githubId']: mentor, cityName, ...currentQuery } = router.query;\n\n  const setQueryParams = useCallback(\n    (query: ParsedUrlQuery) => {\n      router.push({ pathname: router.pathname, query }, undefined, { shallow: true });\n    },\n    [router],\n  );\n\n  const getCourseScore = useCallback(\n    async (pagination: IPaginationInfo, filters: ScoreTableFilters, scoreOrder: ScoreOrder) => {\n      const { cityName, ['mentor.githubId']: mentor, githubId, name } = filters;\n      const newQueryParams = getQueryParams({ cityName, ['mentor.githubId']: mentor, githubId, name }, currentQuery);\n      setQueryParams(newQueryParams);\n      const field = scoreOrder.column?.sorter || 'rank';\n      const courseScore = await courseService.getCourseScore(\n        pagination,\n        { ...filters, activeOnly },\n        { field, order: scoreOrder.order },\n      );\n      return { content: courseScore.content, pagination: courseScore.pagination };\n    },\n    [currentQuery, activeOnly],\n  );\n\n  return [getCourseScore];\n}\n"
  },
  {
    "path": "client/src/modules/Score/pages/ScorePage/UpdateAlert.module.css",
    "content": ".container {\n  position: relative;\n}\n\n.wrapper {\n  width: 55ch;\n  border-radius: 2.5ch;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  position: absolute;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  animation: disappear 5s ease-in-out 5s forwards;\n}\n\n.icon {\n  opacity: 0;\n  animation:\n    appear 1.3s ease-in 8s forwards,\n    pulse 0.7s 10s ease-in 1;\n}\n\n@media (max-width: 820px) {\n  .wrapper {\n    visibility: hidden;\n  }\n\n  .icon {\n    opacity: 1;\n  }\n}\n\n@keyframes disappear {\n  0% {\n    width: 55ch;\n    opacity: 1;\n  }\n  80% {\n    opacity: 1;\n    height: initial;\n  }\n  100% {\n    width: 0;\n    height: 0;\n    opacity: 0;\n  }\n}\n\n@keyframes appear {\n  from {\n    opacity: 0;\n    scale: 0;\n  }\n  to {\n    opacity: 1;\n    scale: 1;\n  }\n}\n\n@keyframes pulse {\n  0% {\n    scale: 0.8;\n  }\n  60% {\n    scale: 1.2;\n  }\n  75% {\n    scale: 0.8;\n  }\n  100% {\n    scale: 1;\n  }\n}\n"
  },
  {
    "path": "client/src/modules/Score/pages/ScorePage/UpdateAlert.tsx",
    "content": "import { theme, Tooltip, Typography } from 'antd';\nimport { QuestionCircleOutlined } from '@ant-design/icons';\nimport styles from './UpdateAlert.module.css';\n\nconst { Text } = Typography;\n\nexport const UpdateAlert = () => {\n  const { token } = theme.useToken();\n  const text = 'Total score and position is updated every day at 04:00 GMT+3';\n  return (\n    <>\n      <div className={styles.container}>\n        <div className={styles.wrapper} style={{ backgroundColor: token.colorWarning }}>\n          <Text>{text}</Text>\n        </div>\n        <div className={styles.icon}>\n          <Tooltip title={text} placement=\"left\">\n            <QuestionCircleOutlined\n              style={{\n                color: token.colorWarning,\n                fontSize: '2.5ch',\n                display: 'block',\n              }}\n            />\n          </Tooltip>\n        </div>\n      </div>\n    </>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Score/pages/ScorePage/index.tsx",
    "content": "import { CourseNoAccess } from '@client/modules/Course/components/CourseNoAccess';\nimport { CoursePageLayout } from '@client/components/CoursePageLayout';\nimport { useContext, useState } from 'react';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { ScoreTableTabs } from '@client/modules/Score/components/ScoreTable/ScoreTableTabs';\n\nexport function ScorePage() {\n  const { course } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n\n  const [loading, setLoading] = useState(false);\n\n  return !course ? (\n    <CourseNoAccess />\n  ) : (\n    <CoursePageLayout showCourseName course={course} title=\"Score\" githubId={session.githubId} loading={loading}>\n      <ScoreTableTabs\n        tabProps={{\n          course,\n          session,\n          onLoading: setLoading,\n        }}\n      />\n    </CoursePageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/AvailableReviewCard/AvailableReviewCard.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { AvailableReviewStatsDto } from '@client/api';\nimport { AvailableReviewCard } from './AvailableReviewCard';\n\nconst availableReviews: AvailableReviewStatsDto[] = [\n  {\n    id: 1,\n    name: 'Task 1',\n    completedChecksCount: 2,\n    checksCount: 5,\n  },\n  {\n    id: 2,\n    name: 'Task 2',\n    completedChecksCount: 3,\n    checksCount: 8,\n  },\n];\nconst courseAlias = 'course1';\n\ndescribe('AvailableReviewCard', () => {\n  it.each(availableReviews)('should render with available reviews', review => {\n    render(<AvailableReviewCard availableReviews={availableReviews} courseAlias={courseAlias} />);\n    const link = screen.getByText(review.name);\n    expect(link).toBeInTheDocument();\n    expect(link).toHaveAttribute('href', `./cross-check-review?course=${courseAlias}&taskId=${review.id}`);\n    expect(screen.getByText(`${review.completedChecksCount}/${review.checksCount}`)).toBeInTheDocument();\n  });\n\n  it('should render 1 divider when 2 review', () => {\n    render(<AvailableReviewCard availableReviews={availableReviews} courseAlias={courseAlias} />);\n    const dividers = screen.getAllByRole('separator');\n    expect(dividers.length).toBe(1);\n  });\n\n  it('should not render divider when 1 review', () => {\n    render(<AvailableReviewCard availableReviews={availableReviews.slice(0, 1)} courseAlias={courseAlias} />);\n    const divider = screen.queryByRole('separator');\n    expect(divider).not.toBeInTheDocument();\n  });\n\n  it('should render \"At the moment, there are no tasks available for review.\" when no available reviews', () => {\n    const availableReviews: AvailableReviewStatsDto[] = [];\n    render(<AvailableReviewCard availableReviews={availableReviews} courseAlias={courseAlias} />);\n    expect(screen.getByText('Cross-check [Review]')).toBeInTheDocument();\n    expect(screen.getByText('At the moment, there are no tasks available for review')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/AvailableReviewCard/AvailableReviewCard.tsx",
    "content": "import { Divider, Empty, Progress, Row, Typography } from 'antd';\nimport { AvailableReviewStatsDto } from '@client/api';\nimport CommonCard from '../CommonDashboardCard';\n\ntype Props = {\n  availableReviews: AvailableReviewStatsDto[];\n  courseAlias: string;\n};\nconst { Text, Link } = Typography;\n\nexport function AvailableReviewCard({ availableReviews, courseAlias }: Props) {\n  return (\n    <CommonCard\n      title=\"Cross-check [Review]\"\n      content={\n        <>\n          {availableReviews.length ? (\n            <>\n              {availableReviews.map((el, index) => (\n                <Row key={el.id} gutter={[0, 8]}>\n                  <Link target={'_blank'} href={`./cross-check-review?course=${courseAlias}&taskId=${el.id}`}>\n                    {el.name}\n                  </Link>\n                  <Progress\n                    percent={(el.completedChecksCount / el.checksCount) * 100}\n                    format={() => `${el.completedChecksCount}/${el.checksCount}`}\n                  />\n                  {index + 1 !== availableReviews.length && <Divider />}\n                </Row>\n              ))}\n            </>\n          ) : (\n            <Empty>\n              <Text>At the moment, there are no tasks available for review</Text>\n            </Empty>\n          )}\n        </>\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/AvailableReviewCard/index.ts",
    "content": "export * from './AvailableReviewCard';\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/CommonDashboardCard.tsx",
    "content": "import * as React from 'react';\nimport { Card, Typography, Empty, CardProps } from 'antd';\n\nimport { FullscreenOutlined } from '@ant-design/icons';\n\nconst { Title } = Typography;\n\ntype Props = Omit<CardProps, 'content'> & {\n  isMoreContent?: boolean | undefined;\n  noDataDescription?: string | undefined;\n  content?: React.ReactNode;\n};\n\ntype State = {\n  isVisibilitySettingsVisible: boolean;\n  isProfileSettingsVisible: boolean;\n};\nclass CommonCard extends React.Component<Props, State> {\n  state = {\n    isVisibilitySettingsVisible: false,\n    isProfileSettingsVisible: false,\n  };\n\n  render() {\n    const { title, content, isMoreContent, noDataDescription, ...restProps } = this.props;\n\n    return (\n      <Card\n        title={\n          <Title level={2} ellipsis={true} style={{ fontSize: 16, marginBottom: 0 }}>\n            {title}\n          </Title>\n        }\n        actions={isMoreContent ? [<FullscreenOutlined key=\"main-card-actions-more\" />].filter(Boolean) : []}\n        {...restProps}\n      >\n        {content ? content : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={noDataDescription} />}\n      </Card>\n    );\n  }\n}\n\nexport default CommonCard;\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/MainStatsCard.tsx",
    "content": "import * as React from 'react';\nimport { TrophyOutlined, FireOutlined } from '@ant-design/icons';\nimport { Card, Typography, Image, Divider } from 'antd';\nimport CommonCard from './CommonDashboardCard';\n\nconst DEFAULT_POSITION = 999999;\n\nconst { Text } = Typography;\n\ntype Props = {\n  isActive: boolean;\n  totalScore: number;\n  position: number;\n  maxCourseScore: number;\n  totalStudentsCount: number;\n};\n\nexport function MainStatsCard({ totalScore, position, maxCourseScore, totalStudentsCount }: Props) {\n  const currentPositionText = `${position}${totalStudentsCount ? ` / ${totalStudentsCount}` : ''}`;\n  const positionText = position >= DEFAULT_POSITION ? 'New' : currentPositionText;\n  const totalScoreText = `${totalScore}${maxCourseScore ? ` / ${maxCourseScore}` : ''}`;\n  const { gridStyle, contentColStyle, contentDivStyle, iconStyle, textStyle } = STYLE;\n\n  return (\n    <CommonCard\n      title=\"Your stats\"\n      bodyStyle={{ padding: 0 }}\n      content={\n        <>\n          <Card.Grid hoverable={false} style={gridStyle}>\n            <Image preview={false} src=\"/static/images/im-fine.svg\" />\n          </Card.Grid>\n          <Divider />\n          <Card.Grid hoverable={false} style={gridStyle}>\n            <div style={contentColStyle}>\n              <div style={contentDivStyle}>\n                <Text type=\"secondary\">\n                  <TrophyOutlined style={iconStyle} />\n                  Position\n                </Text>\n                <Text strong style={textStyle}>\n                  {positionText}\n                </Text>\n              </div>\n              <div style={contentDivStyle}>\n                <Text type=\"secondary\">\n                  <FireOutlined style={iconStyle} />\n                  Total Score\n                </Text>\n                <Text strong style={textStyle}>\n                  {totalScoreText}\n                </Text>\n              </div>\n            </div>\n          </Card.Grid>\n        </>\n      }\n    />\n  );\n}\n\nconst STYLE: Record<string, React.CSSProperties> = {\n  contentDivStyle: {\n    minWidth: 106,\n    maxHeight: 58,\n    height: '100%',\n    display: 'flex',\n    flexDirection: 'column',\n    alignItems: 'center',\n  },\n  gridStyle: {\n    width: '100%',\n    boxShadow: 'none',\n    display: 'flex',\n    alignItems: 'center',\n    justifyContent: 'center',\n    padding: 16,\n  },\n  contentColStyle: {\n    width: '100%',\n    maxWidth: 350,\n    display: 'flex',\n    flexWrap: 'wrap',\n    columnGap: 5,\n    rowGap: 20,\n    justifyContent: 'space-around',\n    alignItems: 'center',\n  },\n  iconStyle: {\n    marginRight: 8,\n  },\n  textStyle: {\n    fontSize: '20px',\n    lineHeight: '28px',\n    textAlign: 'center',\n  },\n};\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/MentorCard/MentorCard.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { MentorCardProps, MentorCard, ASSERTION } from '.';\n\nconst MENTOR_MOCK = {\n  id: 1,\n  githubId: 'github-id',\n  name: 'mentor-name',\n  isActive: true,\n  cityName: 'city-name',\n  countryName: 'country-name',\n  students: [],\n  contactsEmail: 'email@example.com',\n  contactsNotes: 'contact-notes',\n  contactsPhone: 'contact-phone',\n  contactsSkype: 'contact-skype',\n  contactsTelegram: 'contact-telegram',\n  contactsWhatsApp: 'contact-whatsapp',\n};\nconst PROPS_MOCK: MentorCardProps = {\n  mentor: MENTOR_MOCK,\n  courseId: 10,\n};\n\ndescribe('MentorCard', () => {\n  describe('when student has a mentor', () => {\n    it.each`\n      info\n      ${MENTOR_MOCK.githubId}\n      ${MENTOR_MOCK.name}\n      ${MENTOR_MOCK.cityName}\n      ${MENTOR_MOCK.countryName}\n      ${MENTOR_MOCK.contactsEmail}\n      ${MENTOR_MOCK.contactsNotes}\n      ${MENTOR_MOCK.contactsPhone}\n      ${MENTOR_MOCK.contactsSkype}\n      ${MENTOR_MOCK.contactsTelegram}\n    `('should render mentor info \"$info\"', ({ info }: { info: string }) => {\n      render(<MentorCard {...PROPS_MOCK} />);\n\n      expect(screen.getByText(new RegExp(info))).toBeInTheDocument();\n    });\n  });\n\n  describe('when student does not have a mentor', () => {\n    const propsWithoutMentor = { ...PROPS_MOCK, mentor: undefined };\n\n    it('should not render mentor data', () => {\n      render(<MentorCard {...propsWithoutMentor} />);\n\n      expect(screen.queryByText(MENTOR_MOCK.githubId)).not.toBeInTheDocument();\n    });\n\n    it('should render note', () => {\n      render(<MentorCard {...propsWithoutMentor} />);\n\n      expect(screen.getByText(ASSERTION)).toBeInTheDocument();\n    });\n  });\n\n  it('should render \"Submit task\" button', () => {\n    render(<MentorCard {...PROPS_MOCK} />);\n\n    const submitButton = screen.getByRole('button', { name: /submit task/i });\n    expect(submitButton).toBeInTheDocument();\n  });\n\n  it('should open modal window when \"Submit task\" was clicked', async () => {\n    render(<MentorCard {...PROPS_MOCK} />);\n\n    const submitButton = screen.getByRole('button', { name: /submit task/i });\n    fireEvent.click(submitButton);\n\n    const modal = await screen.findByRole('dialog', { name: /submit task for mentor review/i });\n    expect(modal).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/MentorCard/MentorCard.tsx",
    "content": "import { Col, Typography, Row } from 'antd';\nimport CommonCard from '../CommonDashboardCard';\nimport { MentorInfo } from '../MentorInfo';\nimport { SubmitTaskSolution } from '../SubmitTaskSolution';\nimport { MentorStudentSummaryDto } from '@client/api';\n\nexport type MentorCardProps = {\n  mentor?: MentorStudentSummaryDto | null;\n  courseId: number;\n};\n\nconst { Text } = Typography;\n\nexport const ASSERTION =\n  \"Even if you don't have your own mentor, you can submit a task for review and someone else's mentor will review it if they have the time and desire\";\n\nexport function MentorCard({ mentor, courseId }: MentorCardProps) {\n  return (\n    <CommonCard\n      title=\"Mentor's check\"\n      content={\n        <>\n          {mentor ? (\n            <MentorInfo mentor={mentor} />\n          ) : (\n            <Row gutter={8} style={{ marginBottom: 16 }} wrap={false}>\n              <Col flex={'none'}>\n                <Text type=\"secondary\">Note:</Text>\n              </Col>\n              <Col flex={'auto'}>\n                <Text>{ASSERTION}</Text>\n              </Col>\n            </Row>\n          )}\n          <Row justify=\"center\">\n            <SubmitTaskSolution courseId={courseId} />\n          </Row>\n        </>\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/MentorCard/index.tsx",
    "content": "export * from './MentorCard';\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/MentorInfo/MentorInfo.tsx",
    "content": "import { Col, Row, Space, Typography } from 'antd';\nimport GithubFilled from '@ant-design/icons/GithubFilled';\nimport EnvironmentFilled from '@ant-design/icons/EnvironmentFilled';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { MentorStudentSummaryDto } from '@client/api';\n\nconst { Text, Link } = Typography;\n\nexport interface MentorContact {\n  contactsEmail?: string;\n  contactsPhone?: string;\n  contactsSkype?: string;\n  contactsTelegram?: string;\n  contactsNotes?: string;\n}\n\ninterface Props {\n  mentor: MentorStudentSummaryDto;\n}\n\nfunction MentorInfo({ mentor }: Props) {\n  const {\n    name,\n    githubId,\n    contactsEmail,\n    contactsPhone,\n    contactsSkype,\n    contactsTelegram,\n    contactsNotes,\n    cityName,\n    countryName,\n  } = mentor;\n\n  const contacts = [\n    { name: 'E-mail', value: contactsEmail },\n    { name: 'Telegram', value: contactsTelegram },\n    { name: 'Phone', value: contactsPhone },\n    { name: 'Skype', value: contactsSkype },\n    { name: 'Notes', value: contactsNotes },\n  ];\n\n  const filledContacts = contacts.filter(({ value }) => value);\n\n  return (\n    <div style={{ marginBottom: 16 }}>\n      <Row justify=\"center\" align=\"middle\" gutter={12} style={{ marginBottom: 16 }}>\n        <Col>\n          <GithubAvatar size={48} githubId={githubId!} />\n        </Col>\n        <Col>\n          <Space direction=\"vertical\" size={4}>\n            {name && <Text strong>{name}</Text>}\n            <Link target=\"_blank\" href={`https://github.com/${githubId}`}>\n              <GithubFilled /> {githubId}\n            </Link>\n          </Space>\n        </Col>\n      </Row>\n      <Row justify=\"center\" gutter={8} style={{ marginBottom: 16 }}>\n        <Col>\n          <span>\n            <EnvironmentFilled /> {`${cityName}, ${countryName}`}\n          </span>\n        </Col>\n      </Row>\n      {filledContacts?.length > 0 &&\n        filledContacts.map(({ name, value }, idx) => (\n          <Row key={idx} justify=\"center\" gutter={8} style={{ marginBottom: 8 }}>\n            <Col>\n              <Text type=\"secondary\">{`${name}:`}</Text>\n            </Col>\n            <Col>\n              <Text>{value}</Text>\n            </Col>\n          </Row>\n        ))}\n    </div>\n  );\n}\n\nexport default MentorInfo;\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/MentorInfo/index.ts",
    "content": "export { default as MentorInfo, type MentorContact } from './MentorInfo';\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/NextEventCard/NextEventCard.module.css",
    "content": ".table :global(.ant-table-thead) {\n  display: none;\n}\n\n.table :global(tr:last-child > td) {\n  border-bottom: none;\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/NextEventCard/NextEventCard.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { CourseScheduleItemDto, CourseScheduleItemDtoTagEnum, CourseScheduleItemDtoTypeEnum } from '@client/api';\nimport { NextEventCard } from '.';\n\nconst NEXT_EVENTS = generateAvailableTasks();\n\nconst PROPS_MOCK = {\n  nextEvents: NEXT_EVENTS,\n  courseAlias: 'course-alias',\n};\n\ndescribe('NextEventCard', () => {\n  it.each`\n    text\n    ${'Available tasks'}\n    ${'View all'}\n    ${NEXT_EVENTS[0]?.name}\n    ${NEXT_EVENTS[0]?.tag}\n    ${'Feb 01'}\n  `('should render $text', ({ text }: { text: string }) => {\n    render(<NextEventCard {...PROPS_MOCK} />);\n\n    const match = new RegExp(text, 'i');\n    expect(screen.getByText(match)).toBeInTheDocument();\n  });\n});\n\nfunction generateAvailableTasks(count = 3): CourseScheduleItemDto[] {\n  return new Array(count).fill({}).map((_, idx) => ({\n    id: idx,\n    type: CourseScheduleItemDtoTypeEnum.CourseTask,\n    name: `Available Task ${idx}`,\n    startDate: '1970-01-01T00:00:00.000Z',\n    endDate: `1970-02-0${idx + 1}T00:00:00.000Z`,\n    crossCheckEndDate: '1970-01-01T00:00:00.000Z',\n    maxScore: idx + 100,\n    scoreWeight: 0.2,\n    organizer: {\n      id: idx,\n      name: '',\n      githubId: `organizer ${idx}`,\n    },\n    status: 'available',\n    score: idx + 20,\n    tag: Object.values(CourseScheduleItemDtoTagEnum)[idx] as CourseScheduleItemDtoTagEnum,\n    descriptionUrl: 'task-description-url',\n  }));\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/NextEventCard/NextEventCard.tsx",
    "content": "import CommonCard from '../CommonDashboardCard';\nimport { Typography, Table } from 'antd';\nimport { getAvailableTasksColumns } from './renderers';\nimport { CourseScheduleItemDto } from '@client/api';\n\nimport styles from './NextEventCard.module.css';\n\nconst { Link } = Typography;\n\ntype Props = {\n  nextEvents: CourseScheduleItemDto[];\n  courseAlias: string;\n};\n\nfunction NextEventCard({ nextEvents, courseAlias }: Props) {\n  const columns = getAvailableTasksColumns();\n\n  return (\n    <>\n      <CommonCard\n        title=\"Available Tasks\"\n        extra={<Link href={`/course/schedule?course=${courseAlias}`}>View all</Link>}\n        content={\n          <Table\n            locale={{\n              // disable default tooltips on sortable columns\n              triggerDesc: undefined,\n              triggerAsc: undefined,\n              cancelSort: undefined,\n            }}\n            pagination={false}\n            dataSource={nextEvents}\n            rowKey=\"id\"\n            size=\"middle\"\n            columns={columns}\n            className={styles.table}\n          />\n        }\n      />\n    </>\n  );\n}\n\nexport default NextEventCard;\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/NextEventCard/index.tsx",
    "content": "export { default as NextEventCard } from './NextEventCard';\nexport * from './renderers';\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/NextEventCard/renderers.tsx",
    "content": "import { CourseScheduleItemDto } from '@client/api';\nimport { ColumnType } from 'antd/lib/table';\nimport { coloredDateRenderer, renderTask } from '@client/shared/components/Table';\nimport { renderTagWithStyle } from '@client/modules/Schedule/components/TableView/renderers';\nimport CalendarOutlined from '@ant-design/icons/CalendarOutlined';\nimport { Space, Typography } from 'antd';\n\nexport enum AvailableTasksColumnKey {\n  Name = 'name',\n  Type = 'type',\n  EndDate = 'end-date',\n}\n\nexport function getAvailableTasksColumns(): ColumnType<CourseScheduleItemDto>[] {\n  return [\n    {\n      key: AvailableTasksColumnKey.Name,\n      dataIndex: 'name',\n      render: (name, item) => renderTask(name, item.descriptionUrl),\n    },\n    {\n      key: AvailableTasksColumnKey.Type,\n      dataIndex: 'tag',\n      align: 'center',\n      render: (tag: CourseScheduleItemDto['tag']) => renderTagWithStyle(tag),\n    },\n    {\n      key: AvailableTasksColumnKey.EndDate,\n      dataIndex: 'endDate',\n      align: 'right',\n      render: renderEndDate,\n    },\n  ];\n}\n\nfunction renderEndDate(value: string, row: CourseScheduleItemDto) {\n  if (!value && !row.endDate) return;\n\n  const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const date = coloredDateRenderer(timeZone, 'MMM DD HH:mm', 'end', 'Recommended deadline')(value, row);\n\n  return (\n    <Space wrap style={{ justifyContent: 'end' }}>\n      <Typography.Text type=\"secondary\">\n        <CalendarOutlined />\n        &nbsp;Due by:\n      </Typography.Text>\n      {date}\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/RepositoryCard.tsx",
    "content": "import { GithubFilled, WarningTwoTone } from '@ant-design/icons';\nimport { Button, Col, Modal, Row, Spin, theme, Typography } from 'antd';\nimport { useLoading } from '@client/components/useLoading';\nimport CommonCard from './CommonDashboardCard';\n\ntype Props = {\n  url?: string;\n  githubId: string;\n  onSendInviteRepository: (githubId: string) => Promise<void>;\n  onUpdateUrl: () => void;\n};\n\nconst getGithubRepoName = (url: string | null | undefined) => (url ? (url.split('/').pop() ?? '') : '');\n\nexport function RepositoryCard(props: Props) {\n  const { Text, Paragraph } = Typography;\n  const { url, githubId, onSendInviteRepository, onUpdateUrl } = props;\n  const repoName = getGithubRepoName(url);\n  const hasRepo = !!url;\n  const [loading, withLoading] = useLoading(false);\n  const [modal, contextHolder] = Modal.useModal();\n  const { token } = theme.useToken();\n\n  const showInformation = () => {\n    modal.info({\n      okText: 'Got it',\n      icon: <WarningTwoTone twoToneColor={token.colorWarning} />,\n      title: <h3 style={{ color: token.colorWarning }}>Important</h3>,\n      content: (\n        <div>\n          <p>GitHub will automatically send you an invite to access your private repository.</p>\n          <p>\n            The invite comes to email specified when you registered on GitHub (you can find it here{' '}\n            <a href=\"https://github.com/settings/emails\">https://github.com/settings/emails</a>)\n          </p>\n          <p>\n            <b>Your next steps are:</b>\n            <ul>\n              <li>Go to your email box</li>\n              <li>\n                Find the <b>invitation email</b> from GitHub\n              </li>\n              <li>Accept invitation</li>\n            </ul>\n          </p>\n        </div>\n      ),\n    });\n  };\n\n  const handleSubmit = withLoading(async () => {\n    await onSendInviteRepository(githubId);\n    const shouldShowInformation = !hasRepo;\n    if (shouldShowInformation) {\n      showInformation();\n    }\n    onUpdateUrl();\n  });\n\n  return (\n    <Spin spinning={loading}>\n      {contextHolder}\n      <CommonCard\n        title=\"Your repository\"\n        content={\n          <div style={{ display: 'flex', justifyContent: 'space-around', flexWrap: 'wrap', alignItems: 'center' }}>\n            <Row>\n              <Col style={{ marginBottom: 7, textAlign: 'center' }}>\n                {url ? (\n                  <div style={{ marginBottom: 7 }}>\n                    <Text strong>{'Your repository:'}</Text>\n                    <Paragraph style={{ textAlign: 'center', marginBottom: 10 }}>\n                      <a target=\"_blank\" href={url} style={{ fontSize: 16 }}>\n                        <GithubFilled /> {repoName}\n                      </a>\n                    </Paragraph>\n                  </div>\n                ) : (\n                  <div style={{ marginBottom: 7 }}>\n                    <Text style={{ color: token.colorWarning }} strong>\n                      {`Your repository hasn't been created yet`}\n                    </Text>\n                  </div>\n                )}\n                <Button style={{ marginBottom: 7 }} type={url ? 'default' : 'primary'} onClick={handleSubmit}>\n                  {url ? 'Fix repository' : 'Create repository'}\n                </Button>\n              </Col>\n            </Row>\n          </div>\n        }\n      />\n    </Spin>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/SubmitTaskSolution/SubmitTaskSolution.tsx",
    "content": "import { Button, Form, Input } from 'antd';\nimport { CourseTaskSelect } from '@client/shared/components/Forms';\nimport { ModalSubmitForm } from '@client/shared/components/Forms';\nimport { urlPattern } from '@client/services/validators';\nimport { useSubmitTaskSolution } from '@client/modules/StudentDashboard/hooks/useSubmitTaskSolution';\n\ninterface TaskSolutionModalProps {\n  courseId: number;\n}\n\nfunction SubmitTaskSolution({ courseId }: TaskSolutionModalProps) {\n  const { state, closeModal, showModal, saveSolution, setSolutionUrl } = useSubmitTaskSolution(courseId);\n\n  return (\n    <>\n      <Button onClick={showModal} type=\"primary\">\n        Submit Task\n      </Button>\n      <ModalSubmitForm\n        title=\"Submit Task For Mentor Review\"\n        data={state}\n        submitted={state?.submitted}\n        submit={saveSolution}\n        close={closeModal}\n        errorText={state?.errorText}\n        loading={state?.loading}\n        successText=\"Your task has been successfully submitted for review\"\n      >\n        <CourseTaskSelect groupBy=\"deadline\" data={state?.data?.courseTasks ?? []} onChange={setSolutionUrl} />\n        <Form.Item\n          label=\"Add a solution link\"\n          name=\"url\"\n          required\n          rules={[{ message: 'Please enter valid URL', pattern: urlPattern }]}\n        >\n          <Input />\n        </Form.Item>\n      </ModalSubmitForm>\n    </>\n  );\n}\n\nexport default SubmitTaskSolution;\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/SubmitTaskSolution/index.ts",
    "content": "export { default as SubmitTaskSolution } from './SubmitTaskSolution';\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/TasksChart.tsx",
    "content": "import { Pie, PieConfig } from '@ant-design/plots';\nimport { getTaskStatusColor } from '@client/modules/Schedule';\nimport { CourseScheduleItemDtoStatusEnum } from '@client/api';\nimport { useMemo } from 'react';\nimport capitalize from 'lodash/capitalize';\n\ntype Item = { status: string; value: number };\n\ntype Props = {\n  data: Item[];\n  onItemSelected: (item: Item) => void;\n};\n\nexport function TasksChart({ data, onItemSelected }: Props) {\n  const statusColors = useMemo(\n    () => data.map(d => getTaskStatusColor(d.status as CourseScheduleItemDtoStatusEnum)),\n    [data],\n  );\n\n  const config: PieConfig = {\n    data,\n    angleField: 'value',\n    colorField: 'status',\n    radius: 1,\n    innerRadius: 0.78,\n    autoFit: true,\n    label: {\n      text: 'value',\n      position: 'inside',\n      style: {\n        textAlign: 'center',\n        fontSize: 12,\n        fontWeight: 400,\n      },\n    },\n    scale: {\n      color: {\n        range: statusColors,\n      },\n    },\n    legend: {\n      color: {\n        position: 'right',\n        layout: { justifyContent: 'center' },\n        itemLabelText: (datum: Record<string, string>) => {\n          const item = data.find(d => d.status === datum.status);\n          return `${capitalize(datum.status)} ${item?.value ?? ''}`;\n        },\n        itemLabelFontSize: 14,\n        itemLabelFontFamily: 'sans-serif',\n        rowPadding: 20,\n      },\n    },\n    interaction: {\n      elementHighlight: true,\n    },\n    onEvent: (_chart, event) => {\n      if (event.type === 'element:click') {\n        const clickedData = (event as unknown as { data: { data: Item } }).data?.data;\n        if (clickedData) {\n          onItemSelected(clickedData);\n        }\n      }\n    },\n  };\n  return <Pie {...config} />;\n}\n\nexport default TasksChart;\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/TasksStatsCard.tsx",
    "content": "import { CourseScheduleItemDto, CourseScheduleItemDtoStatusEnum } from '@client/api';\nimport dynamic from 'next/dynamic';\nimport { useRouter } from 'next/router';\nimport { useCallback, useEffect, useMemo, useState } from 'react';\nimport { getQueryString } from '@client/shared/utils/queryParams-utils';\nimport CommonCard from './CommonDashboardCard';\nimport { TasksStatsModal } from './TasksStatsModal';\n\nconst TasksChart = dynamic(() => import('./TasksChart'), { ssr: false });\n\nexport type TaskStat = CourseScheduleItemDto & { comment?: string; githubPrUri?: string };\n\ntype Props = {\n  tasksByStatus: Record<CourseScheduleItemDtoStatusEnum, TaskStat[]>;\n  courseName: string;\n};\n\nexport function TasksStatsCard(props: Props) {\n  const [selectedStatus, setSelectedStatus] = useState<CourseScheduleItemDtoStatusEnum | null>(null);\n\n  const { courseName, tasksByStatus } = props;\n\n  const router = useRouter();\n  const queryStatType = router.query.statType ? (router.query.statType as string) : null;\n\n  useEffect(() => {\n    if (queryStatType) {\n      showTasksStatsModal(queryStatType);\n    }\n  }, [queryStatType]);\n\n  function updateUrl(statType?: string) {\n    const query = { ...router.query };\n    if (statType) {\n      query.statType = statType;\n    } else {\n      delete query.statType;\n    }\n    const url = `${router.route}${getQueryString(query)}`;\n    router.replace(url);\n  }\n\n  const showTasksStatsModal = useCallback((name: string) => {\n    const status = name as CourseScheduleItemDtoStatusEnum;\n    if (!Object.values(CourseScheduleItemDtoStatusEnum).includes(status)) return;\n\n    setSelectedStatus(status);\n  }, []);\n\n  const hideTasksStatsModal = () => {\n    setSelectedStatus(null);\n    updateUrl();\n  };\n\n  const chartData = useMemo(\n    () =>\n      Object.entries(tasksByStatus).map(([status, tasks]) => {\n        return {\n          value: tasks.length,\n          status,\n        };\n      }),\n    [tasksByStatus],\n  );\n\n  return (\n    <>\n      <TasksStatsModal\n        courseName={courseName}\n        tableName={`${selectedStatus} tasks`}\n        tasks={selectedStatus ? tasksByStatus[selectedStatus] : []}\n        isVisible={!!selectedStatus}\n        onHide={hideTasksStatsModal}\n      />\n      <CommonCard\n        title=\"Tasks Statistics\"\n        content={\n          <div style={{ height: '220px' }}>\n            <TasksChart data={chartData} onItemSelected={data => updateUrl(data.status)} />\n          </div>\n        }\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/TasksStatsModal.tsx",
    "content": "import { Modal, Table, Typography } from 'antd';\nimport { dateTimeRenderer, dateRenderer } from '@client/shared/components/Table';\nimport { TaskStat } from './TasksStatsCard';\n\nconst { Text } = Typography;\n\ntype Props = {\n  courseName: string;\n  tableName: string;\n  tasks: TaskStat[];\n  isVisible: boolean;\n  onHide: () => void;\n};\n\nexport function TasksStatsModal(props: Props) {\n  const { tableName, tasks, courseName, isVisible, onHide } = props;\n  const columnWidth = 150;\n  // where 500 is approximate sum of columns\n  const tableWidth = 2 * columnWidth + 500;\n\n  return (\n    <Modal\n      title={`${courseName} statistics`}\n      open={isVisible}\n      onCancel={onHide}\n      footer={null}\n      width={'90%'}\n      style={{ top: 30 }}\n    >\n      <p style={{ textAlign: 'center', fontWeight: 700 }}>{tableName.toUpperCase()}</p>\n      <Table\n        dataSource={tasks}\n        size=\"small\"\n        rowKey=\"id\"\n        scroll={{ x: tableWidth, y: 'calc(100vh - 250px)' }}\n        pagination={false}\n        columns={[\n          {\n            title: 'Task',\n            fixed: 'left',\n            dataIndex: 'name',\n            render: (task: string, { descriptionUrl }: any) =>\n              descriptionUrl ? (\n                <a href={descriptionUrl} target=\"_blank\">\n                  {task}\n                </a>\n              ) : (\n                task\n              ),\n          },\n          {\n            title: 'Score / Max',\n            width: 120,\n            dataIndex: 'score',\n            render: (score: string, { maxScore }: any) => (\n              <>\n                <Text strong>{score != null ? score : '-'}</Text> / {maxScore ? maxScore : '-'}\n              </>\n            ),\n          },\n          {\n            title: 'Weight',\n            width: 140,\n            dataIndex: 'scoreWeight',\n            render: (scoreWeight: number, { score }: any) => (\n              <Text>\n                {Number(score)} * {scoreWeight}\n                {score ? (\n                  <Text>\n                    {' '}\n                    = <Text strong>{(Number(score) * scoreWeight).toFixed(2)}</Text>\n                  </Text>\n                ) : (\n                  ''\n                )}\n              </Text>\n            ),\n          },\n          {\n            title: 'Start date',\n            width: 140,\n            dataIndex: 'startDate',\n            render: (startDate: string) => <Text>{dateRenderer(startDate)}</Text>,\n          },\n          {\n            title: 'Deadline',\n            width: 140,\n            dataIndex: 'endDate',\n            render: (endDate: string) => <Text>{dateTimeRenderer(endDate)}</Text>,\n          },\n          {\n            title: 'Comment',\n            dataIndex: 'comment',\n            ellipsis: false,\n          },\n          {\n            title: 'GitHub PR Url',\n            dataIndex: 'githubPrUri',\n            render: (uri: string) => (uri ? <a href={uri}>PR</a> : uri),\n            ellipsis: true,\n            width: 120,\n          },\n        ]}\n      />\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/components/index.ts",
    "content": "export * from './CommonDashboardCard';\nexport * from './MentorCard';\nexport * from './MainStatsCard';\nexport * from './TasksStatsCard';\nexport * from './TasksStatsModal';\nexport * from './NextEventCard';\nexport * from './NextEventCard/renderers';\nexport * from './RepositoryCard';\nexport * from './AvailableReviewCard';\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/hooks/useDashboardData.ts",
    "content": "import groupBy from 'lodash/groupBy';\nimport omitBy from 'lodash/omitBy';\n\nimport { useRequest } from 'ahooks';\nimport {\n  CourseScheduleItemDtoStatusEnum,\n  CourseScheduleItemDtoTypeEnum,\n  CoursesScheduleApi,\n  CoursesTasksApi,\n  CourseStatsApi,\n  StudentsApi,\n} from '@client/api';\nimport { TaskStat } from '@client/modules/StudentDashboard/components';\nimport { UserService } from '@client/services/user';\n\nconst coursesTasksApi = new CoursesTasksApi();\nconst coursesStatsApi = new CourseStatsApi();\nconst userService = new UserService();\nconst studentsApi = new StudentsApi();\n\nexport function useDashboardData(courseId: number, githubId: string) {\n  return useRequest(async () => {\n    const [\n      { data: studentSummary },\n      { data: courseTasks },\n      statisticsCourses,\n      { data: courseStats },\n      { data: scheduleItems },\n      { data: availableReviews },\n    ] = await Promise.all([\n      studentsApi.getStudentSummary(courseId, githubId),\n      coursesTasksApi.getCourseTasks(courseId),\n      userService.getProfileInfo(githubId),\n      coursesStatsApi.getCourseStats(courseId),\n      new CoursesScheduleApi().getSchedule(courseId),\n      coursesTasksApi.getAvailableCrossCheckReviewStats(courseId),\n    ]);\n\n    const nextEvents = scheduleItems.filter(({ status }) => status === CourseScheduleItemDtoStatusEnum.Available);\n\n    const tasksDetailCurrentCourse =\n      statisticsCourses.studentStats?.find(currentCourse => currentCourse.courseId === courseId)?.tasks ?? [];\n\n    const tasksByStatus = omitBy(\n      groupBy(\n        scheduleItems\n          .filter(scheduleItem => scheduleItem.type === CourseScheduleItemDtoTypeEnum.CourseTask)\n          .map(task => {\n            const { comment, githubPrUri } =\n              tasksDetailCurrentCourse.find(taskDetail => taskDetail.name === task.name) ?? {};\n\n            return { ...task, comment, githubPrUri };\n          }),\n        'status',\n      ),\n      (_, status) => status === CourseScheduleItemDtoStatusEnum.Archived,\n    ) as Record<CourseScheduleItemDtoStatusEnum, TaskStat[]>;\n\n    const maxCourseScore = Math.round(\n      courseTasks.reduce((score, task) => score + (task.maxScore ?? 0) * task.scoreWeight, 0),\n    );\n\n    return {\n      studentSummary,\n      courseTasks,\n      courseStats,\n      scheduleItems,\n      availableReviews,\n      nextEvents,\n      tasksDetailCurrentCourse,\n      tasksByStatus,\n      maxCourseScore,\n    };\n  });\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/hooks/useSubmitTaskSolution.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\nimport { CoursesTasksApi, CoursesTaskSolutionsApi, CourseTaskDto } from '@client/api';\nimport { AxiosError, AxiosResponse } from 'axios';\nimport { useSubmitTaskSolution } from './useSubmitTaskSolution';\n\nconst COURSE_ID = 10;\n\nconst resolvedMock = {\n  status: 200,\n  statusText: 'OK',\n  headers: {},\n  config: {},\n  data: {},\n} as unknown as AxiosResponse;\n\nconst rejectedMock: AxiosError = {\n  message: 'Bad Response',\n  code: '500',\n  config: {},\n  response: {\n    status: 500,\n    statusText: 'Bad Response',\n  },\n} as AxiosError;\n\ndescribe('useSubmitTaskSolution', () => {\n  it('should show modal with course tasks', async () => {\n    vi.spyOn(CoursesTasksApi.prototype, 'getCourseTasksWithStudentSolution').mockResolvedValue({\n      ...resolvedMock,\n      data: generateCourseTasks(),\n    });\n    const { result } = renderHook(() => useSubmitTaskSolution(COURSE_ID));\n\n    await act(async () => {\n      await result.current.showModal();\n    });\n\n    expect(result.current.state?.data?.courseTasks).toHaveLength(3);\n  });\n\n  it('should save solution', async () => {\n    vi.spyOn(CoursesTaskSolutionsApi.prototype, 'createTaskSolution').mockResolvedValue(resolvedMock);\n    const { result } = renderHook(() => useSubmitTaskSolution(COURSE_ID));\n\n    await act(async () => {\n      await result.current.saveSolution({ courseTaskId: 100, url: 'some-url' });\n    });\n\n    expect(result.current.state?.submitted).toBeTruthy();\n  });\n\n  it('should close modal', () => {\n    const { result } = renderHook(() => useSubmitTaskSolution(COURSE_ID));\n\n    act(() => {\n      result.current.closeModal();\n    });\n\n    expect(result.current.state).toBeNull();\n  });\n\n  describe('should show error', () => {\n    it('when getCourseTasksWithStudentSolution request failed', async () => {\n      vi.spyOn(CoursesTasksApi.prototype, 'getCourseTasksWithStudentSolution').mockRejectedValue(rejectedMock);\n      const { result } = renderHook(() => useSubmitTaskSolution(COURSE_ID));\n\n      await act(async () => {\n        await result.current.showModal();\n      });\n\n      expect(result.current.state?.errorText).toBeDefined();\n    });\n\n    it('when createTaskSolution request failed', async () => {\n      vi.spyOn(CoursesTaskSolutionsApi.prototype, 'createTaskSolution').mockRejectedValue(rejectedMock);\n      const { result } = renderHook(() => useSubmitTaskSolution(COURSE_ID));\n\n      await act(async () => {\n        await result.current.saveSolution({ courseTaskId: 100, url: 'some-url' });\n      });\n\n      expect(result.current.state?.errorText).toBeDefined();\n    });\n  });\n});\n\nfunction generateCourseTasks(count = 3): CourseTaskDto[] {\n  return new Array(count).fill({}).map((_, idx) => ({\n    id: idx,\n    checker: 'mentor',\n  })) as CourseTaskDto[];\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/hooks/useSubmitTaskSolution.ts",
    "content": "import { AxiosError } from 'axios';\nimport { useReducer } from 'react';\nimport {\n  CourseTaskDto,\n  CoursesTasksApi,\n  CheckerEnum,\n  CoursesTaskSolutionsApi,\n  CourseTaskDtoTypeEnum,\n} from '@client/api';\n\ntype Action = {\n  type: 'loading' | 'open' | 'close' | 'error' | 'submit' | 'set-solution-url';\n  state?: State;\n};\n\nexport type State = {\n  errorText?: string;\n  submitted?: boolean;\n  data?: { courseTasks: CourseTaskDto[] };\n  selectedSolutionUrl?: string;\n  loading?: boolean;\n} | null;\n\nfunction reducer(state: State, action: Action): State {\n  switch (action.type) {\n    case 'loading':\n      return { loading: true };\n    case 'open':\n      return { loading: false, data: action.state?.data };\n    case 'submit':\n      return { submitted: true, data: state?.data };\n    case 'set-solution-url':\n      return { selectedSolutionUrl: action.state?.selectedSolutionUrl, data: state?.data };\n    case 'close':\n      return null;\n    case 'error':\n      return { errorText: action.state?.errorText, data: state?.data };\n    default:\n      return state;\n  }\n}\n\nexport function useSubmitTaskSolution(courseId: number) {\n  const [state, dispatch] = useReducer(reducer, null);\n\n  const showModal = async () => {\n    try {\n      dispatch({ type: 'loading' });\n      const coursesTasksApi = new CoursesTasksApi();\n      const { data } = await coursesTasksApi.getCourseTasksWithStudentSolution(courseId);\n      const courseTasks = data.filter(\n        item =>\n          item.checker === CheckerEnum.Mentor &&\n          item.type != CourseTaskDtoTypeEnum.Selfeducation &&\n          item.type != CourseTaskDtoTypeEnum.StageInterview &&\n          item.type != CourseTaskDtoTypeEnum.Interview,\n      );\n      dispatch({ type: 'open', state: { data: { courseTasks } } });\n    } catch (err) {\n      const error = err as AxiosError;\n      dispatch({ type: 'error', state: { errorText: (error.response?.data as Error)?.message ?? error.message } });\n    }\n  };\n\n  const saveSolution = async (values: { courseTaskId: number; url: string }) => {\n    try {\n      const api = new CoursesTaskSolutionsApi();\n      await api.createTaskSolution(courseId, values.courseTaskId, { url: values.url });\n      dispatch({ type: 'submit' });\n    } catch (err) {\n      const error = err as AxiosError;\n      dispatch({ type: 'error', state: { errorText: (error.response?.data as Error)?.message ?? error.message } });\n    }\n  };\n\n  const setSolutionUrl = (courseTaskId: number) => {\n    const courseTask = state?.data?.courseTasks.find(courseTask => courseTask.id === courseTaskId);\n    let url = '';\n    if (courseTask?.taskSolutions) {\n      const [taskSolution] = Object.values(courseTask?.taskSolutions);\n      url = taskSolution.url;\n    }\n    dispatch({ type: 'set-solution-url', state: { selectedSolutionUrl: url ?? '' } });\n  };\n\n  const closeModal = () => dispatch({ type: 'close' });\n\n  return {\n    state,\n    saveSolution,\n    setSolutionUrl,\n    showModal,\n    closeModal,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/StudentDashboard/index.ts",
    "content": "export * from './hooks/useDashboardData';\nexport * from './hooks/useSubmitTaskSolution';\nexport * from './components';\n"
  },
  {
    "path": "client/src/modules/Students/Pages/Students.tsx",
    "content": "import { Drawer, TablePaginationConfig, message } from 'antd';\nimport { FilterValue } from 'antd/es/table/interface';\nimport { StudentsApi, UserStudentDto } from '@client/api';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { useLoading } from '@client/components/useLoading';\nimport { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport StudentsTable from '../components/StudentsTable';\nimport { ColumnKey } from '../components/StudentsTable/renderers';\nimport { StudentInfo } from '../components/StudentInfo';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\nconst studentsApi = new StudentsApi();\n\ntype StudentsState = {\n  content: UserStudentDto[];\n  pagination: IPaginationInfo;\n};\n\nexport const Students = () => {\n  const { courses } = useActiveCourseContext();\n  const [students, setStudents] = useState<StudentsState>({\n    content: [],\n    pagination: { current: 1, pageSize: 20 },\n  });\n  const [activeStudent, setActiveStudent] = useState<UserStudentDto | null>(null);\n\n  const [loading, withLoading] = useLoading(false);\n\n  const getStudents = withLoading(\n    async (pagination: TablePaginationConfig, filters?: Record<ColumnKey, FilterValue | null>) => {\n      try {\n        const { student, country, city, onGoingCourses, previousCourses } = filters || {};\n        const { data } = await studentsApi.getUserStudents(\n          String(pagination.current),\n          String(pagination.pageSize),\n          student?.toString(),\n          country?.toString(),\n          city?.toString(),\n          onGoingCourses?.toString(),\n          previousCourses?.toString(),\n        );\n        setStudents({ ...students, ...data });\n      } catch {\n        message.error('Failed to load students list. Please try again.');\n      }\n    },\n  );\n\n  useAsync(async () => await getStudents(students.pagination), []);\n\n  return (\n    <AdminPageLayout loading={loading} title=\"Students List\" courses={courses}>\n      <StudentsTable\n        handleChange={getStudents}\n        loading={loading}\n        content={students.content}\n        pagination={students.pagination}\n        courses={courses}\n        setActiveStudent={setActiveStudent}\n      />\n      <Drawer mask={false} title=\"Student Details\" onClose={() => setActiveStudent(null)} open={!!activeStudent}>\n        {activeStudent && <StudentInfo student={activeStudent} />}\n      </Drawer>\n    </AdminPageLayout>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Students/components/CourseItem/index.tsx",
    "content": "import { Flex, List, Space, Typography } from 'antd';\nimport { SafetyCertificateTwoTone } from '@ant-design/icons';\nimport { UserStudentCourseDto } from '@client/api';\n\nconst { Text, Paragraph, Link } = Typography;\n\ntype Props = {\n  course: UserStudentCourseDto;\n};\n\nexport const CourseItem = ({ course }: Props) => {\n  const { name, certificateId, mentorGithubId, mentorFullName, rank, totalScore } = course;\n\n  return (\n    <List.Item>\n      <Flex vertical gap={4}>\n        <Text strong>{name}</Text>\n        {certificateId && (\n          <Paragraph>\n            <SafetyCertificateTwoTone twoToneColor=\"#52c41a\" />{' '}\n            <Link target=\"__blank\" href={`/certificate/${certificateId}`}>\n              Certificate\n            </Link>\n          </Paragraph>\n        )}\n        {mentorGithubId && (\n          <Paragraph>\n            Mentor: <Link href={`/profile?githubId=${mentorGithubId}`}>{mentorFullName}</Link>\n          </Paragraph>\n        )}\n        <Space wrap>\n          {rank && <Paragraph>Position: {rank}</Paragraph>}\n          <Paragraph>Score: {totalScore}</Paragraph>\n        </Space>\n      </Flex>\n    </List.Item>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Students/components/StudentInfo/index.tsx",
    "content": "import { Avatar, Col, Collapse, List, Row, Space, Typography } from 'antd';\nimport GithubFilled from '@ant-design/icons/GithubFilled';\nimport MailOutlined from '@ant-design/icons/MailOutlined';\nimport LinkedinOutlined from '@ant-design/icons/LinkedinOutlined';\nimport SkypeOutlined from '@ant-design/icons/SkypeOutlined';\nimport PhoneOutlined from '@ant-design/icons/PhoneOutlined';\nimport SendOutlined from '@ant-design/icons/SendOutlined';\nimport { DiscordOutlined } from '@client/shared/components/Icons/DiscordOutlined';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { UserStudentDto } from '@client/api';\nimport { CourseItem } from '../CourseItem';\n\ntype Props = {\n  student: UserStudentDto;\n};\n\nconst { Panel } = Collapse;\n\nconst { Text } = Typography;\n\nexport function StudentInfo(props: Props) {\n  const { student } = props;\n  const { githubId, fullName } = student;\n  const hasName = fullName && fullName !== '(Empty)';\n  const location = [student.city, student.country].filter(Boolean).join(', ');\n\n  const UserContacts = (student: UserStudentDto) => {\n    const contacts = [\n      { type: 'Email', value: student.contactsEmail, icon: <MailOutlined /> },\n      { type: 'Telegram', value: student.contactsTelegram, icon: <SendOutlined rotate={-45} /> },\n      { type: 'LinkedIn', value: student.contactsLinkedIn, icon: <LinkedinOutlined /> },\n      { type: 'Skype', value: student.contactsSkype, icon: <SkypeOutlined /> },\n      { type: 'Phone', value: student.contactsPhone, icon: <PhoneOutlined /> },\n      { type: 'Discord', value: student.discord?.username, icon: <DiscordOutlined /> },\n    ];\n    return contacts.filter(contact => contact.value);\n  };\n\n  return (\n    <Space direction=\"vertical\" style={{ width: '100%' }}>\n      <Row align=\"middle\" gutter={24}>\n        <Col>\n          <GithubAvatar githubId={githubId} size={48} />\n        </Col>\n        <Col>\n          {hasName && (\n            <Row>\n              <Typography.Link target=\"_blank\" href={`/profile?githubId=${githubId}`} strong>\n                {fullName}\n              </Typography.Link>\n            </Row>\n          )}\n          <Row>\n            <Typography.Link target=\"_blank\" href={`https://github.com/${githubId}`}>\n              <GithubFilled /> {githubId}\n            </Typography.Link>\n          </Row>\n        </Col>\n      </Row>\n      <Row>\n        <Col span={24}>\n          <Row>\n            <Text type=\"secondary\">Location</Text>\n          </Row>\n          <Row>\n            <Text>{location}</Text>\n          </Row>\n        </Col>\n      </Row>\n      <Collapse defaultActiveKey={['courses']}>\n        <Panel header=\"Contacts\" key=\"contacts\">\n          <List\n            itemLayout=\"horizontal\"\n            dataSource={UserContacts(student)}\n            renderItem={item => (\n              <List.Item>\n                <List.Item.Meta avatar={<Avatar icon={item.icon} />} title={item.type} description={item.value} />\n              </List.Item>\n            )}\n          />\n        </Panel>\n        <Panel header=\"Courses\" key=\"courses\">\n          <List\n            itemLayout=\"horizontal\"\n            dataSource={[...student.previousCourses, ...student.onGoingCourses].sort(course =>\n              course.hasCertificate ? -1 : 1,\n            )}\n            renderItem={course => <CourseItem course={course} />}\n          />\n        </Panel>\n      </Collapse>\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Students/components/StudentsTable/index.tsx",
    "content": "import { Table, TablePaginationConfig, TableProps } from 'antd';\nimport { CourseDto, UserStudentDto } from '@client/api';\nimport { getColumns } from './renderers';\n\ntype Props = {\n  content: UserStudentDto[];\n  pagination: false | TablePaginationConfig;\n  handleChange: TableProps<UserStudentDto>['onChange'];\n  loading: boolean;\n  courses: CourseDto[];\n  setActiveStudent: (student: UserStudentDto | null) => void;\n};\n\nexport default function StudentsTable({\n  content,\n  pagination,\n  handleChange,\n  loading,\n  courses,\n  setActiveStudent,\n}: Props) {\n  return (\n    <Table<UserStudentDto>\n      showHeader\n      dataSource={content}\n      size=\"small\"\n      columns={getColumns(courses)}\n      onChange={handleChange}\n      rowKey=\"id\"\n      pagination={pagination}\n      onRow={record => {\n        return {\n          onClick: () => setActiveStudent(record),\n        };\n      }}\n      loading={loading}\n      bordered\n      /**\n       * @see\n       * dirty-hack to fix x-scroll in antd table\n       */\n      scroll={{ y: 'calc(100vh - 260px)', x: '' }}\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Students/components/StudentsTable/renderers.tsx",
    "content": "import { Flex, Space, Tag, Tooltip } from 'antd';\nimport { ColumnsType } from 'antd/lib/table';\nimport { CourseDto, UserStudentCourseDto, UserStudentDto } from '@client/api';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { getColumnSearchProps } from '@client/shared/components/Table';\n\nexport enum ColumnKey {\n  Student = 'student',\n  OnGoingCourses = 'onGoingCourses',\n  PreviousCourses = 'previousCourses',\n  Country = 'country',\n  City = 'city',\n  Languages = 'languages',\n}\n\nenum ColumnName {\n  Student = 'Student',\n  OnGoingCourses = 'Ongoing Courses',\n  PreviousCourses = 'Previous Courses',\n  Country = 'Country',\n  City = 'City',\n  Languages = 'Languages',\n}\n\nconst getSearchProps = (key: string) => ({\n  ...getColumnSearchProps(key),\n  onFilter: undefined,\n});\n\nconst coursesRenderer = (courses: UserStudentCourseDto[]) => {\n  const visibleCourses = courses.slice(0, 3);\n  const hiddenCourses = courses.slice(3);\n  const hiddenCoursesCount = hiddenCourses.length;\n  const hasCertifiedHiddenCourse = hiddenCourses.some(course => course.hasCertificate);\n\n  return (\n    <Space wrap size=\"small\">\n      {visibleCourses.map(course => (\n        <Tag color={course.hasCertificate ? 'green' : undefined} key={course.alias}>\n          {course.alias}\n        </Tag>\n      ))}\n      {hiddenCoursesCount > 0 && (\n        <Tooltip\n          title={\n            <Space wrap>\n              {hiddenCourses.map(course => (\n                <Tag color={course.hasCertificate ? 'green' : undefined} key={course.alias}>\n                  {course.alias}\n                </Tag>\n              ))}\n            </Space>\n          }\n          color=\"#fff\"\n        >\n          <Tag color={hasCertifiedHiddenCourse ? 'green' : undefined}>+{hiddenCoursesCount} more</Tag>\n        </Tooltip>\n      )}\n    </Space>\n  );\n};\n\nexport const getColumns = (courses: CourseDto[]): ColumnsType<UserStudentDto> => [\n  {\n    key: ColumnKey.Student,\n    title: ColumnName.Student,\n    dataIndex: ColumnKey.Student,\n    width: 225,\n    render: (_v, record) => <GithubUserLink value={record.githubId} fullName={record.fullName} />,\n    ...getSearchProps(ColumnKey.Student),\n  },\n  {\n    title: ColumnName.OnGoingCourses,\n    key: ColumnKey.OnGoingCourses,\n    dataIndex: ColumnKey.OnGoingCourses,\n    width: 400,\n    render: coursesRenderer,\n    filters: courses.filter(course => !course.completed).map(course => ({ text: course.alias, value: course.id })),\n    filterSearch: true,\n  },\n  {\n    title: ColumnName.PreviousCourses,\n    key: ColumnKey.PreviousCourses,\n    dataIndex: ColumnKey.PreviousCourses,\n    render: coursesRenderer,\n    width: 400,\n    filters: courses.filter(course => course.completed).map(course => ({ text: course.alias, value: course.id })),\n    filterSearch: true,\n  },\n  {\n    title: ColumnName.Country,\n    key: ColumnKey.Country,\n    dataIndex: ColumnKey.Country,\n    width: 200,\n    ...getSearchProps(ColumnKey.Country),\n  },\n  {\n    title: ColumnName.City,\n    key: ColumnKey.City,\n    dataIndex: ColumnKey.City,\n    width: 200,\n    ...getSearchProps(ColumnKey.City),\n  },\n  {\n    title: ColumnName.Languages,\n    dataIndex: ColumnKey.Languages,\n    key: ColumnKey.Languages,\n    width: 150,\n    render: (languages: string[]) => (\n      <Flex wrap=\"wrap\">\n        {languages.map(language => (\n          <Tag key={language}>{language}</Tag>\n        ))}\n      </Flex>\n    ),\n  },\n];\n"
  },
  {
    "path": "client/src/modules/Tasks/components/CrossCheckTaskCriteriaPanel/CrossCheckTaskCriteriaPanel.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { CriteriaDto } from '@client/api';\nimport { LABELS } from '@client/modules/Tasks/constants';\nimport { CrossCheckTaskCriteriaPanel } from './CrossCheckTaskCriteriaPanel';\n\nconst criteriaMock: CriteriaDto = { type: 'title', text: 'Title', key: 'key', index: 1 };\n\nconst renderPanel = (dataCriteria: CriteriaDto[] = [], setDataCriteria = vi.fn()) => {\n  render(\n    <Form>\n      <CrossCheckTaskCriteriaPanel dataCriteria={dataCriteria} setDataCriteria={setDataCriteria} />\n    </Form>,\n  );\n};\n\ndescribe('Criteria For Cross-Check Task', () => {\n  test.each`\n    label\n    ${LABELS.crossCheckCriteria}\n  `('should render fields with $label label', async ({ label }) => {\n    renderPanel();\n\n    const field = await screen.findByText(label);\n    expect(field).toBeInTheDocument();\n  });\n\n  // AddCriteriaForCrossCheck\n  test('should render \"Criteria Type\" field', async () => {\n    renderPanel();\n\n    const field = await screen.findByText('Criteria Type');\n    expect(field).toBeInTheDocument();\n  });\n\n  // Divider\n  test('should render divider', async () => {\n    renderPanel([criteriaMock]);\n\n    const divider = await screen.findByRole('separator');\n    expect(divider).toBeInTheDocument();\n  });\n\n  test('should not render divider when no dataCriteria', async () => {\n    renderPanel();\n\n    const divider = screen.queryByRole('separator');\n    expect(divider).not.toBeInTheDocument();\n  });\n\n  // EditableTable\n  test('should render criteria table', async () => {\n    renderPanel([criteriaMock]);\n\n    const table = await screen.findByRole('table');\n    expect(table).toBeInTheDocument();\n  });\n\n  test('should not render criteria table when no dataCriteria', async () => {\n    renderPanel();\n\n    const table = screen.queryByRole('table');\n    expect(table).not.toBeInTheDocument();\n  });\n\n  // ExportJSONButton\n  test('should render \"Export JSON\" button', async () => {\n    renderPanel([criteriaMock]);\n\n    const button = await screen.findByRole('button', { name: /export json/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  test('should not render \"Export JSON\" button when no dataCriteria', async () => {\n    renderPanel();\n\n    const button = screen.queryByRole('button', { name: /export json/i });\n    expect(button).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Tasks/components/CrossCheckTaskCriteriaPanel/CrossCheckTaskCriteriaPanel.tsx",
    "content": "import { Divider, Flex, Form } from 'antd';\nimport { CriteriaDto } from '@client/api';\nimport {\n  addKeyAndIndex,\n  UploadCriteriaJSON,\n  AddCriteriaForCrossCheck,\n  EditableTable,\n  ExportJSONButton,\n} from '@client/modules/CrossCheck';\nimport { DeleteAllCrossCheckCriteriaButton } from '@client/modules/CrossCheck/DeleteAllCrossCheckCriteriaButton';\nimport { LABELS } from '@client/modules/Tasks/constants';\nimport { Dispatch, SetStateAction } from 'react';\n\ntype Props = {\n  dataCriteria: CriteriaDto[];\n  setDataCriteria: Dispatch<SetStateAction<CriteriaDto[]>>;\n};\n\nexport function CrossCheckTaskCriteriaPanel({ dataCriteria, setDataCriteria }: Props) {\n  const addCriteria = (criteria: CriteriaDto) => {\n    const newDataCriteria = [...dataCriteria, criteria];\n    setDataCriteria(addKeyAndIndex(newDataCriteria));\n  };\n\n  const addJSONtoCriteria = (criteria: CriteriaDto[]) => {\n    const newCriteria = [...dataCriteria, ...criteria];\n    setDataCriteria(addKeyAndIndex(newCriteria));\n  };\n\n  return (\n    <>\n      <Form.Item label={LABELS.crossCheckCriteria}>\n        <UploadCriteriaJSON onLoad={addJSONtoCriteria} />\n      </Form.Item>\n      <AddCriteriaForCrossCheck onCreate={addCriteria} />\n      {dataCriteria.length ? (\n        <>\n          <Divider />\n          <EditableTable dataCriteria={dataCriteria} setDataCriteria={setDataCriteria} />\n          <Flex gap={'10px'} justify={'flex-end'}>\n            <DeleteAllCrossCheckCriteriaButton setDataCriteria={setDataCriteria} />\n            <ExportJSONButton dataCriteria={dataCriteria} />\n          </Flex>\n        </>\n      ) : null}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Tasks/components/GitHubPanel/GitHubPanel.test.tsx",
    "content": "import { render, screen, fireEvent, waitFor } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { ERROR_MESSAGES, LABELS, PLACEHOLDERS } from '@client/modules/Tasks/constants';\nimport { GitHubPanel } from './GitHubPanel';\n\nconst renderPanel = () => {\n  render(\n    <Form>\n      <GitHubPanel />\n    </Form>,\n  );\n};\n\ndescribe('GitHub', () => {\n  test.each`\n    label\n    ${LABELS.repoUrl}\n    ${LABELS.expectedRepoName}\n  `('should render fields with $label label', async ({ label }) => {\n    renderPanel();\n\n    const field = await screen.findByText(label);\n    expect(field).toBeInTheDocument();\n  });\n\n  test('should render \"Pull Request required\" checkbox', async () => {\n    renderPanel();\n\n    const checkbox = await screen.findByRole('checkbox', { name: /pull request required/i });\n    expect(checkbox).toBeInTheDocument();\n  });\n\n  test.each`\n    placeholder\n    ${PLACEHOLDERS.sourceGithubRepoUrl}\n    ${PLACEHOLDERS.githubRepoName}\n  `('should render field with $placeholder placeholder', async ({ placeholder }) => {\n    renderPanel();\n\n    const field = await screen.findByPlaceholderText(placeholder);\n    expect(field).toBeInTheDocument();\n  });\n\n  test('should render error message on invalid source GitHub repo URL input', async () => {\n    renderPanel();\n\n    const field = await screen.findByPlaceholderText(PLACEHOLDERS.sourceGithubRepoUrl);\n    expect(field).toBeInTheDocument();\n\n    fireEvent.change(field, { target: { value: 'http://github.com/i-vasilich-i' } });\n\n    const errorMessage = await screen.findByText(ERROR_MESSAGES.sourceGithubRepoUrl);\n    expect(errorMessage).toBeInTheDocument();\n    expect(errorMessage).toHaveTextContent(ERROR_MESSAGES.sourceGithubRepoUrl);\n  });\n\n  test('should not render error message on valid source GitHub repo URL input', async () => {\n    renderPanel();\n\n    const field = await screen.findByPlaceholderText(PLACEHOLDERS.sourceGithubRepoUrl);\n    expect(field).toBeInTheDocument();\n\n    fireEvent.change(field, { target: { value: 'https://github.com/rolling-scopes-school/task1' } });\n\n    await waitFor(() => {\n      const errorMessage = screen.queryByText(ERROR_MESSAGES.sourceGithubRepoUrl);\n      expect(errorMessage).not.toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Tasks/components/GitHubPanel/GitHubPanel.tsx",
    "content": "import { Form, Checkbox, Input } from 'antd';\nimport { LABELS, ERROR_MESSAGES, PLACEHOLDERS } from '@client/modules/Tasks/constants';\nimport { githubRepoUrl } from '@client/services/validators';\n\nexport function GitHubPanel() {\n  return (\n    <>\n      <Form.Item name=\"githubPrRequired\" valuePropName=\"checked\">\n        <Checkbox>Pull Request required</Checkbox>\n      </Form.Item>\n      <Form.Item\n        name=\"sourceGithubRepoUrl\"\n        label={LABELS.repoUrl}\n        rules={[{ pattern: githubRepoUrl, message: ERROR_MESSAGES.sourceGithubRepoUrl }]}\n      >\n        <Input placeholder={PLACEHOLDERS.sourceGithubRepoUrl} />\n      </Form.Item>\n      <Form.Item name=\"githubRepoName\" label={LABELS.expectedRepoName}>\n        <Input placeholder={PLACEHOLDERS.githubRepoName} />\n      </Form.Item>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Tasks/components/JsonAttributesPanel/JsonAttributesPanel.test.tsx",
    "content": "import { render, screen, fireEvent } from '@testing-library/react';\nimport { JsonAttributesPanel } from './JsonAttributesPanel';\nimport { Form } from 'antd';\nimport { ERROR_MESSAGES, PLACEHOLDERS } from '@client/modules/Tasks/constants';\n\nconst renderPanel = () => {\n  render(\n    <Form>\n      <JsonAttributesPanel />\n    </Form>,\n  );\n};\n\ndescribe('JSON Attributes', () => {\n  test('should render attributes textarea', async () => {\n    renderPanel();\n\n    const textarea = await screen.findByRole('textbox');\n    expect(textarea).toBeInTheDocument();\n    expect(textarea).toHaveProperty('placeholder', PLACEHOLDERS.jsonAttributes);\n  });\n\n  test('should render error message on invalid JSON input', async () => {\n    renderPanel();\n    const invalidJson = `{ name: 'Pit' }`;\n\n    const textarea = await screen.findByRole('textbox');\n    expect(textarea).toBeInTheDocument();\n\n    fireEvent.change(textarea, { target: { value: invalidJson } });\n\n    const errorMessage = await screen.findByText(ERROR_MESSAGES.invalidJson);\n    expect(errorMessage).toBeInTheDocument();\n    expect(errorMessage).toHaveTextContent(ERROR_MESSAGES.invalidJson);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Tasks/components/JsonAttributesPanel/JsonAttributesPanel.tsx",
    "content": "import { Form, Input } from 'antd';\nimport { ERROR_MESSAGES, PLACEHOLDERS } from '@client/modules/Tasks/constants';\n\nexport function JsonAttributesPanel() {\n  return (\n    <Form.Item\n      name=\"attributes\"\n      rules={[\n        {\n          validator: async (_, value: string) => (value ? JSON.parse(value) : Promise.resolve()),\n          message: ERROR_MESSAGES.invalidJson,\n        },\n      ]}\n    >\n      <Input.TextArea rows={6} placeholder={PLACEHOLDERS.jsonAttributes} />\n    </Form.Item>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Tasks/components/TaskModal/TaskModal.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { generateTasksData } from '@client/modules/Tasks/utils/test-utils';\nimport { FormValues } from '@client/modules/Tasks/types';\nimport {\n  ERROR_MESSAGES,\n  LABELS,\n  MODAL_TITLES,\n  PLACEHOLDERS,\n  TASK_SETTINGS_HEADERS,\n} from '@client/modules/Tasks/constants';\nimport { ModalProps, TaskModal } from './TaskModal';\n\nconst mockData = generateData();\n\ndescribe('TaskModal', () => {\n  test('should render modal with proper title', () => {\n    render(<TaskModal {...mockData} />);\n\n    const modal = screen.getByRole('dialog');\n    expect(modal).toBeInTheDocument();\n\n    const title = screen.getByText(MODAL_TITLES.edit);\n    expect(title).toBeInTheDocument();\n  });\n\n  test('should render labels', () => {\n    render(<TaskModal {...mockData} />);\n\n    const name = screen.getByLabelText(LABELS.name);\n    const taskType = screen.getByLabelText(LABELS.taskType);\n    const discipline = screen.getByLabelText(LABELS.discipline);\n    const tags = screen.getByLabelText(LABELS.tags);\n    const descriptionUrl = screen.getByLabelText(LABELS.descriptionUrl);\n    const summary = screen.getByLabelText(LABELS.summary);\n    const skills = screen.getByLabelText(LABELS.skills);\n\n    expect(name).toBeInTheDocument();\n    expect(taskType).toBeInTheDocument();\n    expect(discipline).toBeInTheDocument();\n    expect(tags).toBeInTheDocument();\n    expect(descriptionUrl).toBeInTheDocument();\n    expect(summary).toBeInTheDocument();\n    expect(skills).toBeInTheDocument();\n  });\n\n  test('should render \"Used in courses\" card', () => {\n    render(<TaskModal {...mockData} />);\n\n    const card = screen.getByText(LABELS.usedInCourses);\n    expect(card).toBeInTheDocument();\n  });\n\n  // Inputs\n  test('should render input placeholders', () => {\n    render(<TaskModal {...mockData} />);\n\n    const name = screen.getByPlaceholderText(PLACEHOLDERS.name);\n    const descriptionUrl = screen.getByPlaceholderText(PLACEHOLDERS.descriptionUrl);\n    const summary = screen.getByPlaceholderText(PLACEHOLDERS.summary);\n\n    expect(name).toBeInTheDocument();\n    expect(descriptionUrl).toBeInTheDocument();\n    expect(summary).toBeInTheDocument();\n  });\n\n  // Selects\n  test('should render select placeholders', () => {\n    render(<TaskModal {...generateData(true)} />);\n    const taskType = screen.getByText(PLACEHOLDERS.taskType);\n    const discipline = screen.getByText(PLACEHOLDERS.discipline);\n    const tags = screen.getByText(PLACEHOLDERS.tags);\n    const skills = screen.getByText(PLACEHOLDERS.skills);\n\n    expect(taskType).toBeInTheDocument();\n    expect(discipline).toBeInTheDocument();\n    expect(tags).toBeInTheDocument();\n    expect(skills).toBeInTheDocument();\n  });\n\n  describe('incorrect input handling', () => {\n    test('should render error message on invalid description URL input', async () => {\n      render(<TaskModal {...mockData} />);\n\n      const input = screen.getByPlaceholderText(PLACEHOLDERS.descriptionUrl);\n      expect(input).toBeInTheDocument();\n\n      const value = 'not url';\n\n      fireEvent.change(input, {\n        target: {\n          value,\n        },\n      });\n\n      expect(input).toHaveValue(value);\n\n      const errorMessage = await screen.findByText(ERROR_MESSAGES.validUrl);\n      expect(errorMessage).toBeInTheDocument();\n    });\n\n    test('should render error messages on required fields', async () => {\n      render(<TaskModal {...generateData(true)} />);\n\n      const save = screen.getByRole('button', { name: /save/i });\n      expect(save).toBeInTheDocument();\n\n      fireEvent.click(save);\n\n      const errors = await Promise.all([\n        screen.findByText(ERROR_MESSAGES.name),\n        screen.findByText(ERROR_MESSAGES.taskType),\n        screen.findByText(ERROR_MESSAGES.discipline),\n        screen.findByText(ERROR_MESSAGES.descriptionUrl),\n      ]);\n\n      expect(errors).toHaveLength(4);\n\n      errors.forEach(error => {\n        expect(error).toBeInTheDocument();\n      });\n    });\n  });\n\n  test('should render task setting panel headers', () => {\n    render(<TaskModal {...mockData} />);\n\n    const crossCheckCriteria = screen.getByText(TASK_SETTINGS_HEADERS.crossCheckCriteria);\n    const github = screen.getByText(TASK_SETTINGS_HEADERS.github);\n    const jsonAttributes = screen.getByText(TASK_SETTINGS_HEADERS.jsonAttributes);\n\n    expect(crossCheckCriteria).toBeInTheDocument();\n    expect(github).toBeInTheDocument();\n    expect(jsonAttributes).toBeInTheDocument();\n  });\n});\n\nfunction generateData(isEmpty = false): ModalProps {\n  const tasks = generateTasksData();\n  const formData: FormValues = {\n    ...tasks[0],\n    attributes: undefined,\n    discipline: tasks[0]?.discipline?.id,\n  };\n\n  if (isEmpty) {\n    formData.name = undefined;\n    formData.type = undefined;\n    formData.discipline = undefined;\n    formData.descriptionUrl = undefined;\n    formData.tags = undefined;\n    formData.skills = undefined;\n  }\n\n  return {\n    tasks,\n    dataCriteria: [],\n    formData,\n    modalLoading: false,\n    disciplines: [],\n    mode: isEmpty ? 'create' : 'edit',\n    setDataCriteria: vi.fn(),\n    handleModalSubmit: vi.fn(),\n    toggleModal: vi.fn(),\n  };\n}\n"
  },
  {
    "path": "client/src/modules/Tasks/components/TaskModal/TaskModal.tsx",
    "content": "import { Row, Col, Form, Input, Select, Card, Space, Tag, Empty, Typography } from 'antd';\nimport { Dispatch, SetStateAction, useMemo } from 'react';\nimport union from 'lodash/union';\nimport { TaskDto, CriteriaDto, DisciplineDto } from '@client/api';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { stringSorter } from '@client/shared/components/Table';\nimport { SKILLS } from '@client/data/skills';\nimport { TASK_TYPES } from '@client/data/taskTypes';\nimport { ModalFormMode } from '@client/hooks';\nimport { TaskSettings } from '@client/modules/Tasks/components';\nimport { ERROR_MESSAGES, LABELS, MODAL_TITLES, PLACEHOLDERS } from '@client/modules/Tasks/constants';\nimport { FormValues } from '@client/modules/Tasks/types';\nimport { urlPattern } from '@client/services/validators';\n\nconst { Text } = Typography;\nconst { TextArea } = Input;\n\nconst taskTypes = TASK_TYPES.sort(stringSorter('name')).map(({ id, name }) => ({ value: id, label: name }));\n\nexport type ModalProps = {\n  tasks: TaskDto[];\n  formData: FormValues | undefined;\n  dataCriteria: CriteriaDto[];\n  modalLoading: boolean;\n  disciplines: DisciplineDto[];\n  mode: ModalFormMode;\n  toggleModal: (data?: FormValues) => void;\n  setDataCriteria: Dispatch<SetStateAction<CriteriaDto[]>>;\n  handleModalSubmit: (values: FormValues) => Promise<void>;\n};\n\nexport function TaskModal({\n  tasks,\n  dataCriteria,\n  modalLoading,\n  disciplines,\n  mode,\n  formData,\n  toggleModal,\n  setDataCriteria,\n  handleModalSubmit,\n}: ModalProps) {\n  const [form] = Form.useForm<FormValues>();\n  const typeField = Form.useWatch('type', form);\n\n  const allTags = useMemo(() => union(...tasks.map(task => task.tags || [])), [tasks]);\n  const allSkills = useMemo(\n    () =>\n      union(\n        tasks\n          .flatMap(task => task.skills || [])\n          .concat(SKILLS)\n          .sort(),\n      ),\n    [tasks],\n  );\n\n  const handleTypeChange = () => {\n    // reset settings\n    form.setFieldsValue({\n      githubPrRequired: undefined,\n      githubRepoName: undefined,\n      sourceGithubRepoUrl: undefined,\n      attributes: undefined,\n    });\n    setDataCriteria([]);\n  };\n\n  return (\n    <ModalForm\n      data={formData ?? {}}\n      form={form}\n      title={MODAL_TITLES[mode]}\n      submit={handleModalSubmit}\n      cancel={() => {\n        toggleModal();\n        setDataCriteria([]);\n      }}\n      loading={modalLoading}\n    >\n      <Form.Item name=\"name\" label={LABELS.name} rules={[{ required: true, message: ERROR_MESSAGES.name }]}>\n        <Input placeholder={PLACEHOLDERS.name} />\n      </Form.Item>\n      <Form.Item\n        name=\"descriptionUrl\"\n        label={LABELS.descriptionUrl}\n        rules={[\n          {\n            required: true,\n            message: ERROR_MESSAGES.descriptionUrl,\n          },\n          {\n            message: ERROR_MESSAGES.validUrl,\n            pattern: urlPattern,\n          },\n        ]}\n      >\n        <Input placeholder={PLACEHOLDERS.descriptionUrl} />\n      </Form.Item>\n      <Row gutter={24}>\n        <Col span={12}>\n          <Form.Item name=\"type\" label={LABELS.taskType} rules={[{ required: true, message: ERROR_MESSAGES.taskType }]}>\n            <Select placeholder={PLACEHOLDERS.taskType} options={taskTypes} onChange={handleTypeChange} />\n          </Form.Item>\n        </Col>\n        <Col span={12}>\n          <Form.Item\n            name=\"discipline\"\n            label={LABELS.discipline}\n            rules={[{ required: true, message: ERROR_MESSAGES.discipline }]}\n          >\n            <Select\n              placeholder={PLACEHOLDERS.discipline}\n              options={disciplines.map(({ id, name }) => ({ value: id, label: name }))}\n            />\n          </Form.Item>\n        </Col>\n      </Row>\n      <Row gutter={24}>\n        <Col span={12}>\n          <Form.Item name=\"tags\" label={LABELS.tags}>\n            <Select\n              mode=\"tags\"\n              placeholder={PLACEHOLDERS.tags}\n              options={allTags.map(tag => ({ value: tag, label: tag }))}\n            />\n          </Form.Item>\n        </Col>\n        <Col span={12}>\n          <Form.Item name=\"skills\" label={LABELS.skills}>\n            <Select\n              mode=\"tags\"\n              placeholder={PLACEHOLDERS.skills}\n              options={allSkills.map(tag => ({ value: tag, label: tag }))}\n            />\n          </Form.Item>\n        </Col>\n      </Row>\n      <Form.Item name=\"description\" label={LABELS.summary}>\n        <TextArea placeholder={PLACEHOLDERS.summary} maxLength={100} showCount />\n      </Form.Item>\n      <Row gutter={24}>\n        <Col span={24}>\n          <Space direction=\"vertical\" size={8} style={{ width: '100%', marginBottom: 24 }}>\n            <Text>{LABELS.usedInCourses}</Text>\n            <Card bodyStyle={{ padding: 8 }}>\n              {formData?.courses?.length ? (\n                <Space size={[0, 8]} wrap>\n                  {formData.courses.map(({ name, isActive }) => (\n                    <Tag key={name} color={isActive ? 'blue' : ''}>\n                      {name}\n                    </Tag>\n                  ))}\n                </Space>\n              ) : (\n                <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} style={{ margin: 0 }} />\n              )}\n            </Card>\n          </Space>\n        </Col>\n      </Row>\n      <TaskSettings dataCriteria={dataCriteria} setDataCriteria={setDataCriteria} taskType={typeField} />\n    </ModalForm>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Tasks/components/TaskSettings/TaskSettings.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { CriteriaDto } from '@client/api';\nimport { TASK_SETTINGS_HEADERS } from '@client/modules/Tasks/constants';\nimport { TaskSettings } from './TaskSettings';\n\nconst renderTaskSettings = (dataCriteria: CriteriaDto[] = [], setDataCriteria = vi.fn()) => {\n  render(\n    <Form>\n      <TaskSettings dataCriteria={dataCriteria} setDataCriteria={setDataCriteria} taskType={undefined} />\n    </Form>,\n  );\n};\n\ndescribe('TaskSettings', () => {\n  test.each`\n    header\n    ${TASK_SETTINGS_HEADERS.crossCheckCriteria}\n    ${TASK_SETTINGS_HEADERS.github}\n    ${TASK_SETTINGS_HEADERS.jsonAttributes}\n  `('should render task setting panel $header', ({ header }) => {\n    renderTaskSettings();\n\n    const panel = screen.getByText(header);\n    expect(panel).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Tasks/components/TaskSettings/TaskSettings.tsx",
    "content": "import { Collapse, CollapseProps, Space, Typography } from 'antd';\nimport { CollapsibleType } from 'antd/es/collapse/CollapsePanel';\nimport { CriteriaDto, TaskDtoTypeEnum } from '@client/api';\n\nimport { CrossCheckTaskCriteriaPanel, GitHubPanel, JsonAttributesPanel } from '@client/modules/Tasks/components';\nimport { TASK_SETTINGS_HEADERS } from '@client/modules/Tasks/constants';\nimport { Settings, SettingsSet } from '@client/modules/Tasks/types';\nimport { Dispatch, SetStateAction, useEffect, useState } from 'react';\n\ntype Props = {\n  dataCriteria: CriteriaDto[];\n  setDataCriteria: Dispatch<SetStateAction<CriteriaDto[]>>;\n  taskType: TaskDtoTypeEnum | undefined;\n};\n\nconst { Text } = Typography;\n\nexport function TaskSettings({ dataCriteria, taskType, setDataCriteria }: Props) {\n  const [activeKey, setActiveKey] = useState<CollapseProps['activeKey']>([]);\n  const { json, github, crossCheck } = getSettings(taskType);\n\n  const collapseItems = [\n    {\n      label: TASK_SETTINGS_HEADERS.crossCheckCriteria,\n      children: <CrossCheckTaskCriteriaPanel dataCriteria={dataCriteria} setDataCriteria={setDataCriteria} />,\n      forceRender: true,\n      collapsible: isCollapsible(crossCheck),\n    },\n    {\n      label: TASK_SETTINGS_HEADERS.github,\n      children: <GitHubPanel />,\n      forceRender: true,\n      collapsible: isCollapsible(github),\n    },\n    {\n      label: TASK_SETTINGS_HEADERS.jsonAttributes,\n      children: <JsonAttributesPanel />,\n      forceRender: true,\n      collapsible: isCollapsible(json),\n    },\n  ];\n\n  const handleChange: CollapseProps['onChange'] = key => setActiveKey(key);\n\n  // collapse opened panel(s)\n  useEffect(() => {\n    setActiveKey([]);\n  }, [taskType]);\n\n  return (\n    <Space direction=\"vertical\" size={16} style={{ width: '100%' }}>\n      <Text strong>Settings for some types of tasks</Text>\n      <Collapse items={collapseItems} defaultActiveKey={[]} activeKey={activeKey} onChange={handleChange} accordion />\n    </Space>\n  );\n}\n\nconst taskSettingsSet: SettingsSet = {\n  json: [\n    TaskDtoTypeEnum.Interview,\n    TaskDtoTypeEnum.StageInterview,\n    TaskDtoTypeEnum.Selfeducation,\n    TaskDtoTypeEnum.Codewars,\n    TaskDtoTypeEnum.Ipynb,\n    TaskDtoTypeEnum.Kotlintask,\n    TaskDtoTypeEnum.Objctask,\n  ],\n  github: [\n    TaskDtoTypeEnum.Codejam,\n    TaskDtoTypeEnum.Jstask,\n    TaskDtoTypeEnum.Ipynb,\n    TaskDtoTypeEnum.Kotlintask,\n    TaskDtoTypeEnum.Objctask,\n    TaskDtoTypeEnum.Cvhtml,\n    TaskDtoTypeEnum.Cvmarkdown,\n  ],\n  crossCheck: [TaskDtoTypeEnum.Htmltask, TaskDtoTypeEnum.Jstask],\n};\n\nconst defaultSettings: Settings = { json: false, github: false, crossCheck: false };\n\nfunction getSettings(taskType?: TaskDtoTypeEnum): Settings {\n  if (!taskType) {\n    return defaultSettings;\n  }\n\n  const json = taskSettingsSet.json.includes(taskType);\n  const github = taskSettingsSet.github.includes(taskType);\n  const crossCheck = taskSettingsSet.crossCheck.includes(taskType);\n\n  return { json, github, crossCheck };\n}\n\nfunction isCollapsible(isPanelEnabled: boolean) {\n  return (!isPanelEnabled ? 'disabled' : undefined) as CollapsibleType;\n}\n"
  },
  {
    "path": "client/src/modules/Tasks/components/TasksTable/TasksTable.test.tsx",
    "content": "import assert from 'node:assert';\nimport { fireEvent, render, screen, within } from '@testing-library/react';\nimport { TaskDto } from '@client/api';\nimport { TasksTable } from './TasksTable';\nimport { ColumnName } from '@client/modules/Tasks/types';\nimport { TASK_TYPES } from '@client/data/taskTypes';\nimport { COURSE_NAME_MOCK, generateTasksData } from '@client/modules/Tasks/utils/test-utils';\n\nconst renderTasksTable = (data: TaskDto[] = generateTasksData(1), handleEditItem = vi.fn()) => {\n  render(<TasksTable data={data} handleEditItem={handleEditItem} />);\n};\n\ndescribe('TasksTable', () => {\n  const [mockData] = generateTasksData(1);\n\n  test.each`\n    label\n    ${ColumnName.Id}\n    ${ColumnName.Name}\n    ${ColumnName.Discipline}\n    ${ColumnName.Tags}\n    ${ColumnName.Skills}\n    ${ColumnName.Type}\n    ${ColumnName.UsedInCourses}\n    ${ColumnName.DescriptionURL}\n    ${ColumnName.PRRequired}\n    ${ColumnName.RepoName}\n    ${ColumnName.Actions}\n  `('should render column \"$label\"', ({ label }: { label: ColumnName }) => {\n    renderTasksTable();\n\n    expect(screen.getByText(label)).toBeInTheDocument();\n  });\n\n  test.each`\n    value\n    ${mockData?.id}\n    ${mockData?.name}\n    ${mockData?.discipline.name}\n    ${mockData?.tags[0]}\n    ${mockData?.tags[1]}\n    ${mockData?.skills[0]}\n    ${mockData?.skills[1]}\n    ${mockData?.type}\n    ${mockData?.githubRepoName}\n    ${mockData?.courses[0]?.name}\n  `('should render data field \"$value\"', ({ value }) => {\n    renderTasksTable();\n\n    const [dataField] = screen.getAllByText(value ?? '');\n    expect(dataField).toBeInTheDocument();\n  });\n\n  test('should render description link fields', () => {\n    const data = generateTasksData();\n\n    renderTasksTable(data);\n\n    const links = screen.getAllByRole('link', { name: /link/i });\n    expect(links).toHaveLength(data.length);\n\n    data.forEach((task, i) => {\n      expect(links[i]).toHaveAttribute('href', task.descriptionUrl);\n    });\n  });\n\n  test('should render \"Edit\" link fields', () => {\n    const data = generateTasksData();\n\n    renderTasksTable(data);\n\n    const links = screen.getAllByText(/edit/i);\n    expect(links).toHaveLength(data.length);\n  });\n\n  test('should call handleEditItem on \"Edit\" click with proper record', () => {\n    const handleEditItem = vi.fn();\n    const data = generateTasksData();\n\n    renderTasksTable(data, handleEditItem);\n\n    const links = screen.getAllByText('Edit');\n\n    data.forEach((task, i) => {\n      const link = links[i];\n      assert(link);\n      fireEvent.click(link);\n      expect(handleEditItem).toHaveBeenCalledWith(task);\n    });\n  });\n\n  test('should render PR Required fields with proper mark', () => {\n    const data = generateTasksData();\n    const notRequiredPRs = data.filter(elem => !elem.githubPrRequired);\n    const requiredPRs = data.filter(elem => elem.githubPrRequired);\n\n    renderTasksTable(data);\n\n    const minusMarks = screen.getAllByLabelText('minus-circle');\n    expect(minusMarks).toHaveLength(notRequiredPRs.length);\n\n    const checkMarks = screen.getAllByLabelText('check-circle');\n    expect(checkMarks).toHaveLength(requiredPRs.length);\n  });\n\n  describe('filter & search data', () => {\n    test('should check filter in dropdown when tag is selected', async () => {\n      const tag = TASK_TYPES[0]?.name ?? '';\n      const data = generateTasksData();\n      renderTasksTable(data);\n\n      const columnHeader = screen.getByLabelText(/type/i);\n\n      const tagFilterBtn = within(columnHeader).getByRole('button', { name: /filter/i });\n      fireEvent.click(tagFilterBtn);\n\n      const filtersDropdown = await screen.findByRole('menu');\n      const menuItem = within(filtersDropdown).getByRole('menuitem', { name: new RegExp(tag, 'i') });\n      fireEvent.click(menuItem);\n\n      const checkbox = within(menuItem).getByRole('checkbox');\n      expect(checkbox).toBeChecked();\n    });\n\n    test('should reset filter on Reset click', async () => {\n      const tag = TASK_TYPES[0]?.name ?? '';\n      const data = generateTasksData();\n      renderTasksTable(data);\n\n      const columnHeader = screen.getByLabelText(/type/i);\n\n      const tagFilterBtn = within(columnHeader).getByRole('button', { name: /filter/i });\n      fireEvent.click(tagFilterBtn);\n\n      const filtersDropdown = await screen.findByRole('menu');\n      const menuItem = within(filtersDropdown).getByRole('menuitem', { name: new RegExp(tag, 'i') });\n      fireEvent.click(menuItem);\n\n      const checkbox = within(menuItem).getByRole('checkbox');\n      expect(checkbox).toBeChecked();\n\n      const resetBtn = screen.getByRole('button', { name: /reset/i });\n      fireEvent.click(resetBtn);\n\n      expect(checkbox).not.toBeChecked();\n    });\n\n    test('should render only filtered by Type data', async () => {\n      const data = generateTasksData();\n      const selectedTag = TASK_TYPES[0];\n      const notSelectedTags = data.filter(elem => elem.type !== selectedTag?.id).map(task => task.type);\n      renderTasksTable(data);\n\n      // find and click filter button for Type column\n      const columnHeader = screen.getByLabelText(/type/i);\n      const tagFilterBtn = within(columnHeader).getByRole('button', { name: /filter/i });\n      fireEvent.click(tagFilterBtn);\n\n      // find and click selected Type tag\n      const filtersDropdown = await screen.findByRole('menu');\n      const menuItem = within(filtersDropdown).getByRole('menuitem', {\n        name: new RegExp(selectedTag?.name ?? '', 'i'),\n      });\n      fireEvent.click(menuItem);\n\n      // submit filter\n      const okBtn = screen.getByRole('button', { name: /ok/i });\n      fireEvent.click(okBtn);\n\n      // find filtered in rows\n      const filteredInRow = screen.getByText(selectedTag?.id ?? '');\n      expect(filteredInRow).toBeInTheDocument();\n\n      // check that other data rows aren't rendered\n      notSelectedTags.forEach(notSelectedTag => {\n        const filteredOutRow = screen.queryByText(notSelectedTag);\n        expect(filteredOutRow).not.toBeInTheDocument();\n      });\n    });\n\n    test('should render only filtered by Course data', async () => {\n      const data = generateTasksData();\n      renderTasksTable(data);\n\n      // find and click filter button for Used in Courses column\n      const [, tagFilterBtn] = screen.getAllByRole('button', { name: /filter/i });\n      expect(tagFilterBtn).toBeInTheDocument();\n      if (tagFilterBtn) {\n        fireEvent.click(tagFilterBtn);\n      }\n\n      // find and click selected course tag\n      const filtersDropdown = await screen.findByRole('menu');\n      const menuItem = within(filtersDropdown).getByRole('menuitem', { name: COURSE_NAME_MOCK });\n      fireEvent.click(menuItem);\n\n      // submit filter\n      const okBtn = screen.getByRole('button', { name: /ok/i });\n      fireEvent.click(okBtn);\n\n      // find filtered in rows\n      const table = screen.getByRole('table');\n      const filteredInRow = within(table).getByText(COURSE_NAME_MOCK);\n      expect(filteredInRow).toBeInTheDocument();\n\n      // count all rows by Action column(\"Edit\"), since every row has it\n      const rows = within(table).getAllByText(/edit/i);\n      expect(rows.length).not.toBe(data.length);\n    });\n\n    test('should render only data with \"Not assigned\" course', async () => {\n      const data = generateTasksData();\n      renderTasksTable(data);\n\n      // find and click filter button for Used in Courses column\n      const [, tagFilterBtn] = screen.getAllByRole('button', { name: /filter/i });\n      expect(tagFilterBtn).toBeInTheDocument();\n      if (tagFilterBtn) {\n        fireEvent.click(tagFilterBtn);\n      }\n\n      // find and click selected tag\n      const filtersDropdown = await screen.findByRole('menu');\n      const menuItem = within(filtersDropdown).getByRole('menuitem', { name: /not assigned/i });\n      fireEvent.click(menuItem);\n\n      // submit filter\n      const okBtn = screen.getByRole('button', { name: /ok/i });\n      fireEvent.click(okBtn);\n\n      // find filtered in rows\n      const table = screen.getByRole('table');\n      const filteredInRow = within(table).queryByText(COURSE_NAME_MOCK);\n      expect(filteredInRow).not.toBeInTheDocument();\n\n      // count all rows by Action column(\"Edit\"), since every row has it\n      const rows = within(table).getAllByText(/edit/i);\n      const notAssignedCount = data.filter(task => !task.courses.length).length;\n      expect(rows).toHaveLength(notAssignedCount);\n    });\n\n    test('should render only data filtered by Name column search', async () => {\n      const data = generateTasksData();\n      const searchQuery = TASK_TYPES[0]?.id ?? '';\n      renderTasksTable(data);\n\n      // Check that all items rendered\n      const table = screen.getByRole('table');\n      const rows = within(table).getAllByText(/edit/i);\n      expect(rows).toHaveLength(data.length);\n\n      // Find and click search button for column\n      const searchButton = screen.getByRole('button', { name: /search/i });\n      fireEvent.click(searchButton);\n\n      // Type search query inside search input\n      const searchInput = await screen.findByRole('textbox');\n      fireEvent.change(searchInput, { target: { value: searchQuery } });\n\n      // Apply search\n      const inputSearchBtn = screen.getByRole('button', { name: /search search/i });\n      fireEvent.click(inputSearchBtn);\n\n      // Find the line with search query and no others\n      const item = await screen.findByText(searchQuery);\n      expect(item).toBeInTheDocument();\n      const secondTaskName = data[1]?.name ?? 'non-existent';\n      expect(screen.queryByText(secondTaskName)).not.toBeInTheDocument();\n    });\n\n    test('should render all data when search query is cleared', async () => {\n      const data = generateTasksData();\n      const searchQuery = TASK_TYPES[0]?.id ?? '';\n      renderTasksTable(data);\n\n      // Find and click search button for column\n      const searchButton = screen.getByRole('button', { name: /search/i });\n      fireEvent.click(searchButton);\n\n      // Type search query inside search input\n      const searchInput = await screen.findByRole('textbox');\n      fireEvent.change(searchInput, { target: { value: searchQuery } });\n\n      // Apply search\n      const inputSearchBtn = screen.getByRole('button', { name: /search search/i });\n      fireEvent.click(inputSearchBtn);\n\n      // Find the line with search query and no others\n      const item = await screen.findByText(searchQuery);\n      expect(item).toBeInTheDocument();\n      const secondTaskName = data[1]?.name ?? 'non-existent';\n      expect(screen.queryByText(secondTaskName)).not.toBeInTheDocument();\n\n      // Reset search\n      const inputResetBtn = screen.getByRole('button', { name: /reset/i });\n      fireEvent.click(inputResetBtn);\n\n      // Check that all items rendered\n      const table = screen.getByRole('table');\n      const rows = within(table).getAllByText(/edit/i);\n      expect(rows).toHaveLength(data.length);\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Tasks/components/TasksTable/TasksTable.tsx",
    "content": "import { Table } from 'antd';\nimport { ColumnsType } from 'antd/es/table';\nimport { TaskDto } from '@client/api';\nimport {\n  stringSorter,\n  getColumnSearchProps,\n  tagsRenderer,\n  tagsCoursesRendererWithRemainingNumber,\n  boolIconRenderer,\n} from '@client/shared/components/Table';\nimport { TASK_TYPES } from '@client/data/taskTypes';\nimport { uniqBy } from 'lodash';\nimport { ColumnName } from '@client/modules/Tasks/types';\nimport { useMemo } from 'react';\n\nfunction getColumns(\n  handleEditItem: (record: TaskDto) => Promise<void>,\n  allUsedCourses: string[],\n): ColumnsType<TaskDto> {\n  return [\n    {\n      title: ColumnName.Id,\n      dataIndex: 'id',\n      fixed: true,\n    },\n    {\n      title: ColumnName.Name,\n      dataIndex: 'name',\n      sorter: stringSorter<TaskDto>('name'),\n      ...getColumnSearchProps('name'),\n    },\n    {\n      title: ColumnName.Discipline,\n      dataIndex: ['discipline', 'name'],\n      sorter: stringSorter<TaskDto>('discipline'),\n    },\n    {\n      title: ColumnName.Tags,\n      dataIndex: 'tags',\n      render: tagsRenderer,\n    },\n    {\n      title: ColumnName.Skills,\n      dataIndex: 'skills',\n      render: tagsRenderer,\n    },\n    {\n      title: ColumnName.Type,\n      dataIndex: 'type',\n      sorter: stringSorter<TaskDto>('type'),\n      filters: TASK_TYPES.map(type => ({ text: type.name, value: type.id })),\n      onFilter: (value, record) => record.type === value,\n    },\n    {\n      title: ColumnName.UsedInCourses,\n      dataIndex: ['courses', 'name'],\n      render: tagsCoursesRendererWithRemainingNumber,\n      filters: [\n        { text: 'Not assigned', value: '' },\n        ...allUsedCourses.map(course => ({ text: course, value: course })),\n      ],\n      onFilter: (value, record) =>\n        value ? record.courses.some(({ name }) => name === `${value}`) : record.courses.length === 0,\n      filterSearch: true,\n    },\n    {\n      title: ColumnName.DescriptionURL,\n      dataIndex: 'descriptionUrl',\n      render: (value: string) =>\n        value ? (\n          <a title={value} href={value} target=\"_blank\">\n            Link\n          </a>\n        ) : null,\n      width: 80,\n    },\n    {\n      title: ColumnName.PRRequired,\n      dataIndex: 'githubPrRequired',\n      render: boolIconRenderer,\n      width: 80,\n    },\n    {\n      title: ColumnName.RepoName,\n      dataIndex: 'githubRepoName',\n    },\n    {\n      title: ColumnName.Actions,\n      dataIndex: 'actions',\n      render: (_, record) => <a onClick={() => handleEditItem(record)}>Edit</a>,\n    },\n  ];\n}\n\ntype Props = {\n  data: TaskDto[];\n  handleEditItem: (record: TaskDto) => Promise<void>;\n};\n\nexport const TasksTable = ({ data, handleEditItem }: Props) => {\n  const allUsedCourses = useMemo(\n    () =>\n      uniqBy(\n        data.flatMap(({ courses }) => courses),\n        course => course.name,\n      )\n        .map(({ name }) => name)\n        .sort(),\n    [data],\n  );\n\n  return (\n    <Table\n      size=\"small\"\n      style={{ marginTop: 8 }}\n      dataSource={data}\n      pagination={{ pageSize: 100, showSizeChanger: false }}\n      rowKey=\"id\"\n      columns={getColumns(handleEditItem, allUsedCourses)}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Tasks/components/index.ts",
    "content": "export * from './TasksTable/TasksTable';\nexport * from './TaskModal/TaskModal';\nexport * from './TaskSettings/TaskSettings';\nexport * from './CrossCheckTaskCriteriaPanel/CrossCheckTaskCriteriaPanel';\nexport * from './GitHubPanel/GitHubPanel';\nexport * from './JsonAttributesPanel/JsonAttributesPanel';\n"
  },
  {
    "path": "client/src/modules/Tasks/constants.ts",
    "content": "import { ModalFormMode } from '@client/hooks';\n\nconst LABELS = {\n  name: 'Task name',\n  taskType: 'Task type',\n  discipline: 'Discipline',\n  tags: 'Tags',\n  descriptionUrl: 'Description URL',\n  summary: 'Summary',\n  skills: 'Skills',\n  usedInCourses: 'Used in Courses',\n  crossCheckCriteria: 'Criteria For Cross-Check',\n  repoUrl: 'Source Repo Url',\n  expectedRepoName: 'Expected Repo Name',\n};\n\nconst TASK_SETTINGS_HEADERS = {\n  crossCheckCriteria: 'Criteria For Cross-Check Task',\n  github: 'GitHub',\n  jsonAttributes: 'JSON Attributes',\n};\n\nconst PLACEHOLDERS = {\n  name: 'Short descriptive title',\n  taskType: 'Choose task type',\n  discipline: 'Choose the right one',\n  tags: 'Choose tags to define task options',\n  descriptionUrl: 'Input the task link',\n  summary: 'Purpose and objectives, student value, etc.',\n  skills: 'Choose skills to define task options',\n  jsonAttributes: 'Input JSON attributes',\n  sourceGithubRepoUrl: 'https://github.com/rolling-scopes-school/task1',\n  githubRepoName: 'task1',\n};\n\nconst ERROR_MESSAGES = {\n  name: 'Please enter task name',\n  taskType: 'Please select a type',\n  discipline: 'Please select a discipline',\n  descriptionUrl: 'Please enter description URL',\n  validUrl: 'Please enter valid URL',\n  sourceGithubRepoUrl: 'Please enter valid source github repo URL',\n  invalidJson: 'Invalid JSON',\n};\n\nconst MODAL_TITLES: Record<ModalFormMode, string> = {\n  create: 'Add Task',\n  edit: 'Edit Task',\n};\n\nexport { LABELS, TASK_SETTINGS_HEADERS, PLACEHOLDERS, ERROR_MESSAGES, MODAL_TITLES };\n"
  },
  {
    "path": "client/src/modules/Tasks/pages/TasksPage/TasksPage.tsx",
    "content": "import { Button, Layout, message } from 'antd';\nimport { useCallback, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport {\n  CreateTaskDto,\n  CriteriaDto,\n  DisciplineDto,\n  DisciplinesApi,\n  TaskDto,\n  TasksApi,\n  TasksCriteriaApi,\n} from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { useModalForm } from '@client/hooks';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { TasksTable, TaskModal } from '@client/modules/Tasks/components';\nimport { FormValues } from '@client/modules/Tasks/types';\nimport { TaskType } from '@client/modules/CrossCheck/constants';\n\nconst { Content } = Layout;\n\nconst tasksApi = new TasksApi();\nconst criteriaApi = new TasksCriteriaApi();\nconst disciplinesApi = new DisciplinesApi();\n\nexport function TasksPage() {\n  const { courses } = useActiveCourseContext();\n  const [tasks, setTasks] = useState<TaskDto[]>([]);\n  const [disciplines, setDisciplines] = useState<DisciplineDto[]>([]);\n  const [modalLoading, setModalLoading] = useState(false);\n  const [dataCriteria, setDataCriteria] = useState<CriteriaDto[]>([]);\n  const { open, mode, formData, toggle: toggleModal } = useModalForm<FormValues>();\n\n  const { loading } = useAsync(async () => {\n    if (formData) return;\n\n    const [{ data: tasksData }, { data: disciplines }] = await Promise.all([\n      tasksApi.getTasks(),\n      disciplinesApi.getDisciplines(),\n    ]);\n    setTasks(tasksData);\n    setDisciplines(disciplines);\n  }, [open, formData]);\n\n  const handleAddItem = () => {\n    setDataCriteria([]);\n    toggleModal();\n  };\n\n  const handleEditItem = async (record: TaskDto) => {\n    const { data } = await criteriaApi.getTaskCriteria(record.id);\n    setDataCriteria(data.criteria ?? []);\n    toggleModal(prepareValues(record));\n  };\n\n  const handleModalSubmit = useCallback(\n    async (values: FormValues) => {\n      const checkCriteria = () => {\n        return dataCriteria.every(item => {\n          if (item.type !== TaskType.Title) {\n            return item.max !== 0;\n          }\n          return true;\n        });\n      };\n\n      const isVerified = checkCriteria();\n      if (!isVerified) {\n        message.error('Please, check criteria! It has subtask with no score.');\n        return;\n      }\n\n      try {\n        if (modalLoading) {\n          return;\n        }\n\n        setModalLoading(true);\n        const record = createRecord(values);\n\n        if (!record) {\n          return;\n        }\n\n        if (mode === 'edit') {\n          if (!formData?.id) {\n            return;\n          }\n\n          await tasksApi.updateTask(formData.id, record);\n          const { data } = await criteriaApi.getTaskCriteria(formData.id);\n\n          if (data.criteria) {\n            await criteriaApi.updateTaskCriteria(formData.id, { criteria: dataCriteria });\n          } else {\n            await criteriaApi.createTaskCriteria(formData.id, { criteria: dataCriteria });\n          }\n        } else {\n          const { data: task } = await tasksApi.createTask(record);\n          await criteriaApi.createTaskCriteria(task.id, { criteria: dataCriteria });\n        }\n\n        toggleModal();\n      } catch {\n        message.error('An error occurred. Please try again later.');\n      } finally {\n        setModalLoading(false);\n      }\n    },\n    [formData, modalLoading, dataCriteria, mode],\n  );\n\n  return (\n    <AdminPageLayout title=\"Manage Tasks\" loading={loading} courses={courses}>\n      <Content style={{ margin: 8 }}>\n        <Button type=\"primary\" onClick={handleAddItem}>\n          Add Task\n        </Button>\n        <TasksTable data={tasks} handleEditItem={handleEditItem} />\n      </Content>\n      {open && (\n        <TaskModal\n          tasks={tasks}\n          disciplines={disciplines}\n          dataCriteria={dataCriteria}\n          modalLoading={modalLoading}\n          mode={mode}\n          formData={formData}\n          setDataCriteria={setDataCriteria}\n          toggleModal={toggleModal}\n          handleModalSubmit={handleModalSubmit}\n        />\n      )}\n    </AdminPageLayout>\n  );\n}\n\nfunction createRecord({\n  type,\n  name,\n  descriptionUrl,\n  discipline,\n  githubPrRequired,\n  githubRepoName,\n  sourceGithubRepoUrl,\n  description,\n  tags,\n  skills,\n  attributes,\n}: FormValues) {\n  if (!type || !name || !descriptionUrl || !discipline) {\n    return null;\n  }\n\n  const data: CreateTaskDto = {\n    // required form fields\n    type,\n    name,\n    disciplineId: discipline,\n    descriptionUrl,\n\n    // not required form fields\n    githubPrRequired: !!githubPrRequired,\n    githubRepoName: githubRepoName ?? '',\n    sourceGithubRepoUrl: sourceGithubRepoUrl ?? '',\n    description: description ?? '',\n    tags: tags ?? [],\n    skills: skills?.map((skill: string) => skill.toLowerCase()) ?? [],\n    attributes: JSON.parse(attributes ?? '{}') as Record<string, unknown>,\n  };\n\n  return data;\n}\n\nfunction prepareValues(task: TaskDto) {\n  return {\n    ...task,\n    attributes: JSON.stringify(task.attributes, null, 2),\n    discipline: task.discipline?.id,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/Tasks/pages/index.ts",
    "content": "export * from './TasksPage/TasksPage';\n"
  },
  {
    "path": "client/src/modules/Tasks/types.ts",
    "content": "import { TaskDto, TaskDtoTypeEnum } from '@client/api';\n\n// discipline on form is the discipline id(number), on TaskDto it's an object(id, name);\ntype FormValues = Partial<Omit<TaskDto, 'discipline' | 'attributes'> & { discipline: number; attributes: string }>;\n\nconst enum ColumnName {\n  Id = 'Id',\n  Name = 'Name',\n  Discipline = 'Discipline',\n  Tags = 'Tags',\n  Skills = 'Skills',\n  Type = 'Type',\n  UsedInCourses = 'Used in Courses',\n  DescriptionURL = 'Description URL',\n  PRRequired = 'PR Required',\n  RepoName = 'Repo Name',\n  Actions = 'Actions',\n}\n\nconst enum Criteria {\n  json = 'json',\n  github = 'github',\n  crossCheck = 'crossCheck',\n}\n\ntype Settings = Record<Criteria, boolean>;\n\ntype SettingsSet = Record<Criteria, TaskDtoTypeEnum[]>;\n\nexport type { FormValues, Settings, SettingsSet };\n\nexport { ColumnName };\n"
  },
  {
    "path": "client/src/modules/Tasks/utils/test-utils.ts",
    "content": "import { TaskDto, TaskDtoTypeEnum } from '@client/api';\nimport { TASK_TYPES } from '@client/data/taskTypes';\n\nexport const COURSE_NAME_MOCK = 'RS2023';\n\nexport function generateTasksData(count = 3): TaskDto[] {\n  const data = new Array(count).fill({}).map((_, i) => {\n    const taskType = TASK_TYPES[i]?.id ?? TaskDtoTypeEnum.Jstask;\n    const task: TaskDto = {\n      discipline: { name: 'JS', id: 0 },\n      id: i,\n      name: `${taskType}-${i}`,\n      description: `Something_${i}`,\n      descriptionUrl: `http://exemple.com/link-${i}`,\n      githubPrRequired: !!i,\n      type: taskType,\n      githubRepoName: 'Repo',\n      sourceGithubRepoUrl: 'http://exemple2.com',\n      createdDate: new Date().toISOString(),\n      updatedDate: new Date().toISOString(),\n      tags: ['tag_1', 'tag_2'],\n      skills: ['skill_1', 'skill_2'],\n      attributes: {},\n      courses: !i ? [{ name: COURSE_NAME_MOCK, isActive: true }] : [],\n    };\n\n    return task;\n  });\n\n  return data;\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/SubmitScoreModal/SubmitScoreModal.tsx",
    "content": "import { useAsync } from 'react-use';\nimport { Card, Form, Modal, Select, Space, Spin, Typography, message } from 'antd';\n\nimport { CoursesTasksApi, TeamDistributionDto } from '@client/api';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useSubmitTeamScore } from '@client/modules/TeamDistribution/hooks/useSubmitTeamScore';\n\nconst { Text } = Typography;\nconst { Option } = Select;\n\ntype Props = {\n  distribution: TeamDistributionDto | null;\n  onClose: () => void;\n};\n\nconst courseTasksApi = new CoursesTasksApi();\n\nexport default function SubmitScoreModal({ distribution, onClose }: Props) {\n  const { course } = useActiveCourseContext();\n  const { loading, handleSubmit, setTaskId, taskId } = useSubmitTeamScore(course.id, distribution?.id ?? 0);\n\n  const fetchCourseTasks = async () => {\n    try {\n      const { data } = await courseTasksApi.getCourseTasks(course.id);\n      return data;\n    } catch {\n      message.error(`Failed to load tasks for course: ${course.name}`);\n    }\n  };\n\n  const { value: courseTasks } = useAsync(fetchCourseTasks, []);\n\n  const handleOnCancel = () => {\n    setTaskId(null);\n    onClose();\n  };\n\n  return (\n    <Modal\n      open={Boolean(distribution)}\n      title=\"Submit Score\"\n      onOk={handleSubmit}\n      onCancel={handleOnCancel}\n      okText=\"Submit score\"\n      okType=\"danger\"\n    >\n      <Spin spinning={loading}>\n        <Space direction=\"vertical\">\n          <Card bordered>\n            <Text type=\"warning\" strong>\n              After submission, reverting changes will be impossible. Please be careful when selecting the task. The\n              same score will be given to all team members.\n            </Text>\n          </Card>\n          <Text strong>Select a task to submit score for all team members:</Text>\n          <Form.Item>\n            {courseTasks?.length ? (\n              <Select\n                placeholder=\"Select task\"\n                onChange={(value: number) => setTaskId(value)}\n                showSearch\n                value={taskId}\n                optionFilterProp=\"label\"\n              >\n                {courseTasks.map(task => (\n                  <Option key={task.id} value={task.id} label={task.name}>\n                    {task.name}\n                  </Option>\n                ))}\n              </Select>\n            ) : (\n              <Text type=\"secondary\">No tasks found for this course</Text>\n            )}\n          </Form.Item>\n        </Space>\n      </Spin>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/SubmitScoreModal/index.tsx",
    "content": "export { default as SubmitScoreModal } from './SubmitScoreModal';\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/Actions.test.tsx",
    "content": "import { screen, render, fireEvent } from '@testing-library/react';\nimport { TeamDistributionDto, TeamDistributionDtoRegistrationStatusEnum } from '@client/api';\nimport { Actions } from './Actions';\n\nconst mockOnRegister = vi.fn();\nconst mockOnDeleteRegister = vi.fn();\n\nconst distribution = {\n  id: 1,\n  startDate: '2021-12-31T00:00:00Z',\n  endDate: '2022-01-03T00:00:00Z',\n  registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Available,\n} as TeamDistributionDto;\n\nfunction renderActions(distribution: TeamDistributionDto, isManager = false) {\n  return render(\n    <Actions\n      distribution={distribution}\n      register={mockOnRegister}\n      deleteRegister={mockOnDeleteRegister}\n      isManager={isManager}\n      courseAlias=\"test\"\n      isCourseDementor={false}\n      onOpenSubmitScoreModal={vi.fn()}\n    />,\n  );\n}\n\ndescribe('Actions', () => {\n  beforeAll(() => vi.useFakeTimers().setSystemTime(new Date('2022-01-02')));\n\n  afterAll(() => vi.useRealTimers());\n\n  afterEach(() => {\n    mockOnRegister.mockClear();\n    mockOnDeleteRegister.mockClear();\n  });\n\n  it('should render a register button when the distribution is available', () => {\n    renderActions(distribution);\n\n    const registerButton = screen.getByRole('button', {\n      name: /register/i,\n    });\n    expect(registerButton).toBeInTheDocument();\n  });\n\n  it('should call register when the register button is clicked', () => {\n    renderActions(distribution);\n\n    const registerButton = screen.getByRole('button', {\n      name: /register/i,\n    });\n    fireEvent.click(registerButton);\n    expect(mockOnRegister).toHaveBeenCalledWith(1);\n  });\n\n  it('should render a disabled download button when the distribution is completed', () => {\n    const completedDistribution = {\n      ...distribution,\n      registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Completed,\n    };\n    renderActions(completedDistribution);\n\n    const registeredButton = screen.getByRole('button', {\n      name: /registered/i,\n    });\n    expect(registeredButton).toBeInTheDocument();\n    expect(registeredButton).toBeDisabled();\n  });\n\n  it('should render a cancel registration link when the distribution is completed and end date has not passed', () => {\n    const completedDistribution = {\n      ...distribution,\n      registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Completed,\n    };\n    renderActions(completedDistribution);\n\n    const cancel = screen.getByText(/cancel/i);\n    expect(cancel).toBeInTheDocument();\n  });\n\n  it('should render the \"Registration is closed\" text when the distribution is completed and end date has passed', () => {\n    const completedDistribution = {\n      ...distribution,\n      endDate: '2022-01-01T00:00:00Z',\n      registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Completed,\n    };\n    renderActions(completedDistribution);\n\n    const text = screen.getByText(/registration is closed/i);\n    expect(text).toBeInTheDocument();\n  });\n\n  it('should render a disabled register button when the distribution is in the future', () => {\n    const futureDistribution = {\n      ...distribution,\n      registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Future,\n    };\n    renderActions(futureDistribution);\n\n    const registerButton = screen.getByRole('button', {\n      name: /register/i,\n    });\n    expect(registerButton).toBeInTheDocument();\n    expect(registerButton).toBeDisabled();\n  });\n\n  it('should render a disabled register button and displays \"Registration is closed\" text when the distribution is closed', () => {\n    const closedDistribution = {\n      ...distribution,\n      registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Closed,\n    };\n    renderActions(closedDistribution);\n\n    const registerButton = screen.getByRole('button', {\n      name: /register/i,\n    });\n    expect(registerButton).toBeInTheDocument();\n    expect(registerButton).toBeDisabled();\n    expect(screen.getByText('Registration is closed')).toBeInTheDocument();\n  });\n\n  it('should render a warning text when the end date is within 48 hours of the current time', () => {\n    renderActions(distribution);\n\n    expect(screen.getByText('Register before 2022-01-03 00:00')).toHaveClass('ant-typography-danger');\n  });\n\n  it('should render connect with teams button for managers', () => {\n    renderActions(distribution, true);\n\n    const registerButton = screen.getByRole('button', {\n      name: /connect with teams/i,\n    });\n    expect(registerButton).toBeInTheDocument();\n  });\n\n  it('should render connect with teams when registration status is completed', () => {\n    renderActions({ ...distribution, registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Completed });\n\n    const registerButton = screen.getByRole('button', {\n      name: /connect with teams/i,\n    });\n    expect(registerButton).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/Actions.tsx",
    "content": "import { Button, Modal, Row, Space, Typography } from 'antd';\nimport Link from 'next/link';\nimport { DownOutlined } from '@ant-design/icons';\nimport { TextProps } from 'antd/lib/typography/Text';\nimport { TeamDistributionDto, TeamDistributionDtoRegistrationStatusEnum } from '@client/api';\nimport { dateWithTimeZoneRenderer } from '@client/shared/components/Table';\nimport dayjs from 'dayjs';\n\nconst { Text, Link: LinkButton } = Typography;\n\ntype Props = {\n  distribution: TeamDistributionDto;\n  register: (distributionId: number) => Promise<void>;\n  deleteRegister: (distributionId: number) => Promise<void>;\n  isManager: boolean;\n  isCourseDementor?: boolean;\n  courseAlias: string;\n  onOpenSubmitScoreModal: () => void;\n};\n\nconst getDateColor = (date: string): TextProps['type'] => {\n  const now = dayjs();\n  const currentDate = dayjs(date);\n\n  const isDeadlineSoon = now <= currentDate && currentDate.diff(now, 'hours') < 48;\n\n  if (isDeadlineSoon) return 'danger';\n};\n\nexport function Actions({\n  distribution,\n  register,\n  deleteRegister,\n  isManager,\n  isCourseDementor,\n  courseAlias,\n  onOpenSubmitScoreModal,\n}: Props) {\n  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;\n  const endDateText = dateWithTimeZoneRenderer(timezone, 'YYYY-MM-DD HH:mm')(distribution.endDate);\n  const [modal, contextHolder] = Modal.useModal();\n\n  const handleCancel = async () => {\n    modal.confirm({\n      title: 'Cancel registration',\n      content: (\n        <>\n          Are you sure you want to cancel your group task registration? You will be able to register again until{' '}\n          {endDateText}.\n        </>\n      ),\n      okText: 'Cancel Registration',\n      okButtonProps: { danger: true },\n      onOk: () => {\n        deleteRegister(distribution.id);\n      },\n    });\n  };\n\n  const renderRegistrationCancelSection = () => (\n    <>\n      {contextHolder}\n      You can{' '}\n      <LinkButton type=\"danger\" underline onClick={handleCancel}>\n        Cancel\n      </LinkButton>{' '}\n      registration before {endDateText}\n    </>\n  );\n\n  const renderActions = () => {\n    switch (distribution.registrationStatus) {\n      case TeamDistributionDtoRegistrationStatusEnum.Future:\n        return (\n          <>\n            <Button disabled>Register</Button>\n            <Text type=\"secondary\">Registration will be opened later</Text>\n          </>\n        );\n      case TeamDistributionDtoRegistrationStatusEnum.Available:\n        return (\n          <>\n            <Button type=\"primary\" onClick={() => register(distribution.id)}>\n              Register\n            </Button>\n            <Text type={getDateColor(distribution.endDate)}>Register before {endDateText}</Text>\n          </>\n        );\n      case TeamDistributionDtoRegistrationStatusEnum.Completed:\n        return (\n          <>\n            <Button icon={<DownOutlined />} disabled>\n              Registered\n            </Button>\n            <Text type=\"secondary\">\n              {dayjs() > dayjs(distribution.endDate) ? 'Registration is closed' : renderRegistrationCancelSection()}\n            </Text>\n          </>\n        );\n      case TeamDistributionDtoRegistrationStatusEnum.Distributed:\n        return (\n          <Button icon={<DownOutlined />} disabled>\n            Registered\n          </Button>\n        );\n      case TeamDistributionDtoRegistrationStatusEnum.Closed:\n        return (\n          <>\n            <Button disabled>Register</Button>\n            <Text type=\"secondary\">Registration is closed</Text>\n          </>\n        );\n\n      default:\n        return null;\n    }\n  };\n\n  return distribution.registrationStatus !== TeamDistributionDtoRegistrationStatusEnum.Unavailable ||\n    isManager ||\n    isCourseDementor ? (\n    <Row style={{ marginTop: 16 }}>\n      <Space size={24} wrap>\n        {(isManager ||\n          isCourseDementor ||\n          distribution.registrationStatus === TeamDistributionDtoRegistrationStatusEnum.Completed ||\n          distribution.registrationStatus === TeamDistributionDtoRegistrationStatusEnum.Distributed) && (\n          <Link href={`teams?course=${courseAlias}&teamDistributionId=${distribution.id}`}>\n            <Button type=\"primary\">Connect with teams</Button>\n          </Link>\n        )}\n        {isManager && (\n          <Button type=\"dashed\" onClick={onOpenSubmitScoreModal}>\n            Submit score\n          </Button>\n        )}\n        {renderActions()}\n      </Space>\n    </Row>\n  ) : null;\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/CardTitle.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport { TeamDistributionDto, TeamDistributionDtoRegistrationStatusEnum } from '@client/api';\nimport { CardTitle } from './CardTitle';\n\nconst distribution = {\n  name: 'test name',\n  startDate: '2023-01-24T00:00:00.000Z',\n  endDate: '2023-01-31T23:59:00.000Z',\n  strictTeamSize: 3,\n  minTotalScore: 100500,\n  registrationStatus: TeamDistributionDtoRegistrationStatusEnum.Available,\n} as TeamDistributionDto;\n\ndescribe('CardTitle', () => {\n  it('should display distribution name', () => {\n    render(<CardTitle distribution={distribution} />);\n    expect(screen.getByText('test name')).toBeInTheDocument();\n  });\n\n  it('should display min score when it is not 0 and registrationStatus not completed or distributed', () => {\n    render(<CardTitle distribution={distribution} />);\n    expect(screen.getByText(`Min score ${distribution.minTotalScore}`)).toBeInTheDocument();\n  });\n\n  it('should not display min score when it is 0', () => {\n    render(\n      <CardTitle\n        distribution={{\n          ...distribution,\n          minTotalScore: 0,\n        }}\n      />,\n    );\n    expect(screen.queryByText('Min score 0')).not.toBeInTheDocument();\n  });\n\n  it.each`\n    registrationStatus\n    ${TeamDistributionDtoRegistrationStatusEnum.Completed}\n    ${TeamDistributionDtoRegistrationStatusEnum.Distributed}\n  `('should not display min score when registrationStatus is $registrationStatus', ({ registrationStatus }) => {\n    render(\n      <CardTitle\n        distribution={{\n          ...distribution,\n          registrationStatus,\n        }}\n      />,\n    );\n    expect(screen.queryByText(`Min score ${distribution.minTotalScore}`)).not.toBeInTheDocument();\n  });\n\n  it.each`\n    registrationStatus                                       | text\n    ${TeamDistributionDtoRegistrationStatusEnum.Completed}   | ${'without team'}\n    ${TeamDistributionDtoRegistrationStatusEnum.Distributed} | ${'distributed'}\n  `('should render tag with $text when registrationStatus is $registrationStatus', ({ registrationStatus, text }) => {\n    render(\n      <CardTitle\n        distribution={{\n          ...distribution,\n          registrationStatus,\n        }}\n      />,\n    );\n    expect(screen.getByText(text)).toBeInTheDocument();\n  });\n\n  it('should display strict team size', () => {\n    render(<CardTitle distribution={distribution} />);\n    expect(screen.getByText(`${distribution.strictTeamSize} members`)).toBeInTheDocument();\n  });\n\n  it('should display distribution period', () => {\n    render(<CardTitle distribution={distribution} />);\n    expect(screen.getByText(/2023-01-24/i)).toBeInTheDocument();\n    expect(screen.getByText(/2023-01-31/i)).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/CardTitle.tsx",
    "content": "import { Col, Row, Space, Typography } from 'antd';\nimport TeamOutlined from '@ant-design/icons/TeamOutlined';\nimport { TeamDistributionDto, TeamDistributionDtoRegistrationStatusEnum } from '@client/api';\nimport { DistributionPeriod } from './DistributionPeriod';\nimport { RenderMinTotalScore, RenderRegistrationStatus } from './renderers';\n\ntype Props = {\n  distribution: TeamDistributionDto;\n};\n\nconst { Text, Title } = Typography;\n\nexport function CardTitle({ distribution }: Props) {\n  const isRegistrationCompletedOrDistributed =\n    distribution.registrationStatus === TeamDistributionDtoRegistrationStatusEnum.Completed ||\n    distribution.registrationStatus === TeamDistributionDtoRegistrationStatusEnum.Distributed;\n\n  return (\n    <Row gutter={24} wrap justify=\"space-between\">\n      <Col>\n        <Title level={5}>{distribution.name}</Title>\n      </Col>\n      <Col>\n        <Space size=\"middle\" wrap>\n          {isRegistrationCompletedOrDistributed ? (\n            <RenderRegistrationStatus status={distribution.registrationStatus} />\n          ) : (\n            <RenderMinTotalScore score={distribution.minTotalScore} />\n          )}\n          <Text style={{ fontSize: 14 }} type=\"secondary\">\n            <TeamOutlined style={{ marginRight: 8 }} />\n            {distribution.strictTeamSize} members\n          </Text>\n          <DistributionPeriod startDate={distribution.startDate} endDate={distribution.endDate} />\n        </Space>\n      </Col>\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/DistributionPeriod.tsx",
    "content": "import { CalendarOutlined, SwapRightOutlined } from '@ant-design/icons';\nimport { Typography } from 'antd';\nimport { dateRenderer } from '@client/shared/components/Table';\n\ntype Props = {\n  startDate: string;\n  endDate: string;\n};\n\nconst { Text } = Typography;\n\nexport function DistributionPeriod({ startDate, endDate }: Props) {\n  return (\n    <Text type=\"secondary\" style={{ fontSize: 14 }}>\n      <CalendarOutlined style={{ marginRight: 8 }} />\n      {dateRenderer(startDate)} <SwapRightOutlined /> {dateRenderer(endDate)}\n    </Text>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/TeamDistributionCard.test.tsx",
    "content": "import { render, screen, fireEvent } from '@testing-library/react';\nimport { TeamDistributionDto } from '@client/api';\nimport TeamDistributionCard from './TeamDistributionCard';\n\nconst distribution = {\n  id: 1,\n  name: 'Team Distribution 1',\n  startDate: '2022-01-01',\n  endDate: '2022-01-31',\n  description: 'This is the first team distribution.',\n  descriptionUrl: 'http://example.com',\n} as TeamDistributionDto;\n\nconst mockOnDelete = vi.fn(() => Promise.resolve());\nconst mockOnEdit = vi.fn();\nconst onRegister = vi.fn();\nconst onDeleteRegister = vi.fn();\nconst onOpenSubmitScoreModal = vi.fn();\n\nfunction renderCard(distribution: TeamDistributionDto, isManager = false) {\n  return render(\n    <TeamDistributionCard\n      distribution={distribution}\n      isManager={isManager}\n      onDelete={mockOnDelete}\n      onEdit={mockOnEdit}\n      deleteRegister={onDeleteRegister}\n      register={onRegister}\n      courseAlias=\"Test\"\n      isCourseDementor={false}\n      onOpenSubmitScoreModal={onOpenSubmitScoreModal}\n    />,\n  );\n}\n\ndescribe('TeamDistributionCard', () => {\n  it('should render the distribution name and description', () => {\n    renderCard(distribution);\n    expect(screen.getByText(distribution.name)).toBeInTheDocument();\n    expect(screen.getByText(distribution.description)).toBeInTheDocument();\n  });\n\n  it('should render the distribution period', () => {\n    renderCard(distribution);\n\n    expect(screen.getByText(/2022-01-01/)).toBeInTheDocument();\n    expect(screen.getByText(/2022-01-31/)).toBeInTheDocument();\n  });\n\n  it('should render the edit and delete buttons for managers', () => {\n    renderCard(distribution, true);\n\n    expect(screen.getByRole('button', { name: /edit/i })).toBeInTheDocument();\n    expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();\n  });\n\n  it('should not render the edit and delete buttons for non-managers', () => {\n    renderCard(distribution);\n\n    expect(screen.queryByRole('button', { name: /edit/i })).not.toBeInTheDocument();\n    expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();\n  });\n\n  it('should call the onDelete function when the delete button is clicked', () => {\n    renderCard(distribution, true);\n\n    fireEvent.click(screen.getByRole('button', { name: /delete/i }));\n\n    expect(mockOnDelete).toHaveBeenCalledWith(distribution.id);\n  });\n\n  it('should call the onEdit function when the edit button is clicked', () => {\n    renderCard(distribution, true);\n\n    fireEvent.click(screen.getByRole('button', { name: /edit/i }));\n\n    expect(mockOnEdit).toHaveBeenCalledWith(distribution);\n  });\n\n  it('should render read more link when distribution has descriptionUrl', () => {\n    renderCard(distribution);\n\n    expect(screen.getByRole('link', { name: /read more/i })).toBeInTheDocument();\n  });\n\n  it('should not render read more link when distribution has not descriptionUrl', () => {\n    renderCard({ ...distribution, descriptionUrl: '' });\n    expect(screen.queryByRole('link', { name: /read more/i })).not.toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/TeamDistributionCard.tsx",
    "content": "import { Button, Card } from 'antd';\nimport { EditOutlined, DeleteOutlined } from '@ant-design/icons';\nimport { TeamDistributionDto } from '@client/api';\nimport { Actions } from './Actions';\nimport { CardTitle } from './CardTitle';\n\ntype Props = {\n  distribution: TeamDistributionDto;\n  isManager: boolean;\n  isCourseDementor: boolean;\n  onDelete: (id: number) => Promise<void>;\n  onEdit: (distribution: TeamDistributionDto) => void;\n  register: (distributionId: number) => Promise<void>;\n  deleteRegister: (distributionId: number) => Promise<void>;\n  courseAlias: string;\n  onOpenSubmitScoreModal: () => void;\n};\n\nexport default function TeamDistributionCard({\n  distribution,\n  isManager,\n  isCourseDementor,\n  onDelete,\n  onEdit,\n  register,\n  deleteRegister,\n  courseAlias,\n  onOpenSubmitScoreModal,\n}: Props) {\n  return (\n    <Card\n      style={{ marginTop: 24 }}\n      title={<CardTitle distribution={distribution} />}\n      actions={\n        isManager\n          ? [\n              <Button key=\"edit\" icon={<EditOutlined />} onClick={() => onEdit(distribution)} />,\n              <Button key=\"delete\" icon={<DeleteOutlined />} onClick={() => onDelete(distribution.id)} />,\n            ]\n          : undefined\n      }\n    >\n      {distribution.description}\n      {distribution.descriptionUrl && (\n        <a href={distribution.descriptionUrl} target=\"_blank\">\n          {' '}\n          Read more\n        </a>\n      )}\n      <Actions\n        isManager={isManager}\n        isCourseDementor={isCourseDementor}\n        distribution={distribution}\n        register={register}\n        deleteRegister={deleteRegister}\n        courseAlias={courseAlias}\n        onOpenSubmitScoreModal={onOpenSubmitScoreModal}\n      />\n    </Card>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/index.ts",
    "content": "export { default as TeamDistributionCard } from './TeamDistributionCard';\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionCard/renderers.tsx",
    "content": "import { TeamDistributionDtoRegistrationStatusEnum } from '@client/api';\nimport ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';\nimport { Tag, Typography } from 'antd';\n\nconst { Text } = Typography;\n\ntype RegistrationStatusProps = {\n  status: TeamDistributionDtoRegistrationStatusEnum;\n};\n\nexport const RenderRegistrationStatus = ({ status }: RegistrationStatusProps) => {\n  switch (status) {\n    case TeamDistributionDtoRegistrationStatusEnum.Distributed:\n      return (\n        <Tag icon={<ClockCircleOutlined />} color=\"green\">\n          distributed\n        </Tag>\n      );\n    case TeamDistributionDtoRegistrationStatusEnum.Completed:\n      return <Tag icon={<ClockCircleOutlined />}>without team</Tag>;\n    default:\n      return null;\n  }\n};\ntype MinScoreProps = {\n  score: number;\n};\n\nexport const RenderMinTotalScore = ({ score }: MinScoreProps) => {\n  if (!score) return null;\n  return (\n    <Text style={{ fontSize: 14 }} type=\"secondary\">\n      Min score {score}\n    </Text>\n  );\n};\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionModal/TeamDistributionModal.tsx",
    "content": "import { DatePicker, Form, Input, InputNumber, message, Select, Typography, Modal } from 'antd';\nimport { CreateTeamDistributionDto, TeamDistributionApi, TeamDistributionDto } from '@client/api';\nimport { TIMEZONES } from '@client/configs/timezones';\nimport dayjs, { Dayjs } from 'dayjs';\nimport { formatTimezoneToUTC } from '@client/services/formatter';\nimport { urlPattern } from '@client/services/validators';\nimport timezone from 'dayjs/plugin/timezone';\n\nconst { TextArea } = Input;\n\ndayjs.extend(timezone);\n\ntype Props = {\n  data?: TeamDistributionDto;\n  onCancel: () => void;\n  onSubmit: () => Promise<void>;\n  courseId: number;\n};\n\nconst { Text } = Typography;\n\nconst formLayoutProps = {\n  labelCol: { span: 8 },\n  wrapperCol: { span: 24 },\n};\n\ninterface FormState extends TeamDistributionDto {\n  timeZone: string;\n  range: Dayjs[];\n}\n\nconst { Option } = Select;\n\nconst teamDistributionApi = new TeamDistributionApi();\n\nfunction getInitialValues(data: TeamDistributionDto) {\n  const timeZone = 'UTC';\n  return {\n    ...data,\n    range:\n      data.startDate && data.endDate\n        ? [data.startDate ? dayjs.utc(data.startDate) : null, data.endDate ? dayjs.utc(data.endDate) : null]\n        : null,\n    timeZone,\n    strictTeamSizeMode: data.strictTeamSizeMode ?? true,\n    strictTeamSize: data.strictTeamSize ?? 3,\n    minTotalScore: data.minTotalScore ?? 0,\n  };\n}\n\nconst createRecord = (values: Partial<FormState>): CreateTeamDistributionDto => {\n  const [startDate, endDate] = values.range as [dayjs.Dayjs, dayjs.Dayjs];\n  const record = {\n    name: values.name!,\n    description: values.description ?? '',\n    startDate: formatTimezoneToUTC(startDate, values.timeZone),\n    endDate: formatTimezoneToUTC(endDate, values.timeZone),\n    strictTeamSizeMode: values.strictTeamSizeMode ?? true,\n    minTeamSize: values.minTeamSize ?? 2,\n    maxTeamSize: values.maxTeamSize ?? 4,\n    strictTeamSize: values.strictTeamSize ?? 3,\n    minTotalScore: values.minTotalScore ?? 0,\n    descriptionUrl: values.descriptionUrl ?? '',\n  };\n  return record;\n};\n\nconst submitTeamDistribution = async (courseId: number, values: FormState, id?: number): Promise<void> => {\n  try {\n    const record = createRecord(values);\n    if (id) {\n      await teamDistributionApi.updateTeamDistribution(courseId, id, record);\n    } else {\n      await teamDistributionApi.createTeamDistribution(courseId, record);\n    }\n  } catch {\n    message.error('Failed to create team distribution. Please try later.');\n  }\n};\n\nexport default function TeamDistributionModal({ data, onCancel, courseId, onSubmit }: Props) {\n  const [form] = Form.useForm<FormState>();\n  const handleModalSubmit = async (values: FormState) => {\n    await submitTeamDistribution(courseId, values, data?.id);\n    await onSubmit();\n  };\n\n  return (\n    <Modal\n      open={true}\n      title={'Team distribution'}\n      width={756}\n      onOk={async () => {\n        const values = await form.validateFields().catch(() => null);\n        if (values == null) {\n          return;\n        }\n        handleModalSubmit(values);\n      }}\n      onCancel={() => {\n        onCancel();\n        form.resetFields();\n      }}\n    >\n      <Form {...formLayoutProps} form={form} initialValues={data ? getInitialValues(data) : undefined}>\n        <Text strong>\n          You are {data ? 'editing' : 'creating'} a group distribution event. Fill out the form to add it to the\n          schedule.\n        </Text>\n        <Form.Item name=\"name\" label=\"Name\" rules={[{ required: true, message: 'Please enter event name' }]}>\n          <Input />\n        </Form.Item>\n        <Form.Item name=\"timeZone\" label=\"TimeZone\" initialValue=\"UTC\">\n          <Select placeholder=\"Please select a timezone\">\n            {TIMEZONES.map(tz => (\n              <Option key={tz} value={tz}>\n                {/* there is no 'Europe / Kyiv' time zone at the moment */}\n                {tz === 'Europe/Kiev' ? 'Europe/Kyiv' : tz}\n              </Option>\n            ))}\n          </Select>\n        </Form.Item>\n        <Form.Item\n          name=\"range\"\n          label=\"Pre-distribution period\"\n          tooltip=\"Time frame for student registration and self distribution\"\n          rules={[{ required: true, type: 'array', message: 'Please enter start and end date' }]}\n        >\n          <DatePicker.RangePicker\n            showTime={{ format: 'HH:mm', defaultValue: [dayjs().hour(0).minute(0), dayjs().hour(23).minute(59)] }}\n          />\n        </Form.Item>\n        <Form.Item\n          name=\"strictTeamSize\"\n          label=\"Team size\"\n          initialValue={3}\n          rules={[{ required: true, message: 'Please enter team size' }]}\n        >\n          <InputNumber min={2} />\n        </Form.Item>\n        <Form.Item\n          initialValue={0}\n          name=\"minTotalScore\"\n          label=\"Minimum passing score\"\n          tooltip=\"Shows the activity of the students and their maturity to complete group tasks\"\n        >\n          <InputNumber min={0} />\n        </Form.Item>\n        <Form.Item name=\"description\" label=\"Description\">\n          <TextArea />\n        </Form.Item>\n        <Form.Item\n          name=\"descriptionUrl\"\n          label=\"Description Url\"\n          rules={[{ message: 'Please enter valid URL', pattern: urlPattern }]}\n        >\n          <Input />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/TeamDistributionModal/index.ts",
    "content": "export { default as TeamDistributionModal } from './TeamDistributionModal';\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/WelcomeCard/WelcomeCard.test.tsx",
    "content": "import { render, screen } from '@testing-library/react';\nimport WelcomeCard from './WelcomeCard';\n\ndescribe('WelcomeCard', () => {\n  it('should display correct title for managers', () => {\n    render(<WelcomeCard isManager={true} handleCreateTeamDistribution={vi.fn()} />);\n    const title = screen.getByText('Create student teams to solve group tasks!');\n    expect(title).toBeInTheDocument();\n  });\n\n  it('should display correct title for non-managers', () => {\n    render(<WelcomeCard isManager={false} handleCreateTeamDistribution={vi.fn()} />);\n    const title = screen.getByText('Become a member of the team!');\n    expect(title).toBeInTheDocument();\n  });\n\n  it('should display the create team distribution button for managers', () => {\n    render(<WelcomeCard isManager={true} handleCreateTeamDistribution={vi.fn()} />);\n    const button = screen.getByRole('button', { name: /add a new distribution/i });\n    expect(button).toBeInTheDocument();\n  });\n\n  it('should not display the create team distribution button for non-managers', () => {\n    render(<WelcomeCard isManager={false} handleCreateTeamDistribution={vi.fn()} />);\n    expect(screen.queryByRole('button', { name: /add a new distribution/i })).not.toBeInTheDocument();\n  });\n\n  it('should call the handleCreateTeamDistribution function when the create team distribution button is clicked', () => {\n    const handleCreateTeamDistribution = vi.fn();\n    render(<WelcomeCard isManager={true} handleCreateTeamDistribution={handleCreateTeamDistribution} />);\n    const button = screen.getByRole('button', { name: /add a new distribution/i });\n    button.click();\n    expect(handleCreateTeamDistribution).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/WelcomeCard/WelcomeCard.tsx",
    "content": "import { Button, Card, Col, Row, theme, Typography } from 'antd';\n\nimport { PlusOutlined } from '@ant-design/icons';\nimport { useMedia } from 'react-use';\n\nconst { Text } = Typography;\n\ntype Props = {\n  isManager: boolean;\n  handleCreateTeamDistribution: () => void;\n};\n\nexport default function WelcomeCard({ isManager, handleCreateTeamDistribution }: Props) {\n  const mobileView = useMedia('(max-width: 768px)');\n  const { token } = theme.useToken();\n  return (\n    <Card\n      title={<Text>{isManager ? 'Create student teams to solve group tasks!' : 'Become a member of the team!'}</Text>}\n      style={{\n        marginTop: 24,\n        backgroundColor: token.colorPrimaryBg,\n      }}\n    >\n      <Row gutter={[24, 12]} style={{ minHeight: '100px' }}>\n        <Col sm={24} md={12}>\n          <Text>\n            Group task – it is a possibility to unite with your colleagues to develop the best solutions, and to gain\n            knowledge and skills.\n            <br /> To become a member of a team, you can create your own team or join an existing team. If you don't\n            choose to do either of these, you will be added to a team automatically.\n          </Text>\n        </Col>\n        {!mobileView && (\n          <Col md={12} style={{ position: 'relative', display: 'flex', justifyContent: 'end', paddingRight: '24px' }}>\n            <div\n              style={{\n                backgroundImage: `url(https://cdn.rs.school/sloths/stickers/welcome/image.png)`,\n                position: 'absolute',\n                top: '-120px',\n                backgroundSize: 'contain',\n                backgroundRepeat: 'no-repeat',\n                backgroundPosition: 'center',\n                height: '240px',\n                width: '240px',\n              }}\n            />\n          </Col>\n        )}\n        {isManager && (\n          <Col>\n            <Button type=\"primary\" icon={<PlusOutlined />} onClick={handleCreateTeamDistribution}>\n              Add a new distribution\n            </Button>\n          </Col>\n        )}\n      </Row>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/components/WelcomeCard/index.ts",
    "content": "export { default as WelcomeCard } from './WelcomeCard';\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/hooks/useSubmitTeamScore.test.tsx",
    "content": "import { useSubmitTeamScore } from './useSubmitTeamScore';\nimport { TeamDistributionApi } from '@client/api';\nimport { renderHook } from '@testing-library/react';\nimport { act } from 'react-dom/test-utils';\n\nvi.mock('@client/api');\n\nconst mockError = vi.fn();\nconst mockSuccess = vi.fn();\n\nvi.mock('@client/hooks', () => ({\n  useMessage: () => ({\n    message: {\n      error: mockError,\n      success: mockSuccess,\n    },\n  }),\n}));\n\ndescribe('useSubmitTeamScore', () => {\n  afterEach(() => {\n    vi.clearAllMocks();\n  });\n\n  it('should have the correct initial states', () => {\n    const { result } = renderHook(() => useSubmitTeamScore(1, 2));\n\n    expect(result.current.loading).toBe(false);\n    expect(result.current.taskId).toBe(null);\n  });\n\n  it('should set taskId correctly', () => {\n    const { result } = renderHook(() => useSubmitTeamScore(1, 2));\n\n    act(() => {\n      result.current.setTaskId(3);\n    });\n\n    expect(result.current.taskId).toBe(3);\n  });\n\n  it('should show error message when taskId is not set and handleSubmit is called', async () => {\n    const { result } = renderHook(() => useSubmitTeamScore(1, 2));\n\n    await act(async () => {\n      await result.current.handleSubmit();\n    });\n\n    expect(TeamDistributionApi.prototype.submitScore).toHaveBeenCalledTimes(0);\n    expect(result.current.taskId).toBe(null);\n    expect(result.current.loading).toBe(false);\n    expect(mockSuccess).toHaveBeenCalledTimes(0);\n  });\n\n  it('should handle successful score submission', async () => {\n    vi.mocked(TeamDistributionApi.prototype.submitScore).mockResolvedValueOnce({});\n\n    const { result } = renderHook(() => useSubmitTeamScore(1, 2));\n\n    act(() => {\n      result.current.setTaskId(3);\n    });\n\n    await act(async () => {\n      await result.current.handleSubmit();\n    });\n\n    expect(TeamDistributionApi.prototype.submitScore).toHaveBeenCalledWith(1, 2, 3);\n    expect(result.current.taskId).toBe(null);\n    expect(result.current.loading).toBe(false);\n  });\n\n  it('should handle failed score submission', async () => {\n    vi.mocked(TeamDistributionApi.prototype.submitScore).mockRejectedValueOnce(new Error('API error'));\n\n    const { result } = renderHook(() => useSubmitTeamScore(1, 2));\n\n    act(() => {\n      result.current.setTaskId(3);\n    });\n\n    await act(async () => {\n      await result.current.handleSubmit();\n    });\n\n    expect(TeamDistributionApi.prototype.submitScore).toHaveBeenCalledWith(1, 2, 3);\n    expect(result.current.taskId).toBe(3);\n    expect(result.current.loading).toBe(false);\n    expect(mockSuccess).toHaveBeenCalledTimes(0);\n  });\n});\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/hooks/useSubmitTeamScore.tsx",
    "content": "import { TeamDistributionApi } from '@client/api';\nimport { useState } from 'react';\nimport { useMessage } from '@client/hooks';\n\nconst teamDistributionApi = new TeamDistributionApi();\n\nexport function useSubmitTeamScore(courseId: number, teamDistributionId: number) {\n  const { message } = useMessage();\n  const [loading, setLoading] = useState(false);\n  const [taskId, setTaskId] = useState<number | null>(null);\n\n  const handleSubmit = async () => {\n    if (!taskId) {\n      message.error('Please select a task before submitting.');\n      return;\n    }\n    try {\n      setLoading(true);\n\n      await teamDistributionApi.submitScore(courseId, teamDistributionId, taskId);\n\n      setTaskId(null);\n      message.success('Score submitted successfully.');\n    } catch {\n      message.error('Error occurred while submitting score.');\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return { loading, handleSubmit, setTaskId, taskId };\n}\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/pages/TeamDistributions/TeamDistributions.tsx",
    "content": "import { useMemo, useState, useContext } from 'react';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { isCourseManager, isDementor } from '@client/domain/user';\nimport { TeamDistributionApi, TeamDistributionDto } from '@client/api';\nimport { TeamDistributionModal } from '@client/modules/TeamDistribution/components/TeamDistributionModal/';\nimport { useAsync } from 'react-use';\nimport { TeamDistributionCard } from '@client/modules/TeamDistribution/components/TeamDistributionCard';\nimport { WelcomeCard } from '@client/modules/TeamDistribution/components/WelcomeCard';\nimport { useMessage, useModalForm } from '@client/hooks';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { SubmitScoreModal } from '@client/modules/TeamDistribution/components/SubmitScoreModal';\n\nconst teamDistributionApi = new TeamDistributionApi();\n\nfunction TeamDistributions() {\n  const { message } = useMessage();\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const [distributions, setDistributions] = useState<TeamDistributionDto[]>([]);\n  const {\n    open: openTeamDistributionModal,\n    toggle: toggleTeamDistributionModal,\n    formData: teamDistributionData,\n  } = useModalForm<TeamDistributionDto>();\n  const [submitScoreModalData, setSubmitScoreModalData] = useState<TeamDistributionDto | null>(null);\n\n  const isManager = useMemo(() => isCourseManager(session, course.id), [session, course.id]);\n  const isCourseDementor = useMemo(() => isDementor(session, course.id), [session, course.id]);\n\n  const loadData = async () => {\n    try {\n      const { data } = await teamDistributionApi.getCourseTeamDistributions(course.id);\n      setDistributions(data);\n    } catch {\n      message.error('Something went wrong, please try reloading the page later');\n    }\n  };\n\n  const { loading } = useAsync(loadData, [course.id]);\n\n  const handleCreateTeamDistribution = () => {\n    toggleTeamDistributionModal();\n  };\n\n  const handleEditTeamDistribution = (distribution: TeamDistributionDto) => {\n    toggleTeamDistributionModal(distribution);\n  };\n\n  const handleTeamDistributionSubmit = async () => {\n    toggleTeamDistributionModal();\n    await loadData();\n  };\n\n  const handleRegister = async (distributionId: number) => {\n    try {\n      await teamDistributionApi.teamDistributionRegistry(course.id, distributionId);\n      await loadData();\n      message.success('Registration completed.');\n    } catch {\n      message.error('Registration failed. Please try again later');\n    }\n  };\n\n  const handleDeleteRegister = async (distributionId: number) => {\n    try {\n      await teamDistributionApi.teamDistributionDeleteRegistry(course.id, distributionId);\n      await loadData();\n      message.success('Registration canceled.');\n    } catch {\n      message.error('Cancellation of registration failed. Please try again later');\n    }\n  };\n\n  const handleDeleteTeamDistribution = async (distributionId: number) => {\n    try {\n      await teamDistributionApi.deleteTeamDistribution(course.id, distributionId);\n      await loadData();\n    } catch {\n      message.error('Failed to delete team distribution. Please try later.');\n    }\n  };\n\n  return (\n    <PageLayout loading={loading} title=\"RS Teams\" showCourseName>\n      {openTeamDistributionModal && (\n        <TeamDistributionModal\n          data={teamDistributionData}\n          onSubmit={handleTeamDistributionSubmit}\n          onCancel={() => toggleTeamDistributionModal()}\n          courseId={course.id}\n        />\n      )}\n      {isManager && (\n        <SubmitScoreModal distribution={submitScoreModalData} onClose={() => setSubmitScoreModalData(null)} />\n      )}\n      <div style={{ maxWidth: '1020px', margin: '0 auto' }}>\n        <WelcomeCard isManager={isManager} handleCreateTeamDistribution={handleCreateTeamDistribution} />\n\n        {distributions.length\n          ? distributions.map(distribution => (\n              <TeamDistributionCard\n                register={handleRegister}\n                deleteRegister={handleDeleteRegister}\n                distribution={distribution}\n                courseAlias={course.alias}\n                isManager={isManager}\n                isCourseDementor={isCourseDementor}\n                onDelete={handleDeleteTeamDistribution}\n                onEdit={handleEditTeamDistribution}\n                onOpenSubmitScoreModal={() => setSubmitScoreModalData(distribution)}\n                key={distribution.id}\n              />\n            ))\n          : null}\n      </div>\n    </PageLayout>\n  );\n}\n\nexport default TeamDistributions;\n"
  },
  {
    "path": "client/src/modules/TeamDistribution/pages/TeamDistributions/index.tsx",
    "content": "export { default as TeamDistributions } from './TeamDistributions';\n"
  },
  {
    "path": "client/src/modules/Teams/Pages/Teams.tsx",
    "content": "import { message, Modal, Row, Typography } from 'antd';\nimport { useMemo, useState, useContext } from 'react';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport {\n  JoinTeamModal,\n  MyTeamSection,\n  StudentsWithoutTeamSection,\n  TeamModal,\n  TeamsHeader,\n  TeamsSection,\n} from '../components';\nimport { isActiveStudent, isCourseManager } from '@client/domain/user';\nimport { useCopyToClipboard } from 'react-use';\nimport { CreateTeamDto, TeamApi, TeamDto, JoinTeamDto, TeamDistributionApi } from '@client/api';\nimport { useLoading } from '@client/components/useLoading';\nimport { useDistribution } from '../hooks';\nimport { useMessage, useModalForm } from '@client/hooks';\nimport { SessionContext, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useRouter } from 'next/router';\nimport CheckCircleTwoTone from '@ant-design/icons/CheckCircleTwoTone';\n\nconst { Title, Text } = Typography;\n\nconst teamApi = new TeamApi();\nconst teamDistributionApi = new TeamDistributionApi();\n\nfunction Teams() {\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const router = useRouter();\n  const { notification } = useMessage();\n  const [modal, contextHolder] = Modal.useModal();\n\n  const teamDistributionId = Number(router.query.teamDistributionId);\n\n  const {\n    distribution,\n    loadDistribution,\n    loading: loadingDistribution,\n  } = useDistribution(course.id, teamDistributionId);\n\n  const { open: openTeamModal, toggle: toggleTeamModal, mode, formData: teamData } = useModalForm<Partial<TeamDto>>();\n  const [loading, withLoading] = useLoading(false);\n\n  const [showJoinTeamModal, setShowJoinTeamModal] = useState(false);\n\n  const [activeTab, setActiveTab] = useState('teams');\n\n  const [, copyToClipboard] = useCopyToClipboard();\n\n  const isManager = useMemo(() => isCourseManager(session, course.id), [session, course.id]);\n  const isStudent = useMemo(() => isActiveStudent(session, course.id), [session, course.id]);\n  const studentId = useMemo(() => session.courses[course.id]?.studentId, [session, course.id]);\n\n  const handleCreateTeam = () => {\n    toggleTeamModal();\n  };\n\n  const distributeStudentsToTeam = withLoading(async () => {\n    try {\n      await teamDistributionApi.distributeStudentsToTeam(course.id, teamDistributionId);\n    } catch {\n      message.error('Failed to distribute students to team. Please try later.');\n    }\n  });\n\n  const handleDistributeStudents = async () => {\n    await distributeStudentsToTeam();\n    await loadDistribution();\n  };\n\n  const handleJoinTeam = () => {\n    setShowJoinTeamModal(true);\n  };\n\n  const joinTeam = withLoading(async (teamId: number, record: JoinTeamDto) => {\n    try {\n      const { data: team } = await teamApi.joinTeam(course.id, teamDistributionId, teamId, record);\n      await loadDistribution();\n      setShowJoinTeamModal(false);\n      modal.success({\n        title: <Title level={5}>Successfully joined to the {team.name}</Title>,\n        content: (\n          <div>\n            <Text type=\"secondary\">{team.description}</Text>\n          </div>\n        ),\n        okText: 'Next',\n      });\n    } catch {\n      message.error('Failed to join to team. Please try later.');\n    }\n  });\n\n  const copyPassword = async (teamId: number): Promise<void> => {\n    const teamApi = new TeamApi();\n    try {\n      const { data } = await teamApi.getTeamPassword(course.id, teamDistributionId, teamId);\n      copyToClipboard(data.password);\n      notification.success({ message: 'Password copied to clipboard', duration: 2 });\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    }\n  };\n\n  const changePassword = async (teamId: number): Promise<void> => {\n    const teamApi = new TeamApi();\n    try {\n      const { data } = await teamApi.changeTeamPassword(course.id, teamDistributionId, teamId);\n      copyToClipboard(data.password);\n      notification.success({ message: 'New Password copied to clipboard', duration: 2 });\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    }\n  };\n\n  const submitTeam = withLoading(async (record: CreateTeamDto, id?: number) => {\n    try {\n      if (id) {\n        await teamApi.updateTeam(course.id, teamDistributionId, id, record);\n      } else {\n        const { data: team } = await teamApi.createTeam(course.id, teamDistributionId, record);\n        modal.confirm({\n          title: <Title level={5}>{team.name} is created successfully</Title>,\n          content: (\n            <div>\n              <Title level={5}>As a team lead you get an invitation password to join members</Title>\n              <Text type=\"secondary\">{team.description}</Text>\n            </div>\n          ),\n          cancelText: 'Next',\n          cancelButtonProps: { type: 'primary' },\n          onOk: () => copyPassword(team.id),\n          okText: 'Copy invitation password',\n          okButtonProps: { type: 'default' },\n          icon: <CheckCircleTwoTone twoToneColor=\"#52c41a\" />,\n        });\n      }\n      await loadDistribution();\n      toggleTeamModal();\n    } catch {\n      message.error('Failed to create team. Please try later.');\n    }\n  });\n\n  const contentRenderers = () => {\n    if (!distribution) {\n      return null;\n    }\n    switch (activeTab) {\n      case 'teams':\n        return <TeamsSection distribution={distribution} toggleTeamModal={toggleTeamModal} isManager={isManager} />;\n\n      case 'students':\n        return (\n          <StudentsWithoutTeamSection\n            distribution={distribution}\n            isManager={isManager}\n            reloadDistribution={loadDistribution}\n          />\n        );\n\n      case 'myTeam':\n        return (\n          <MyTeamSection\n            distribution={distribution}\n            toggleTeamModal={toggleTeamModal}\n            studentId={studentId}\n            copyPassword={copyPassword}\n            changePassword={changePassword}\n            reloadDistribution={loadDistribution as () => Promise<void>}\n            setActiveTab={setActiveTab}\n          />\n        );\n\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <PageLayout loading={loadingDistribution || loading} title=\"RS Teams\" showCourseName withMargin={false}>\n      {contextHolder}\n      {openTeamModal && distribution && (\n        <TeamModal\n          mode={mode}\n          isManager={isManager}\n          data={teamData}\n          onSubmit={submitTeam}\n          onCancel={toggleTeamModal}\n          maxStudentsCount={distribution.strictTeamSize}\n          courseId={distribution.courseId}\n        />\n      )}\n      {showJoinTeamModal && <JoinTeamModal onSubmit={joinTeam} onCancel={() => setShowJoinTeamModal(false)} />}\n      {distribution ? (\n        <TeamsHeader\n          courseAlias={course.alias}\n          isManager={isManager}\n          isStudent={isStudent}\n          activeTab={activeTab}\n          distribution={distribution}\n          setActiveTab={setActiveTab}\n          handleCreateTeam={handleCreateTeam}\n          handleDistributeStudents={handleDistributeStudents}\n          handleJoinTeam={handleJoinTeam}\n        />\n      ) : null}\n      <Row style={{ padding: '24px', margin: 24 }}>{contentRenderers()}</Row>\n    </PageLayout>\n  );\n}\n\nexport default Teams;\n"
  },
  {
    "path": "client/src/modules/Teams/components/JoinTeamModal/JoinTeamModal.tsx",
    "content": "import { Form, Input, Typography, Modal } from 'antd';\nimport { JoinTeamDto } from '@client/api';\nimport { passwordPattern } from '@client/services/validators';\n\ntype Props = {\n  onCancel: () => void;\n  onSubmit: (id: number, record: JoinTeamDto) => Promise<void>;\n};\n\nconst { Text } = Typography;\n\nconst layout = {\n  labelCol: { span: 6 },\n  wrapperCol: { span: 18 },\n};\n\nexport default function JoinTeamModal({ onCancel, onSubmit }: Props) {\n  const [form] = Form.useForm<Partial<JoinTeamDto>>();\n\n  const handleModalSubmit = async (values: Partial<JoinTeamDto>) => {\n    const [id, password] = values.password!.split('_');\n\n    if (!password || !Number(id)) {\n      return;\n    }\n    await onSubmit(Number(id), { password });\n  };\n\n  return (\n    <Modal\n      style={{ top: 20 }}\n      width={756}\n      open={true}\n      title=\"Join team\"\n      okText=\"Join\"\n      onOk={async () => {\n        const values = await form.validateFields().catch(() => null);\n        if (values == null) {\n          return;\n        }\n        handleModalSubmit(values);\n      }}\n      onCancel={() => {\n        onCancel();\n        form.resetFields();\n      }}\n    >\n      <Form {...layout} form={form}>\n        <Text>\n          You're joining to the team for group task solving. Enter the invitation password you've received from the team\n          lead.\n        </Text>\n        <Form.Item\n          style={{ marginTop: 16 }}\n          name=\"password\"\n          label=\"Team password\"\n          rules={[\n            { required: true, message: 'Please enter team password' },\n            { message: 'Please enter valid password', pattern: passwordPattern },\n          ]}\n        >\n          <Input.Password />\n        </Form.Item>\n      </Form>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/MyTeamSection/MyTeamSection.tsx",
    "content": "import { Button, Row, Space, Typography } from 'antd';\nimport { EditTwoTone, CopyOutlined, RedoOutlined } from '@ant-design/icons';\nimport { useMemo } from 'react';\nimport { TeamApi, TeamDistributionDetailedDto, TeamDto } from '@client/api';\nimport StudentsTable from '../StudentsTable/StudentsTable';\nimport { useCopyToClipboard } from 'react-use';\nimport { useMessage } from '@client/hooks';\n\nconst { Text, Title } = Typography;\n\ntype Props = {\n  distribution: TeamDistributionDetailedDto;\n  toggleTeamModal: (data?: Partial<TeamDto> | undefined) => void;\n  studentId?: number;\n  copyPassword: (teamId: number) => Promise<void>;\n  changePassword: (teamId: number) => Promise<void>;\n  reloadDistribution: () => Promise<void>;\n  setActiveTab: React.Dispatch<React.SetStateAction<string>>;\n};\n\nconst teamApi = new TeamApi();\n\nexport default function MyTeamSection({\n  distribution,\n  toggleTeamModal,\n  studentId,\n  copyPassword,\n  changePassword,\n  reloadDistribution,\n  setActiveTab,\n}: Props) {\n  const myTeam = distribution.myTeam;\n  const { notification } = useMessage();\n\n  const isTeamLead = useMemo(() => studentId === myTeam?.teamLeadId, [studentId, myTeam?.teamLeadId]);\n  const [, copyToClipboard] = useCopyToClipboard();\n\n  const copyChatLink = () => {\n    copyToClipboard(myTeam?.chatLink);\n    notification.success({ message: 'Chat link copied to clipboard', duration: 2 });\n  };\n\n  const leaveTeam = async () => {\n    await teamApi.leaveTeam(distribution.courseId, distribution.id, myTeam?.id);\n    setActiveTab('teams');\n    await reloadDistribution();\n  };\n\n  return distribution.myTeam ? (\n    <Space size=\"large\" direction=\"vertical\" style={{ width: '100%' }}>\n      <Title level={5}>{myTeam.name}</Title>\n      <Space size={12}>\n        <Text type=\"secondary\">{myTeam.description}</Text>\n        {isTeamLead && <EditTwoTone twoToneColor=\"#1890FF\" onClick={() => toggleTeamModal(myTeam)} />}\n      </Space>\n      <Row justify=\"end\">\n        <Space size=\"small\" wrap>\n          {isTeamLead && (\n            <>\n              <Button onClick={() => copyPassword(myTeam.id)} icon={<CopyOutlined />}>\n                Invitation password\n              </Button>\n              <Button onClick={() => changePassword(myTeam.id)} icon={<RedoOutlined />}>\n                Change password\n              </Button>\n              <Button onClick={() => copyChatLink()} icon={<CopyOutlined />} disabled={!myTeam.chatLink}>\n                Chat link\n              </Button>\n            </>\n          )}\n          <Button type=\"link\" onClick={leaveTeam}>\n            Leave Team\n          </Button>\n        </Space>\n      </Row>\n      <StudentsTable content={myTeam.students} pagination={false} teamLeadId={myTeam.teamLeadId} />\n    </Space>\n  ) : null;\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/StudentsTable/StudentsTable.tsx",
    "content": "import { Table, TablePaginationConfig, TableProps } from 'antd';\nimport { TeamDistributionStudentDto } from '@client/api';\nimport { StudentsTableColumnKey } from '@client/modules/Teams/constants';\nimport { useMemo } from 'react';\nimport { getColumns } from './renderers';\n\ntype Props = {\n  content: TeamDistributionStudentDto[];\n  teamLeadId?: number;\n  notVisibleColumn?: StudentsTableColumnKey[];\n  pagination: false | TablePaginationConfig;\n  handleChange?: TableProps<TeamDistributionStudentDto>['onChange'];\n  loading?: boolean;\n  onDelete?: (student: TeamDistributionStudentDto) => void;\n};\n\nexport default function StudentsTable({\n  content,\n  teamLeadId,\n  notVisibleColumn = [],\n  pagination,\n  handleChange,\n  loading,\n  onDelete,\n}: Props) {\n  const columns = useMemo(\n    () => getColumns(teamLeadId, onDelete).filter(el => !notVisibleColumn.includes(el.key as StudentsTableColumnKey)),\n    [notVisibleColumn, teamLeadId, onDelete],\n  );\n\n  return (\n    <Table<TeamDistributionStudentDto>\n      showHeader\n      dataSource={content}\n      columns={columns}\n      onChange={handleChange}\n      rowKey=\"id\"\n      pagination={pagination}\n      loading={loading}\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/StudentsTable/renderers.tsx",
    "content": "import { Space, Tag, Typography, Button } from 'antd';\nimport { ColumnsType } from 'antd/lib/table';\nimport { TeamDistributionStudentDto } from '@client/api';\nimport { StudentsTableColumnKey, StudentsTableColumnName } from '@client/modules/Teams/constants';\nimport { TeamOutlined, DeleteOutlined } from '@ant-design/icons';\nimport { Breakpoint } from 'antd/lib';\nimport { StudentDiscord } from '@client/components/StudentDiscord';\nconst { Text, Link } = Typography;\n\nfunction renderName({ fullName, cvUuid, id }: TeamDistributionStudentDto, teamLeadId?: number) {\n  return cvUuid ? (\n    <Link target=\"_blank\" href={`${window.location.origin}/cv/${cvUuid}`}>\n      <Space size=\"small\">\n        {fullName}\n        {id === teamLeadId && (\n          <Tag color=\"blue\">\n            <TeamOutlined color=\"white\" />\n          </Tag>\n        )}\n      </Space>\n    </Link>\n  ) : (\n    <Space size=\"small\">\n      {fullName}\n      {id === teamLeadId && (\n        <Tag color=\"blue\">\n          <TeamOutlined color=\"white\" />\n        </Tag>\n      )}\n    </Space>\n  );\n}\n\nfunction renderGithub(_v: string, { githubId }: TeamDistributionStudentDto) {\n  return (\n    <Link target=\"_blank\" href={`https://github.com/${githubId}`}>\n      {githubId}\n    </Link>\n  );\n}\n\nfunction renderPosition(_v: string, { rank }: TeamDistributionStudentDto) {\n  return <Text>{rank}</Text>;\n}\n\nfunction renderLocation(_v: string, { location }: TeamDistributionStudentDto) {\n  return <Text>{location}</Text>;\n}\n\nfunction renderEmail(_v: string, { email }: TeamDistributionStudentDto) {\n  return <Text>{email}</Text>;\n}\n\nfunction renderDiscord(_v: string, { discord }: TeamDistributionStudentDto) {\n  return <StudentDiscord discord={discord} />;\n}\n\nfunction renderStudent(_v: string, student: TeamDistributionStudentDto) {\n  return (\n    <Space direction=\"vertical\" size=\"small\">\n      {renderName(student)}\n      {renderLocation('', student)}\n      <Text>rank: {renderPosition('', student)}</Text>\n    </Space>\n  );\n}\n\nfunction renderContacts(_v: string, student: TeamDistributionStudentDto) {\n  return (\n    <Space direction=\"vertical\" size=\"small\">\n      {renderDiscord('', student)}\n      {renderEmail('', student)}\n    </Space>\n  );\n}\n\nfunction renderDeleteAction(\n  student: TeamDistributionStudentDto,\n  onDelete?: (student: TeamDistributionStudentDto) => void,\n) {\n  if (!onDelete) return null;\n  return <Button type=\"text\" danger icon={<DeleteOutlined />} onClick={() => onDelete(student)} />;\n}\n\nconst DISPLAY_TABLE_BREAKPOINTS: Breakpoint[] = ['md'];\nconst DISPLAY_TABLE_MOBILE_BREAKPOINT: Breakpoint[] = ['xs'];\n\nexport const getColumns = (\n  teamLeadId?: number,\n  onDelete?: (student: TeamDistributionStudentDto) => void,\n): ColumnsType<TeamDistributionStudentDto> => [\n  {\n    key: StudentsTableColumnKey.Name,\n    title: StudentsTableColumnName.Name,\n    dataIndex: 'name',\n    width: '20%',\n    render: (_v, t) => renderName(t, teamLeadId),\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: StudentsTableColumnKey.Position,\n    title: StudentsTableColumnName.Position,\n    dataIndex: 'rank',\n    align: 'right',\n    width: '10%',\n    render: renderPosition,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: StudentsTableColumnKey.Email,\n    title: StudentsTableColumnName.Email,\n    dataIndex: 'email',\n    width: '10%',\n    render: renderEmail,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: StudentsTableColumnKey.Discord,\n    title: StudentsTableColumnName.Discord,\n    dataIndex: 'discord',\n    width: '10%',\n    render: renderDiscord,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: StudentsTableColumnKey.GitHub,\n    title: StudentsTableColumnName.GitHub,\n    dataIndex: 'github',\n    width: '10%',\n    render: renderGithub,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: StudentsTableColumnKey.Location,\n    title: StudentsTableColumnName.Location,\n    dataIndex: 'location',\n    width: '20%',\n    render: renderLocation,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: StudentsTableColumnKey.Student,\n    title: StudentsTableColumnName.Student,\n    width: '50%',\n    render: renderStudent,\n    responsive: DISPLAY_TABLE_MOBILE_BREAKPOINT,\n  },\n  {\n    key: StudentsTableColumnKey.Contacts,\n    title: StudentsTableColumnName.Contacts,\n    width: '50%',\n    render: renderContacts,\n    responsive: DISPLAY_TABLE_MOBILE_BREAKPOINT,\n  },\n  ...(onDelete\n    ? [\n        {\n          key: StudentsTableColumnKey.Action,\n          title: StudentsTableColumnName.Action,\n          width: '10%',\n          render: (_v: string, student: TeamDistributionStudentDto) => renderDeleteAction(student, onDelete),\n          responsive: DISPLAY_TABLE_BREAKPOINTS,\n        },\n      ]\n    : []),\n];\n"
  },
  {
    "path": "client/src/modules/Teams/components/StudentsWithoutTeamSection/StudentsWithoutTeamSection.tsx",
    "content": "import { Col, Input, Row, Space, TablePaginationConfig, Typography, Modal } from 'antd';\nimport { useState } from 'react';\n\nimport { TeamDistributionApi, TeamDistributionDetailedDto, TeamDistributionStudentDto } from '@client/api';\nimport { useAsync } from 'react-use';\nimport StudentsTable from '../StudentsTable/StudentsTable';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport { useLoading } from '@client/components/useLoading';\nimport { useMessage } from '@client/hooks';\n\ntype Props = {\n  distribution: TeamDistributionDetailedDto;\n  isManager: boolean;\n  reloadDistribution: () => Promise<TeamDistributionDetailedDto | undefined>;\n};\n\ntype StudentsState = {\n  content: TeamDistributionStudentDto[];\n  pagination: IPaginationInfo;\n  search: string;\n};\n\nconst { Title } = Typography;\nconst { confirm } = Modal;\n\nconst teamDistributionApi = new TeamDistributionApi();\n\nexport default function StudentsWithoutTeamSection({ distribution, isManager, reloadDistribution }: Props) {\n  const { message } = useMessage();\n  const [students, setStudents] = useState<StudentsState>({\n    content: [],\n    pagination: { current: 1, pageSize: 10 },\n    search: '',\n  });\n  const [loading, withLoading] = useLoading(false);\n  const [search, setSearch] = useState<string>('');\n\n  const onSearch = (value: string) => {\n    setSearch(value);\n  };\n\n  const getStudents = withLoading(async (pagination: TablePaginationConfig) => {\n    const { data } = await teamDistributionApi.getStudentsWithoutTeam(\n      distribution.courseId,\n      distribution.id,\n      pagination.pageSize ?? 10,\n      pagination.current ?? 1,\n      search,\n    );\n    setStudents({ ...students, ...data });\n  });\n\n  const handleDeleteStudent = async (student: TeamDistributionStudentDto) => {\n    confirm({\n      title: 'Are you sure you want to remove this student?',\n      content: `This will remove ${student.fullName} from the team distribution.`,\n      okText: 'Yes',\n      okType: 'danger',\n      cancelText: 'No',\n      onOk: async () => {\n        try {\n          await teamDistributionApi.teamDistributionControllerDeleteStudentFromDistribution(\n            student.id,\n            distribution.courseId,\n            distribution.id,\n          );\n          message.success('Student removed successfully');\n          await getStudents(students.pagination);\n          await reloadDistribution();\n        } catch {\n          message.error('Failed to remove student. Please try again later.');\n        }\n      },\n    });\n  };\n\n  useAsync(async () => await getStudents(students.pagination), [distribution, search]);\n\n  return (\n    <Space size={24} direction=\"vertical\" style={{ width: '100%' }}>\n      <Row justify=\"space-between\">\n        <Col span={8}>\n          <Title level={5}>{`${distribution.name} teams`}</Title>\n        </Col>\n        <Col md={6} xl={4}>\n          <Input.Search placeholder=\"input search text\" allowClear onSearch={onSearch} />\n        </Col>\n      </Row>\n      <StudentsTable\n        content={students.content}\n        pagination={students.pagination}\n        handleChange={getStudents}\n        loading={loading}\n        onDelete={isManager ? handleDeleteStudent : undefined}\n      />\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/TeamModal/TeamModal.test.tsx",
    "content": "import { screen, render, fireEvent } from '@testing-library/react';\nimport TeamModal from './TeamModal';\n\nconst onSubmit = vi.fn();\nconst onCancel = vi.fn();\n\nfunction renderModal() {\n  return render(\n    <TeamModal\n      isManager={false}\n      maxStudentsCount={10}\n      courseId={1}\n      mode=\"create\"\n      data={{}}\n      onCancel={onCancel}\n      onSubmit={onSubmit}\n    />,\n  );\n}\n\ndescribe('TeamModal', () => {\n  it('should call onCancel when canceling', () => {\n    renderModal();\n    const cancelButton = screen.getByRole('button', {\n      name: /cancel/i,\n    });\n    fireEvent.click(cancelButton);\n    expect(onCancel).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Teams/components/TeamModal/TeamModal.tsx",
    "content": "import { Form, Input, message, Space, Typography, Modal } from 'antd';\nimport { CreateTeamDto, TeamDto } from '@client/api';\nimport { StudentSearch } from '@client/shared/components/StudentSearch';\nimport { useLoading } from '@client/components/useLoading';\nimport { urlPattern } from '@client/services/validators';\n\nconst { TextArea } = Input;\n\ntype Props = {\n  data?: Partial<TeamDto>;\n  isManager: boolean;\n  onCancel: () => void;\n  onSubmit: (record: CreateTeamDto, id?: number) => Promise<void>;\n  maxStudentsCount: number;\n  courseId: number;\n  mode: 'create' | 'edit';\n};\n\nconst { Text } = Typography;\n\nconst layout = {\n  labelCol: { span: 6 },\n  wrapperCol: { span: 20 },\n};\n\nexport default function TeamModal({ onCancel, onSubmit, data, courseId, isManager, maxStudentsCount, mode }: Props) {\n  const [form] = Form.useForm<CreateTeamDto>();\n  const [loading, withLoading] = useLoading(false);\n\n  const createRecord = ({\n    name = 'Team name',\n    description = 'Team description',\n    chatLink = 'team chat',\n    studentIds = [] as number[],\n  }): CreateTeamDto => {\n    return {\n      name: name,\n      description: description,\n      chatLink: chatLink,\n      studentIds: isManager ? studentIds : undefined,\n    };\n  };\n\n  const handleModalSubmit = withLoading(async (values: CreateTeamDto) => {\n    const record = createRecord(values);\n    await onSubmit(record, data?.id);\n  });\n\n  const handleChangeStudents = (value: number[]) => {\n    if (value.length <= maxStudentsCount) {\n      form.setFieldsValue({\n        studentIds: value,\n      });\n    } else {\n      message.warning(`You can only select a maximum of ${maxStudentsCount} students.`);\n    }\n  };\n\n  return (\n    <Modal\n      style={{ top: 20 }}\n      width={756}\n      open={true}\n      title={mode === 'edit' ? 'Edit Team' : 'Create Team'}\n      okText={mode === 'edit' ? 'Edit' : 'Create'}\n      onOk={async () => {\n        const values = await form.validateFields().catch(() => null);\n        if (values == null) {\n          return;\n        }\n        handleModalSubmit(values);\n      }}\n      okButtonProps={{ disabled: loading }}\n      onCancel={() => {\n        onCancel();\n        form.resetFields();\n      }}\n    >\n      <Form {...layout} form={form} initialValues={data}>\n        <Text>You're creating the team for solving a group task. Fill out the form to invite new members.</Text>\n        <Form.Item name=\"name\" label=\"Name\" rules={[{ required: true, message: 'Please enter team name' }]}>\n          <Input />\n        </Form.Item>\n        <Form.Item\n          name=\"description\"\n          label=\"Description\"\n          rules={[{ required: true, message: 'Please enter team description' }]}\n        >\n          <TextArea showCount maxLength={600} />\n        </Form.Item>\n        <Space direction=\"vertical\">\n          <Text>\n            The only person who creates the team is the team leader, who shares the team password with the other\n            participants.\n          </Text>\n          <Text>Provide link to the group chat to Discord and create a specify password</Text>\n        </Space>\n        <Form.Item\n          name=\"chatLink\"\n          label=\"Link to Discord server\"\n          rules={[\n            { required: true, message: 'Please enter link' },\n            { message: 'Please enter valid URL', pattern: urlPattern },\n          ]}\n          style={{ marginTop: 16 }}\n        >\n          <Input />\n        </Form.Item>\n        {isManager && (\n          <Form.Item\n            name=\"studentIds\"\n            rules={[\n              { required: true, message: 'Please select students' },\n              {\n                validator: (_, value) =>\n                  value.length <= maxStudentsCount\n                    ? Promise.resolve()\n                    : Promise.reject(`You can only select a maximum of ${maxStudentsCount} students.`),\n                message: `You can only select a maximum of ${maxStudentsCount} students.`,\n              },\n            ]}\n            label=\"Students\"\n            initialValue={mode === 'edit' ? data?.students?.map(s => s.id) : undefined}\n          >\n            <StudentSearch\n              onChange={handleChangeStudents as any}\n              keyField=\"id\"\n              courseId={courseId}\n              mode=\"multiple\"\n              defaultValues={data?.students?.map(student => ({\n                name: student.fullName,\n                id: student.id,\n                githubId: student.githubId,\n              }))}\n            />\n          </Form.Item>\n        )}\n      </Form>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/TeamsHeader/ActionCard.tsx",
    "content": "import { Button, Card, Col, Popconfirm, Space, theme, Typography } from 'antd';\n\nconst { Title, Text } = Typography;\n\ntype Props = {\n  title: string;\n  text: string;\n  buttonCaption: string;\n  onClick: () => void;\n};\n\nexport function ActionCard({ title, text, buttonCaption, onClick }: Props) {\n  const { token } = theme.useToken();\n\n  return (\n    <Col xs={24} sm={12}>\n      <Card title={<Title level={5}>{title}</Title>} style={{ backgroundColor: token.blue1 }}>\n        <Space size={12} direction=\"vertical\">\n          <Text type=\"secondary\">{text}</Text>\n          <Popconfirm title={<>Are you sure you want to {buttonCaption.toLowerCase()}?</>} onConfirm={onClick}>\n            <Button>{buttonCaption}</Button>\n          </Popconfirm>\n        </Space>\n      </Card>\n    </Col>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/TeamsHeader/TeamsHeader.tsx",
    "content": "import { Col, Row, Space, Tabs, Tag, Typography } from 'antd';\nimport { ArrowLeftOutlined, ClockCircleOutlined } from '@ant-design/icons';\nimport Link from 'next/link';\nimport { TeamDistributionDetailedDto } from '@client/api';\nimport { tabRenderer } from '@client/components/TabsWithCounter/renderers';\nimport { Dispatch, SetStateAction, useMemo } from 'react';\nimport { ActionCard } from './ActionCard';\n\nconst { Title, Text } = Typography;\n\ntype Props = {\n  courseAlias: string;\n  isStudent: boolean;\n  isManager: boolean;\n  distribution: TeamDistributionDetailedDto;\n  activeTab: string;\n  setActiveTab: Dispatch<SetStateAction<string>>;\n  handleCreateTeam: () => void;\n  handleDistributeStudents: () => void;\n  handleJoinTeam: () => void;\n};\n\nexport default function TeamsHeader({\n  courseAlias,\n  isStudent,\n  distribution,\n  isManager,\n  activeTab,\n  handleCreateTeam,\n  handleDistributeStudents,\n  handleJoinTeam,\n  setActiveTab,\n}: Props) {\n  const tabs = useMemo(() => {\n    const tabs = [\n      { key: 'teams', label: 'Available teams', count: distribution.teamsCount },\n      { key: 'students', label: 'Students without team', count: distribution.studentsWithoutTeamCount },\n    ];\n    if (distribution.myTeam) {\n      tabs.push({ key: 'myTeam', label: 'My team', count: 0 });\n    }\n    return tabs.map(el => tabRenderer(el, activeTab));\n  }, [activeTab, distribution]);\n\n  return (\n    <Row style={{ padding: '24px 24px 0' }}>\n      <Col span={24}>\n        <Row justify=\"start\" align=\"middle\" gutter={24}>\n          <Col>\n            <Space size=\"small\">\n              {' '}\n              <Link href={`team-distributions?course=${courseAlias}`}>\n                <ArrowLeftOutlined />\n              </Link>\n              <Title level={4} style={{ marginBottom: 0 }}>\n                Teams\n              </Title>\n            </Space>\n          </Col>\n          <Col>\n            <Text type=\"secondary\">Distribution of participants per team</Text>\n          </Col>\n          {isStudent && (\n            <Col>\n              <Space size={12}>\n                <Text type=\"secondary\">My status:</Text>\n                {distribution.myTeam ? (\n                  <Tag icon={<ClockCircleOutlined />} color=\"green\">\n                    distributed\n                  </Tag>\n                ) : (\n                  <Tag icon={<ClockCircleOutlined />}>without team</Tag>\n                )}\n              </Space>\n            </Col>\n          )}\n        </Row>\n        <Title level={5} style={{ marginLeft: 27 }}>\n          The roles of team members are determined automatically.\n        </Title>\n\n        {!isManager && isStudent && !distribution.myTeam && (\n          <Row gutter={[24, 12]}>\n            {!distribution.myTeam && (\n              <ActionCard\n                title=\"Are you going to be a leader completing a group task?\"\n                text=\"Create the team, compose a description and provide a link to a team chat. You’ll get an invitation\n                      password to share with your team members. Being a leader is honorable and responsible\"\n                buttonCaption=\"Create team\"\n                onClick={handleCreateTeam}\n              />\n            )}\n            {isStudent && !distribution.myTeam && (\n              <ActionCard\n                title=\"Have you found a great team to join?\"\n                text=\"View the list of available teams, find an exciting description. Done this? Ask a team lead to\n                      share an invitation password to become a member of the greatest team\"\n                buttonCaption=\"Join team\"\n                onClick={handleJoinTeam}\n              />\n            )}\n          </Row>\n        )}\n        {isManager && (\n          <Row gutter={[24, 12]}>\n            <ActionCard\n              title=\"Team management\"\n              text=\"You can manage unformed teams, combine small of them or create specific team manually\"\n              buttonCaption=\"Create team\"\n              onClick={handleCreateTeam}\n            />\n            <ActionCard\n              title=\"Student distribution\"\n              text=\"All registered students will be grouped into teams according to the distribution settings specified\"\n              buttonCaption=\"Distribute students\"\n              onClick={handleDistributeStudents}\n            />\n          </Row>\n        )}\n        <Tabs tabBarStyle={{ marginBottom: 0 }} activeKey={activeTab} items={tabs} onChange={setActiveTab} />\n      </Col>\n    </Row>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/TeamsSection/TeamsSection.tsx",
    "content": "import { Col, Input, Row, Space, Table, TablePaginationConfig, TableProps, Typography } from 'antd';\nimport { useMemo, useState } from 'react';\n\nimport { TeamApi, TeamDistributionDetailedDto, TeamDto } from '@client/api';\nimport { useAsync } from 'react-use';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\nimport { getColumns, expandedRowRender } from './renderers';\nimport { useLoading } from '@client/components/useLoading';\nimport { TeamsTableColumnKey } from '@client/modules/Teams/constants';\n\ntype Props = {\n  distribution: TeamDistributionDetailedDto;\n  toggleTeamModal: (data?: Partial<TeamDto> | undefined) => void;\n  isManager: boolean;\n};\n\ntype TeamsState = {\n  content: TeamDto[];\n  pagination: IPaginationInfo;\n};\n\nconst { Title } = Typography;\n\nconst teamApi = new TeamApi();\n\nexport default function TeamSection({ distribution, toggleTeamModal, isManager }: Props) {\n  const [teams, setTeams] = useState<TeamsState>({\n    content: [],\n    pagination: { current: 1, pageSize: 10 },\n  });\n  const [search, setSearch] = useState<string>('');\n\n  const onSearch = (value: string) => {\n    setSearch(value);\n  };\n\n  const [loading, withLoading] = useLoading(false);\n\n  const getTeams = withLoading(async (pagination: TablePaginationConfig) => {\n    const { data } = await teamApi.getTeams(\n      distribution.courseId,\n      distribution.id,\n      pagination.pageSize ?? 10,\n      pagination.current ?? 1,\n      search,\n    );\n    setTeams({ ...teams, ...data });\n  });\n\n  const columns = useMemo(() => {\n    return isManager\n      ? getColumns(distribution, toggleTeamModal)\n      : getColumns(distribution, toggleTeamModal).filter(col => col.key !== TeamsTableColumnKey.Action);\n  }, [isManager, distribution, toggleTeamModal]);\n\n  const handleChange: TableProps<TeamDto>['onChange'] = pagination => {\n    getTeams(pagination);\n  };\n\n  useAsync(async () => await getTeams(teams.pagination), [distribution, search]);\n\n  return (\n    <Space size={24} direction=\"vertical\" style={{ width: '100%' }}>\n      <Row justify=\"space-between\">\n        <Col span={8}>\n          <Title level={5}>{`${distribution.name} teams`}</Title>\n        </Col>\n        <Col md={6} xl={4}>\n          <Input.Search placeholder=\"input search text\" allowClear onSearch={onSearch} />\n        </Col>\n      </Row>\n      <Table<TeamDto>\n        showHeader\n        pagination={teams.pagination}\n        rowKey=\"id\"\n        onChange={handleChange}\n        dataSource={teams.content}\n        columns={columns}\n        expandable={{ expandedRowRender, rowExpandable: record => record.students.length > 0 }}\n        loading={loading}\n      />\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/Teams/components/TeamsSection/renderers.tsx",
    "content": "import { Button, Space, Typography } from 'antd';\nimport { ColumnsType } from 'antd/lib/table';\nimport { Breakpoint } from 'antd/lib';\nimport { TeamDistributionDetailedDto, TeamDto } from '@client/api';\nimport { StudentsTableColumnKey, TeamsTableColumnKey, TeamsTableColumnName } from '@client/modules/Teams/constants';\nimport StudentsTable from '../StudentsTable/StudentsTable';\n\nconst { Text } = Typography;\n\nfunction renderName(_v: string, { name }: TeamDto) {\n  return <Text>{name}</Text>;\n}\n\nfunction renderDescription(_v: string, { description }: TeamDto) {\n  return <Text type=\"secondary\">{description}</Text>;\n}\n\nfunction renderMemberCount({ students }: TeamDto, membersCount: number) {\n  return (\n    <Text>\n      {students.length} of {membersCount}\n    </Text>\n  );\n}\n\nfunction renderAction(onEditTeam: () => void) {\n  return (\n    <Button\n      type=\"link\"\n      onClick={() => {\n        onEditTeam();\n      }}\n    >\n      Edit team\n    </Button>\n  );\n}\n\nfunction renderTeam(team: TeamDto, distribution: TeamDistributionDetailedDto) {\n  return (\n    <Space direction=\"vertical\" size=\"small\">\n      {renderName('', team)}\n      {renderDescription('', team)}\n      {renderMemberCount(team, distribution.strictTeamSize)}\n    </Space>\n  );\n}\n\nconst DISPLAY_TABLE_BREAKPOINTS: Breakpoint[] = ['sm'];\nconst DISPLAY_TABLE_MOBILE_BREAKPOINT: Breakpoint[] = ['xs'];\n\nexport const getColumns = (\n  distribution: TeamDistributionDetailedDto,\n  toggleTeamModal: (data?: Partial<TeamDto> | undefined) => void,\n): ColumnsType<TeamDto> => [\n  {\n    key: TeamsTableColumnKey.Name,\n    title: TeamsTableColumnName.Name,\n    dataIndex: 'name',\n    render: renderName,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: TeamsTableColumnKey.Description,\n    title: TeamsTableColumnName.Description,\n    dataIndex: 'description',\n    width: 'auto',\n    render: renderDescription,\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: TeamsTableColumnKey.Members,\n    title: TeamsTableColumnName.Members,\n    dataIndex: 'students',\n    render: (_v, t) => renderMemberCount(t, distribution.strictTeamSize),\n    responsive: DISPLAY_TABLE_BREAKPOINTS,\n  },\n  {\n    key: TeamsTableColumnKey.Team,\n    title: TeamsTableColumnName.Team,\n    render: (_v, t) => renderTeam(t, distribution),\n    responsive: DISPLAY_TABLE_MOBILE_BREAKPOINT,\n  },\n  {\n    key: TeamsTableColumnKey.Action,\n    title: TeamsTableColumnName.Action,\n    render: (_v, t) => renderAction(() => toggleTeamModal(t)),\n  },\n];\n\nexport const expandedRowRender = (team: TeamDto) => {\n  return (\n    <StudentsTable\n      content={team.students}\n      teamLeadId={team.teamLeadId}\n      notVisibleColumn={[StudentsTableColumnKey.Email]}\n      pagination={false}\n    />\n  );\n};\n"
  },
  {
    "path": "client/src/modules/Teams/components/index.ts",
    "content": "export { default as TeamsHeader } from './TeamsHeader/TeamsHeader';\nexport { default as TeamModal } from './TeamModal/TeamModal';\nexport { default as JoinTeamModal } from './JoinTeamModal/JoinTeamModal';\nexport { default as TeamsSection } from './TeamsSection/TeamsSection';\nexport { default as StudentsTable } from './StudentsTable/StudentsTable';\nexport { default as StudentsWithoutTeamSection } from './StudentsWithoutTeamSection/StudentsWithoutTeamSection';\nexport { default as MyTeamSection } from './MyTeamSection/MyTeamSection';\n"
  },
  {
    "path": "client/src/modules/Teams/constants.ts",
    "content": "export enum TeamsTableColumnKey {\n  Name = 'name',\n  Description = 'description',\n  Members = 'members',\n  Action = 'action',\n  Team = 'team',\n}\n\nexport enum TeamsTableColumnName {\n  Name = 'Team name',\n  Description = 'Description',\n  Members = 'Members count',\n  Action = 'Action',\n  Team = 'Team',\n}\n\nexport enum StudentsTableColumnKey {\n  Name = 'name',\n  Position = 'rank',\n  Email = 'email',\n  Discord = 'discord',\n  GitHub = 'github',\n  Location = 'location',\n  Contacts = 'contacts',\n  Student = 'student',\n  Action = 'action',\n}\n\nexport enum StudentsTableColumnName {\n  Name = 'Name',\n  Position = 'Position',\n  Email = 'Email',\n  Discord = 'Discord',\n  GitHub = 'GitHub',\n  Location = 'Location',\n  Contacts = 'Contacts',\n  Student = 'Student',\n  Action = 'Action',\n}\n"
  },
  {
    "path": "client/src/modules/Teams/hooks/index.ts",
    "content": "export { useDistribution } from './useDistribution/useDistribution';\n"
  },
  {
    "path": "client/src/modules/Teams/hooks/useDistribution/useDistribution.test.ts",
    "content": "import { act, renderHook } from '@testing-library/react';\nimport { message } from 'antd';\nimport { TeamDistributionApi, TeamDistributionDetailedDto } from '@client/api';\nimport { useDistribution } from './useDistribution';\nimport { AxiosResponse } from 'axios';\n\nvi.mock('antd', () => ({\n  message: {\n    error: vi.fn(),\n  },\n}));\n\nconst mockDistributionData = {\n  data: { id: 1, name: 'initial distribution' },\n} as unknown as TeamDistributionDetailedDto;\n\ndescribe('useDistribution', () => {\n  let courseId: number;\n\n  beforeEach(() => {\n    courseId = 1;\n  });\n\n  it('should set loading to false and update distribution when getCourseTeamDistributionDetailed is successful', async () => {\n    vi.spyOn(TeamDistributionApi.prototype, 'getCourseTeamDistributionDetailed').mockResolvedValue({\n      status: 200,\n      statusText: 'OK',\n      headers: {},\n      config: {},\n      data: { ...mockDistributionData, name: 'new distribution' },\n    } as AxiosResponse<TeamDistributionDetailedDto>);\n\n    const { result } = renderHook(() => useDistribution(courseId, mockDistributionData.id));\n\n    await act(async () => {\n      await result.current.loadDistribution();\n    });\n\n    expect(result.current.loading).toBe(false);\n    expect(result.current.distribution?.name).toEqual('new distribution');\n  });\n\n  it('should set loading to false and call message.error when getCourseTeamDistributionDetailed fails', async () => {\n    vi.spyOn(TeamDistributionApi.prototype, 'getCourseTeamDistributionDetailed').mockRejectedValueOnce(null);\n    const { result } = renderHook(() => useDistribution(courseId, mockDistributionData.id));\n\n    await act(async () => {\n      await result.current.loadDistribution();\n    });\n\n    expect(result.current.loading).toBe(false);\n    expect(message.error).toHaveBeenCalledWith('Something went wrong, please try reloading the page later');\n  });\n});\n"
  },
  {
    "path": "client/src/modules/Teams/hooks/useDistribution/useDistribution.ts",
    "content": "import { useRequest } from 'ahooks';\nimport { message } from 'antd';\nimport { TeamDistributionApi } from '@client/api';\n\nconst teamDistributionApi = new TeamDistributionApi();\n\nexport function useDistribution(courseId: number, teamDistributionId: number) {\n  const { data, loading, runAsync } = useRequest(async () => {\n    try {\n      const { data } = await teamDistributionApi.getCourseTeamDistributionDetailed(courseId, teamDistributionId);\n      return data;\n    } catch {\n      message.error('Something went wrong, please try reloading the page later');\n    }\n  });\n\n  return {\n    loading,\n    distribution: data,\n    loadDistribution: runAsync,\n  };\n}\n"
  },
  {
    "path": "client/src/modules/Teams/index.tsx",
    "content": "export { default as Teams } from './Pages/Teams';\n"
  },
  {
    "path": "client/src/modules/UserGroupsAdmin/components/UserGroupsModal.tsx",
    "content": "import { Col, Form, Input, Row, Select } from 'antd';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { UserGroupDto, UpdateUserGroupDto } from '@client/api';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { CourseRole } from '@client/services/models';\n\ntype Props = {\n  data: Partial<UserGroupDto> | null;\n  title: string;\n  submit: (values: UpdateUserGroupDto) => Promise<void>;\n  cancel: () => void;\n  getInitialValues: (data: Partial<UserGroupDto>) => any;\n  loading: boolean;\n  loadUsers: (searchText: string) => Promise<any>;\n};\n\nconst roles = [CourseRole.Manager, CourseRole.Supervisor, CourseRole.Dementor];\n\nexport function UserGroupsModal({ data, title, submit, cancel, getInitialValues, loading, loadUsers }: Props) {\n  return data ? (\n    <ModalForm\n      data={data}\n      title={title}\n      submit={submit}\n      cancel={cancel}\n      getInitialValues={getInitialValues}\n      loading={loading}\n    >\n      <Row gutter={24}>\n        <Col span={24}>\n          <Form.Item name=\"name\" label=\"Name\" rules={[{ required: true, message: 'Please enter user group name' }]}>\n            <Input />\n          </Form.Item>\n        </Col>\n        <Col span={24}>\n          <Form.Item name=\"users\" label=\"Users\" rules={[{ required: true, message: 'Please select users' }]}>\n            <UserSearch mode=\"multiple\" searchFn={loadUsers} defaultValues={data?.users} />\n          </Form.Item>\n        </Col>\n        <Col span={24}>\n          <Form.Item name=\"roles\" label=\"Roles\" rules={[{ required: true, message: 'Please select permissions' }]}>\n            <Select mode=\"tags\">\n              {roles.map(role => (\n                <Select.Option key={role} value={role}>\n                  {role}\n                </Select.Option>\n              ))}\n            </Select>\n          </Form.Item>\n        </Col>\n      </Row>\n    </ModalForm>\n  ) : null;\n}\n"
  },
  {
    "path": "client/src/modules/UserGroupsAdmin/components/UserGroupsTable.tsx",
    "content": "import { Table, Tag, Typography } from 'antd';\nimport { stringSorter } from '@client/shared/components/Table';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { UserGroupDto } from '@client/api';\nimport { CustomPopconfirm } from '@client/components/common/CustomPopconfirm';\n\ntype Props = {\n  data: UserGroupDto[];\n  onEdit: (record: UserGroupDto) => void;\n  onDelete: (id: number) => void;\n};\n\nconst rolesColors: Record<string, string> = {\n  supervisor: 'purple',\n  manager: 'volcano',\n};\n\nexport function UserGroupsTable({ data, onEdit, onDelete }: Props) {\n  return (\n    <Table\n      size=\"small\"\n      style={{ marginTop: 8 }}\n      dataSource={data}\n      pagination={{ pageSize: 100 }}\n      rowKey=\"id\"\n      columns={getColumns(onEdit, onDelete)}\n    />\n  );\n}\n\nfunction getColumns(handleEditItem: (record: UserGroupDto) => void, handleDeleteItem: (id: number) => void) {\n  return [\n    {\n      title: 'Id',\n      dataIndex: 'id',\n    },\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      sorter: stringSorter<UserGroupDto>('name'),\n    },\n    {\n      title: 'Users',\n      dataIndex: 'users',\n      render: (_: any, record: UserGroupDto) => (\n        <div>\n          {record.users.map((user, index, array) => (\n            <div key={user.id} style={{ display: 'flex', marginBottom: index < array.length - 1 ? 10 : 0 }}>\n              <GithubAvatar size={24} githubId={user.githubId} />\n              &nbsp;{user.name} ({user.githubId})\n            </div>\n          ))}\n        </div>\n      ),\n    },\n    {\n      title: 'Roles',\n      dataIndex: 'roles',\n      render: (_: any, record: UserGroupDto) => (\n        <div>\n          {record.roles.map(role => (\n            <Tag color={rolesColors[role]} key={role}>\n              {role}\n            </Tag>\n          ))}\n        </div>\n      ),\n    },\n    {\n      title: 'Actions',\n      dataIndex: 'actions',\n      render: (_: any, record: UserGroupDto) => (\n        <>\n          <span>\n            <Typography.Link onClick={() => handleEditItem(record)}>Edit</Typography.Link>{' '}\n          </span>\n          <span style={{ marginLeft: 8 }}>\n            <CustomPopconfirm\n              onConfirm={() => handleDeleteItem(record.id)}\n              title=\"Are you sure you want to delete this item?\"\n            >\n              <Typography.Link>Delete</Typography.Link>\n            </CustomPopconfirm>\n          </span>\n        </>\n      ),\n    },\n  ];\n}\n"
  },
  {
    "path": "client/src/modules/UserGroupsAdmin/hooks/index.ts",
    "content": "export * from './useUserGroups';\n"
  },
  {
    "path": "client/src/modules/UserGroupsAdmin/hooks/useUserGroups.ts",
    "content": "import { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { UserGroupApi, UpdateUserGroupDto, UserGroupDto } from '@client/api';\nimport { UserService } from '@client/services/user';\n\nconst userGroupService = new UserGroupApi();\nconst userService = new UserService();\n\nexport function useUserGroups() {\n  const [data, setData] = useState<UserGroupDto[]>([]);\n\n  const loadData = async () => {\n    const { data } = await userGroupService.getUserGroups();\n    setData(data);\n  };\n\n  const { loading } = useAsync(loadData, []);\n\n  const loadUsers = async (searchText: string) => {\n    return userService.searchUser(searchText);\n  };\n\n  const createUserGroup = async (values: UpdateUserGroupDto) => {\n    await userGroupService.createUserGroup(values);\n    await loadData();\n  };\n\n  const updateUserGroup = async (id: number, values: UpdateUserGroupDto) => {\n    await userGroupService.updateUserGroup(id, values);\n    await loadData();\n  };\n\n  const deleteUserGroup = async (id: number) => {\n    await userGroupService.deleteUserGroup(id);\n    await loadData();\n  };\n\n  return { data, loading, loadUsers, createUserGroup, updateUserGroup, deleteUserGroup };\n}\n"
  },
  {
    "path": "client/src/modules/UserGroupsAdmin/index.ts",
    "content": "export { UserGroupsAdminPage } from './pages/UserGroupsAdminPage';\nexport { useUserGroups } from './hooks/useUserGroups';\nexport { UserGroupsTable } from './components/UserGroupsTable';\nexport { UserGroupsModal } from './components/UserGroupsModal';\n"
  },
  {
    "path": "client/src/modules/UserGroupsAdmin/pages/UserGroupsAdminPage/UserGroupsAdminPage.tsx",
    "content": "import { Button, Layout, message } from 'antd';\nimport { useCallback, useState } from 'react';\nimport { UserGroupDto, UpdateUserGroupDto } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { UserGroupsTable } from '../../components/UserGroupsTable';\nimport { UserGroupsModal } from '../../components/UserGroupsModal';\nimport { useUserGroups } from '../../hooks/useUserGroups';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\nconst { Content } = Layout;\n\nenum ModalAction {\n  update = 'update',\n  create = 'create',\n}\n\nexport function UserGroupsAdminPage() {\n  const { courses } = useActiveCourseContext();\n  const { data, loading, loadUsers, createUserGroup, updateUserGroup, deleteUserGroup } = useUserGroups();\n  const [modalData, setModalData] = useState<Partial<UserGroupDto> | null>(null);\n  const [modalAction, setModalAction] = useState(ModalAction.update);\n  const [modalLoading, setModalLoading] = useState(false);\n\n  const handleAddItem = () => {\n    setModalData({});\n    setModalAction(ModalAction.create);\n  };\n\n  const handleEditItem = (record: UserGroupDto) => {\n    setModalData(record);\n    setModalAction(ModalAction.update);\n  };\n\n  const handleDeleteItem = async (id: number) => {\n    try {\n      await deleteUserGroup(id);\n    } catch {\n      message.error('Failed to delete user group. Please try later.');\n    }\n  };\n\n  const handleModalSubmit = useCallback(\n    async (values: UpdateUserGroupDto) => {\n      try {\n        if (modalLoading) {\n          return;\n        }\n        setModalLoading(true);\n        if (modalAction === ModalAction.update) {\n          await updateUserGroup(modalData!.id!, values);\n        } else {\n          await createUserGroup(values);\n        }\n        setModalData(null);\n      } catch {\n        message.error('An error occurred. Cannot save user group.');\n      } finally {\n        setModalLoading(false);\n      }\n    },\n    [modalAction, modalData, modalLoading, createUserGroup, updateUserGroup],\n  );\n\n  function getInitialValues(modalData: Partial<UserGroupDto>) {\n    return {\n      ...modalData,\n      users: modalData.users?.map(user => user.id) ?? [],\n    };\n  }\n\n  return (\n    <AdminPageLayout title=\"Manage User Groups\" loading={loading} courses={courses}>\n      <Content style={{ margin: 8 }}>\n        <Button type=\"primary\" onClick={handleAddItem}>\n          Add User Group\n        </Button>\n        <UserGroupsTable data={data} onEdit={handleEditItem} onDelete={handleDeleteItem} />\n      </Content>\n      <UserGroupsModal\n        data={modalData}\n        title=\"User Group\"\n        submit={handleModalSubmit}\n        cancel={() => setModalData(null)}\n        getInitialValues={getInitialValues}\n        loading={modalLoading}\n        loadUsers={loadUsers}\n      />\n    </AdminPageLayout>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/UserGroupsAdmin/pages/UserGroupsAdminPage/index.ts",
    "content": "export { UserGroupsAdminPage } from './UserGroupsAdminPage';\n"
  },
  {
    "path": "client/src/modules/UsersAdmin/hooks/index.ts",
    "content": "export * from './useUsersSearch';\n"
  },
  {
    "path": "client/src/modules/UsersAdmin/hooks/useUsersSearch.ts",
    "content": "import { useState } from 'react';\nimport { UsersApi, UserSearchDto } from '@client/api';\n\nconst userApi = new UsersApi();\n\nexport function useUsersSearch() {\n  const [users, setUsers] = useState<UserSearchDto[] | null>(null);\n\n  const searchUsers = async (searchText: string) => {\n    if (!searchText) return;\n    const users = await userApi.searchUsers(searchText);\n    setUsers(users.data);\n  };\n\n  return { users, searchUsers };\n}\n"
  },
  {
    "path": "client/src/modules/UsersAdmin/index.ts",
    "content": "export { UsersAdminPage } from './pages/UsersAdminPage';\nexport { useUsersSearch } from './hooks/useUsersSearch';\n"
  },
  {
    "path": "client/src/modules/UsersAdmin/pages/UsersAdminPage/UsersAdminPage.tsx",
    "content": "import { Button, Col, Input, List, Row, Layout, Form } from 'antd';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useUsersSearch } from '../../hooks/useUsersSearch';\nimport { UserSearchDto } from '@client/api';\n\nconst { Content } = Layout;\n\nexport function UsersAdminPage() {\n  const { courses } = useActiveCourseContext();\n  const { users, searchUsers } = useUsersSearch();\n\n  return (\n    <AdminPageLayout title=\"Users\" loading={false} courses={courses}>\n      <Content>\n        <div className=\"mt-4\">\n          <Form layout=\"horizontal\" onFinish={values => searchUsers(values.searchText)}>\n            <Row gutter={12}>\n              <Col span={6}>\n                <Form.Item name=\"searchText\">\n                  <Input allowClear={true} type=\"search\" width={200} placeholder=\"Search by github or name\" />\n                </Form.Item>\n              </Col>\n              <Col span={6}>\n                <Form.Item>\n                  <Button type=\"primary\" htmlType=\"submit\">\n                    Search\n                  </Button>\n                </Form.Item>\n              </Col>\n            </Row>\n          </Form>\n          {users && (\n            <Row>\n              <Col offset={2} xs={20} sm={16} md={10} lg={8}>\n                <List\n                  rowKey=\"id\"\n                  locale={{ emptyText: 'No results' }}\n                  dataSource={users}\n                  renderItem={(user: UserSearchDto) => (\n                    <List.Item>\n                      <List.Item.Meta\n                        avatar={<GithubAvatar size={48} githubId={user.githubId} />}\n                        title={<a href={`/profile?githubId=${user.githubId}`}>{user.githubId}</a>}\n                        description={\n                          <>\n                            <UserField value={user.name} />\n                            <UserField label=\"Primary Email\" value={user.primaryEmail} />\n                            <UserField label=\"Contacts Email\" value={user.contactsEmail} />\n                            <UserField label=\"Contacts EPAM Email\" value={user.contactsEpamEmail} />\n                            <UserField label=\"Contacts Telegram\" value={user.contactsTelegram} />\n                            <UserField label=\"Contacts Discord\" value={user.contactsDiscord} />\n                            <UserField label=\"Mentor\" value={user.mentors?.map(({ courseName }: any) => courseName)} />\n                            <UserField\n                              label=\"Student\"\n                              value={user.students?.map(({ courseName }: any) => courseName)}\n                            />\n                          </>\n                        }\n                      />\n                    </List.Item>\n                  )}\n                />\n              </Col>\n            </Row>\n          )}\n        </div>\n      </Content>\n    </AdminPageLayout>\n  );\n}\n\nfunction UserField({ label, value }: { label?: string; value: string | string[] | null | undefined }) {\n  const valueStr = Array.isArray(value) ? value.join(', ') : value;\n  if (!valueStr) return null;\n  return (\n    <div>\n      {label ? <span>{label}: </span> : null}\n      <span>{valueStr}</span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/modules/UsersAdmin/pages/UsersAdminPage/index.ts",
    "content": "export { UsersAdminPage } from './UsersAdminPage';\n"
  },
  {
    "path": "client/src/pages/404.tsx",
    "content": "import Image from 'next/image';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { Row } from 'antd';\nimport { SessionProvider } from '@client/modules/Course/contexts';\n\nfunction NotFoundPage() {\n  return (\n    <PageLayout loading={false}>\n      <Row justify=\"center\" style={{ margin: '65px 0 25px 0' }}>\n        <Image src=\"/static/svg/err.svg\" alt=\"Error 404\" width={175} height={175} />\n      </Row>\n      <Row justify=\"center\">\n        <h1 style={{ fontSize: '102px', marginBottom: 0 }}>404</h1>\n      </Row>\n      <Row justify=\"center\">\n        <h2>Sorry, Page Not Found</h2>\n      </Row>\n    </PageLayout>\n  );\n}\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <NotFoundPage />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/_app.tsx",
    "content": "import App from 'next/app';\nimport Head from 'next/head';\n\nimport { ActiveCourseProvider } from '@client/modules/Course/contexts';\nimport 'antd/dist/reset.css';\nimport { initializeFeatures } from '@client/services/features';\nimport { Analytics } from '../components/Analytics';\nimport '../styles/main.css';\nimport { DevToolsProvider, MessageProvider, ThemeProvider } from '@client/providers';\n\nclass RsSchoolApp extends App {\n  render() {\n    const { Component, pageProps, router } = this.props;\n    initializeFeatures(router.query);\n\n    return (\n      <>\n        <Analytics />\n        <Head>\n          <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" />\n          <title>App / The Rolling Scopes School</title>\n        </Head>\n        <ThemeProvider>\n          <MessageProvider>\n            <DevToolsProvider>\n              <ActiveCourseProvider\n                publicRoutes={[\n                  '/login',\n                  '/registry/mentor',\n                  '/registry/student',\n                  '/course/mentor/confirm',\n                  '/mentors-hall-of-fame',\n                ]}\n              >\n                <Component {...pageProps} />\n              </ActiveCourseProvider>\n            </DevToolsProvider>\n          </MessageProvider>\n        </ThemeProvider>\n      </>\n    );\n  }\n}\n\nexport default RsSchoolApp;\n"
  },
  {
    "path": "client/src/pages/_document.tsx",
    "content": "import Document, { Head, Html, Main, NextScript } from 'next/document';\n\nclass AppDocument extends Document {\n  render() {\n    return (\n      <Html lang=\"en\">\n        <Head>\n          <link rel=\"shortcut icon\" href=\"/static/images/favicon.ico\" />\n        </Head>\n        <body>\n          <Main />\n          <NextScript />\n        </body>\n      </Html>\n    );\n  }\n}\n\nexport default AppDocument;\n"
  },
  {
    "path": "client/src/pages/admin/auto-test-task/[taskId].tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { Descriptions, Divider, Form, Space, Switch, Tag, Typography } from 'antd';\nimport { AutoTestsApi, SelfEducationQuestionSelectedAnswersDto } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { Question } from '@client/modules/AutoTest/components';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useRouter } from 'next/router';\nimport { useEffect } from 'react';\nimport { CourseRole } from '@client/services/models';\n\nconst api = new AutoTestsApi();\n\nfunction Page() {\n  const { courses } = useActiveCourseContext();\n  const router = useRouter();\n\n  const { data: selectedTask } = useRequest(async () => {\n    const taskId = Number(router.query.taskId as string);\n    if (Number.isNaN(taskId)) {\n      return null;\n    }\n    const { data } = await api.getAutoTest(Number(router.query.taskId as string));\n    return data;\n  });\n\n  useEffect(() => {\n    if (selectedTask === null) {\n      router.push('/404');\n    }\n  }, [selectedTask]);\n\n  return (\n    <AdminPageLayout title=\"Auto test task\" courses={courses} loading={false}>\n      <Descriptions column={1}>\n        <Descriptions.Item label=\"Name\">{selectedTask?.name}</Descriptions.Item>\n        {selectedTask?.descriptionUrl && (\n          <Descriptions.Item label=\"Description URL\">\n            <a href={selectedTask?.descriptionUrl} target=\"_blank\">\n              {selectedTask?.descriptionUrl}\n            </a>\n          </Descriptions.Item>\n        )}\n        {selectedTask?.discipline?.name && (\n          <Descriptions.Item label=\"Discipline\">{selectedTask?.discipline?.name}</Descriptions.Item>\n        )}\n        {selectedTask?.courses?.length && selectedTask?.courses?.length > 0 && (\n          <Descriptions.Item label=\"Courses\">\n            <Space wrap size=\"small\">\n              {selectedTask.courses.map(course => (\n                <Tag color={course?.isActive ? 'green' : 'red'} key={course.name}>\n                  <Typography.Text>{course.name}</Typography.Text>\n                </Tag>\n              ))}\n            </Space>\n          </Descriptions.Item>\n        )}\n\n        {selectedTask?.tags && (\n          <Descriptions.Item label=\"Tags\">\n            {selectedTask?.tags.map(tag => (\n              <Tag color=\"blue\" key={tag}>\n                {tag}\n              </Tag>\n            ))}\n          </Descriptions.Item>\n        )}\n      </Descriptions>\n\n      {selectedTask?.attributes?.public?.questions && (\n        <>\n          <Divider />\n          <Descriptions\n            title=\"Test Settings\"\n            bordered\n            size=\"small\"\n            column={{\n              xs: 1,\n              md: 2,\n              lg: 3,\n              xxl: 5,\n            }}\n          >\n            <Descriptions.Item label=\"Max Attempts Number\">\n              {selectedTask?.attributes?.public?.maxAttemptsNumber}\n            </Descriptions.Item>\n            <Descriptions.Item label=\"Number of Questions\">\n              {selectedTask?.attributes?.public?.numberOfQuestions}\n            </Descriptions.Item>\n            <Descriptions.Item label=\"Total Questions\">\n              {selectedTask?.attributes?.public?.questions?.length}\n            </Descriptions.Item>\n            <Descriptions.Item label=\"Strict Attempts Mode\">\n              <Switch checked={selectedTask?.attributes?.public?.strictAttemptsMode} disabled />\n            </Descriptions.Item>\n            <Descriptions.Item label=\"Threshold Percentage\">\n              {selectedTask?.attributes?.public?.tresholdPercentage}\n            </Descriptions.Item>\n          </Descriptions>\n          <Divider style={{ border: 'none', margin: '16px 0' }} />\n          <Form layout=\"vertical\" requiredMark={false} disabled={true}>\n            {selectedTask?.attributes.public.questions.map((question, index) => (\n              <Question\n                key={index}\n                question={\n                  {\n                    ...question,\n                    // TODO: Investigate and fix potential type mismatch for selectedAnswers.\n                    // Related issue: https://github.com/rolling-scopes/rsschool-app/issues/2572\n                    selectedAnswers: selectedTask?.attributes.answers[index],\n                  } as SelfEducationQuestionSelectedAnswersDto\n                }\n              />\n            ))}\n          </Form>\n        </>\n      )}\n    </AdminPageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/auto-test.tsx",
    "content": "import { Col, ColProps, Row, message } from 'antd';\nimport { AutoTestsApi, BasicAutoTestTaskDto } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport AutoTestTaskCard from '@client/modules/AutoTest/components/AutoTestTaskCard/AutoTestTaskCard';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseRole } from '@client/services/models';\n\nconst RESPONSIVE_COLUMNS: ColProps = {\n  sm: 24,\n  md: 12,\n  lg: 8,\n  xl: 8,\n  xxl: 6,\n};\n\nconst api = new AutoTestsApi();\n\nfunction Page() {\n  const { courses } = useActiveCourseContext();\n  const [tests, setTests] = useState<BasicAutoTestTaskDto[]>([]);\n  const [isLoading, setIsLoading] = useState(false);\n\n  useAsync(async () => {\n    try {\n      setIsLoading(true);\n      const resp = await api.getBasicAutoTests();\n      setTests(resp.data);\n      setIsLoading(false);\n    } catch {\n      message.error('Something went wrong. Please try again later.');\n    }\n  });\n\n  return (\n    <AdminPageLayout title=\"Auto test tasks\" loading={isLoading} courses={courses}>\n      <Row gutter={[24, 24]} style={{ padding: '0 16px', marginRight: 0 }}>\n        {tests.map(courseTask => (\n          <Col {...RESPONSIVE_COLUMNS} key={courseTask.id}>\n            <AutoTestTaskCard courseTask={courseTask} />\n          </Col>\n        ))}\n      </Row>\n    </AdminPageLayout>\n  );\n}\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/contributors.tsx",
    "content": "import { ContributorPage } from '@client/modules/Contributor/pages/ContributorPage';\nimport { SessionProvider } from '@client/modules/Course/contexts';\n\nexport default function () {\n  return (\n    <SessionProvider adminOnly>\n      <ContributorPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/courses.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { Button, Layout, Table } from 'antd';\nimport { CoursesApi, DisciplinesApi, DiscordServersApi } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { boolIconRenderer, dateUtcRenderer, stringSorter, stringTrimRenderer } from '@client/shared/components/Table';\nimport { DEFAULT_COURSE_ICONS } from '@client/configs/course-icons';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport { isCourseManager } from '@client/domain/user';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CourseModal } from '@client/modules/CourseManagement/components/CourseModal';\nimport { useContext, useState } from 'react';\nimport { Course, CourseRole } from '@client/services/models';\nimport { PublicSvgIcon } from '@client/shared/components/Icons';\n\ndayjs.extend(utc);\n\nconst disciplinesApi = new DisciplinesApi();\nconst discordServersService = new DiscordServersApi();\nconst courseApi = new CoursesApi();\n\nfunction Page() {\n  const session = useContext(SessionContext);\n\n  const { courses: allCourses } = useActiveCourseContext();\n  const [modalId, setModalId] = useState<number | null | undefined>();\n\n  const response = useRequest(async () => {\n    const [{ data: courses }, { data: discordServers }, { data: disciplines }] = await Promise.all([\n      courseApi.getCourses(),\n      discordServersService.getReducedDiscordServers(),\n      disciplinesApi.getDisciplines(),\n    ]);\n    return {\n      courses: courses.filter(course => isCourseManager(session, course.id)),\n      discordServers,\n      disciplines,\n    };\n  });\n\n  return (\n    <AdminPageLayout title=\"Manage Courses\" loading={response.loading} courses={allCourses}>\n      <Layout.Content style={{ margin: 8 }}>\n        <Button type=\"primary\" onClick={() => setModalId(null)}>\n          Add Course\n        </Button>\n        <Table\n          size=\"small\"\n          style={{ marginTop: 8 }}\n          dataSource={response.data?.courses ?? []}\n          pagination={{ pageSize: 100 }}\n          rowKey=\"id\"\n          columns={getColumns((record: Course) => setModalId(record.id))}\n        />\n      </Layout.Content>\n      {modalId !== undefined ? (\n        <CourseModal\n          onClose={() => setModalId(undefined)}\n          discordServers={response.data?.discordServers ?? []}\n          disciplines={response.data?.disciplines ?? []}\n          courses={allCourses}\n          courseId={modalId}\n        />\n      ) : null}\n    </AdminPageLayout>\n  );\n}\n\nfunction getColumns(handleEditItem: any) {\n  return [\n    {\n      title: 'Id',\n      dataIndex: 'id',\n    },\n    {\n      title: 'Logo',\n      dataIndex: 'logo',\n      render: (logo: string) => <PublicSvgIcon size=\"25px\" src={DEFAULT_COURSE_ICONS[logo]?.active} />,\n    },\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      sorter: stringSorter<Course>('name'),\n    },\n    {\n      title: 'Full Name',\n      dataIndex: 'fullName',\n      sorter: stringSorter<Course>('fullName'),\n      width: 200,\n    },\n    {\n      title: 'Alias',\n      dataIndex: 'alias',\n      sorter: stringSorter<Course>('alias'),\n    },\n    {\n      title: 'Description',\n      dataIndex: 'description',\n      render: stringTrimRenderer,\n    },\n    {\n      title: 'Start Date',\n      dataIndex: 'startDate',\n      render: dateUtcRenderer,\n      width: 120,\n    },\n    {\n      title: 'End Date',\n      dataIndex: 'endDate',\n      render: dateUtcRenderer,\n    },\n    {\n      title: 'Discipline',\n      dataIndex: ['discipline', 'name'],\n    },\n    {\n      title: 'Completed',\n      dataIndex: 'completed',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Planned',\n      dataIndex: 'planned',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Invite Only',\n      dataIndex: 'inviteOnly',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Actions',\n      dataIndex: 'actions',\n      render: (_: any, record: any) => <a onClick={() => handleEditItem(record)}>Edit</a>,\n    },\n  ];\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]} anyCoursePowerUser>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/disciplines.tsx",
    "content": "import { DisciplinePage } from '@client/modules/Discipline/pages/DisciplinePage';\nimport { SessionProvider } from '@client/modules/Course/contexts';\n\nexport default function () {\n  return (\n    <SessionProvider adminOnly>\n      <DisciplinePage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/discord-telegram.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { DiscordAdminPage } from '@client/modules/DiscordAdmin';\n\nexport default function () {\n  return (\n    <SessionProvider adminOnly>\n      <DiscordAdminPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/events.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\nimport { EventsAdminPage } from '@client/modules/EventsAdmin';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]} anyCoursePowerUser>\n      <EventsAdminPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/mentor-registry.module.css",
    "content": ".tabs :global(.custom-mentor-registry-tabs .ant-tabs-tab) {\n  min-width: 100px;\n}\n\n@media (min-width: 575px) {\n  .tabs {\n    padding: 12px 24px 0;\n    height: 64px;\n  }\n}\n"
  },
  {
    "path": "client/src/pages/admin/mentor-registry.tsx",
    "content": "import FileExcelOutlined from '@ant-design/icons/FileExcelOutlined';\nimport { Alert, Button, Col, Form, message, notification, Row, Select, Space, Tabs, Tooltip, Typography } from 'antd';\nimport { useCallback, useContext, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\n\nimport { DisciplineDto, DisciplinesApi, MentorRegistryDto } from '@client/api';\n\nimport { CommentModal } from '@client/shared/components/CommentModal';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { tabRenderer } from '@client/components/TabsWithCounter/renderers';\nimport { useLoading } from '@client/components/useLoading';\nimport { SessionContext, SessionProvider } from '@client/modules/Course/contexts';\nimport {\n  CombinedFilter,\n  MentorRegistryDeleteModal,\n  MentorRegistryResendModal,\n  MentorRegistryTable,\n  MentorRegistryTableContainer,\n  MentorRegistryTabsMode,\n  PAGINATION,\n} from '@client/modules/MentorRegistry';\nimport dynamic from 'next/dynamic';\nimport { CoursesService } from '@client/services/courses';\nimport { MentorRegistryService } from '@client/services/mentorRegistry';\nimport { Course, CourseRole } from '@client/services/models';\nimport styles from './mentor-registry.module.css';\n\nconst InviteMentorsModal = dynamic(() => import('@client/modules/MentorRegistry/components/InviteMentorsModal'), {\n  ssr: false,\n});\n\ntype NotificationType = 'success' | 'info' | 'warning' | 'error';\n\nexport enum ModalDataMode {\n  Invite = 'invite',\n  Resend = 'resend',\n  Delete = 'delete',\n  Comment = 'comment',\n  BatchInvite = 'batchInvite',\n}\n\ntype ModalData = Partial<{\n  record: MentorRegistryDto;\n  mode: ModalDataMode;\n}>;\n\ntype FormData = {\n  preselectedCourses: number[];\n};\n\nconst mentorRegistryService = new MentorRegistryService();\nconst coursesService = new CoursesService();\nconst disciplinesApi = new DisciplinesApi();\n\nfunction Page() {\n  const [loading, withLoading] = useLoading(false);\n  const session = useContext(SessionContext);\n\n  const [api, contextHolder] = notification.useNotification();\n\n  const [courses, setCourses] = useState<Course[]>([]);\n  const [modalLoading, setModalLoading] = useState(false);\n  const [data, setData] = useState<MentorRegistryDto[]>([]);\n  const [allData, setAllData] = useState<MentorRegistryDto[]>([]);\n  const [maxStudents, setMaxStudents] = useState(0);\n  const [modalData, setModalData] = useState<ModalData | null>(null);\n  const [activeTab, setActiveTab] = useState<MentorRegistryTabsMode>(MentorRegistryTabsMode.New);\n  const [disciplines, setDisciplines] = useState<DisciplineDto[]>([]);\n  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);\n  const [tagFilters, setTagFilters] = useState<string[]>([]);\n  const [combinedFilter, setCombinedFilter] = useState<CombinedFilter>({\n    preferredCourses: [],\n    preselectedCourses: [],\n    technicalMentoring: [],\n    githubId: [],\n    cityName: [],\n    status: MentorRegistryTabsMode.New,\n  });\n  const [currentPage, setCurrentPage] = useState(1);\n  const [total, setTotal] = useState({\n    [MentorRegistryTabsMode.New]: 0,\n    [MentorRegistryTabsMode.All]: 0,\n  });\n\n  const loadData = withLoading(async () => {\n    const [allData, courses] = await Promise.all([\n      mentorRegistryService.getMentors({\n        status: activeTab,\n        pageSize: PAGINATION,\n        currentPage,\n        githubId: combinedFilter.githubId?.[0] ?? undefined,\n        cityName: combinedFilter.cityName?.[0] ?? undefined,\n        preferedCourses: combinedFilter.preferredCourses?.length\n          ? combinedFilter.preferredCourses.map(Number)\n          : undefined,\n        preselectedCourses: combinedFilter.preselectedCourses?.length\n          ? combinedFilter.preselectedCourses.map(Number)\n          : undefined,\n        technicalMentoring: combinedFilter.technicalMentoring?.length ? combinedFilter.technicalMentoring : undefined,\n      }),\n      coursesService.getCourses(),\n    ]);\n    const { data: disciplines } = await disciplinesApi.getDisciplines();\n    setAllData(allData.mentors);\n    setData(allData.mentors);\n    setTotal(total => ({ ...total, [activeTab]: allData.total }));\n    setMaxStudents(allData.mentors.reduce((sum, it) => sum + it.maxStudentsLimit, 0));\n    setCourses(courses);\n    setDisciplines(disciplines);\n  });\n\n  const cancelMentor = withLoading(async (githubId: string) => {\n    setModalData(null);\n    await mentorRegistryService.cancelMentorRegistry(githubId);\n    await loadData();\n    setIsModalOpen(false);\n  });\n\n  const sendMentorRegistryComment = withLoading(async (comment: string) => {\n    if (!modalData?.record?.githubId) return;\n    try {\n      await mentorRegistryService.sendCommentMentorRegistry(modalData?.record?.githubId, comment);\n      await loadData();\n    } catch {\n      message.error('An error occurred. Please try again later.');\n    } finally {\n      setIsModalOpen(false);\n    }\n  });\n\n  useAsync(loadData, [combinedFilter, currentPage, activeTab]);\n\n  const openNotificationWithIcon = (type: NotificationType) => {\n    api[type]({\n      message: 'Success',\n      description: 'Your invitation was successfully send',\n    });\n  };\n\n  const handleModalSubmit = useCallback(\n    async (values: FormData) => {\n      const originalSortedData = modalData?.record?.preselectedCourses.map(courseId => String(courseId)).sort();\n      const updatedPreselectedCourses = values.preselectedCourses.map(courseId => String(courseId));\n      const updatedSortedData = updatedPreselectedCourses.sort();\n\n      const isSame = JSON.stringify(originalSortedData) === JSON.stringify(updatedSortedData);\n\n      if (isSame) {\n        setModalData(null);\n        return;\n      }\n\n      try {\n        setModalLoading(true);\n        if (modalData?.record?.githubId) {\n          await mentorRegistryService.updateMentor(modalData.record.githubId, {\n            preselectedCourses: updatedPreselectedCourses,\n          });\n        }\n        setModalData(null);\n        await loadData();\n        openNotificationWithIcon('success');\n      } catch {\n        message.error('An error occurred. Please try again later.');\n      } finally {\n        setModalLoading(false);\n      }\n    },\n    [modalData, openNotificationWithIcon, loadData],\n  );\n\n  const renderModal = useCallback(() => {\n    const data = modalData?.record;\n    if (!data) {\n      return null;\n    }\n\n    const allShownCourses = courses.filter(course => {\n      const isCompletedAndPreselected = course.completed && data.preselectedCourses.includes(course.id);\n      const isActiveWithPersonalMentoring = !course.completed && course.personalMentoring;\n      return isCompletedAndPreselected || isActiveWithPersonalMentoring;\n    });\n\n    return (\n      <ModalForm\n        data={data}\n        title=\"Record\"\n        submit={handleModalSubmit}\n        cancel={() => setModalData(null)}\n        loading={modalLoading}\n      >\n        <Form.Item name=\"preselectedCourses\" label=\"Pre-Selected Courses\">\n          <Select mode=\"multiple\" optionFilterProp=\"children\">\n            {allShownCourses.map(course => (\n              <Select.Option key={course.id} value={course.id}>\n                {course.completed ? (\n                  <Tooltip title=\"Completed course\">\n                    <span style={{ color: 'red' }}>{course.name}</span>\n                  </Tooltip>\n                ) : (\n                  course.name\n                )}\n              </Select.Option>\n            ))}\n          </Select>\n        </Form.Item>\n      </ModalForm>\n    );\n  }, [modalData]);\n\n  async function resendConfirmation(record: MentorRegistryDto) {\n    try {\n      setModalLoading(true);\n      setModalData(null);\n      await mentorRegistryService.updateMentor(record!.githubId, {\n        preselectedCourses: record.preselectedCourses.map(v => String(v)),\n      });\n      loadData();\n    } catch {\n      message.error('An error occurred. Please try again later.');\n    } finally {\n      setModalLoading(false);\n      setModalData(null);\n    }\n  }\n\n  const tabs = useMemo(() => {\n    const tabs = [\n      { key: MentorRegistryTabsMode.New, label: 'New applications', count: Number(total[MentorRegistryTabsMode.New]) },\n      { key: MentorRegistryTabsMode.All, label: 'All Mentors', count: Number(total[MentorRegistryTabsMode.All]) },\n    ];\n    return tabs.map(el => tabRenderer(el, activeTab));\n  }, [activeTab, allData]);\n\n  const handleTabChange = useCallback((key: string) => {\n    setActiveTab(key as MentorRegistryTabsMode);\n    setCurrentPage(1);\n  }, []);\n\n  const handleModalDataChange = (mode: ModalDataMode, record: MentorRegistryDto) => {\n    setIsModalOpen(true);\n    setModalData({ mode, record });\n  };\n\n  const onCancelModal = () => {\n    setModalData(null);\n    setIsModalOpen(false);\n  };\n\n  return (\n    <AdminPageLayout title=\"Mentor Registry\" loading={loading} courses={courses} styles={{ margin: 0, padding: 0 }}>\n      <Row justify=\"space-between\" style={{ padding: '0 24px', minHeight: 64 }} align=\"bottom\" className={styles.tabs}>\n        <Tabs\n          className=\"custom-mentor-registry-tabs\"\n          tabBarStyle={{ margin: '0' }}\n          activeKey={activeTab}\n          items={tabs}\n          onChange={handleTabChange}\n        />\n        <Space style={{ alignSelf: 'center' }}>\n          <Button icon={<FileExcelOutlined />} onClick={() => (window.location.href = `/api/registry/mentors/csv`)}>\n            Export CSV\n          </Button>\n          {session.isAdmin && (\n            <Button type=\"primary\" onClick={() => setModalData({ mode: ModalDataMode.BatchInvite })}>\n              Invite mentors\n            </Button>\n          )}\n        </Space>\n      </Row>\n      <Col style={{ padding: 24 }}>\n        <Alert\n          message={\n            <>\n              The number of mentors below can mentor: <Typography.Text strong>{maxStudents} students</Typography.Text>\n            </>\n          }\n          type=\"info\"\n          showIcon\n          style={{ marginBottom: 24 }}\n        />\n        <MentorRegistryTableContainer\n          mentors={data}\n          courses={courses}\n          activeTab={activeTab}\n          disciplines={disciplines}\n          handleModalDataChange={handleModalDataChange}\n          tagFilters={tagFilters}\n          setTagFilters={setTagFilters}\n          combinedFilter={combinedFilter}\n          setCombinedFilter={setCombinedFilter}\n          setCurrentPage={setCurrentPage}\n          currentPage={currentPage}\n          total={total}\n        >\n          {mentorRegistryProps => <MentorRegistryTable {...mentorRegistryProps} />}\n        </MentorRegistryTableContainer>\n      </Col>\n      {isModalOpen && modalData?.mode === ModalDataMode.Invite && renderModal()}\n      {isModalOpen && modalData?.mode === ModalDataMode.Resend && (\n        <MentorRegistryResendModal\n          modalData={modalData || {}}\n          modalLoading={modalLoading}\n          resendConfirmation={resendConfirmation}\n          onCancel={onCancelModal}\n        />\n      )}\n      {isModalOpen && modalData?.mode === ModalDataMode.Delete && (\n        <MentorRegistryDeleteModal\n          modalData={modalData || {}}\n          modalLoading={modalLoading}\n          onCancel={onCancelModal}\n          cancelMentor={cancelMentor}\n        />\n      )}\n      {isModalOpen && modalData?.mode === ModalDataMode.Comment && (\n        <CommentModal\n          title=\"Comment\"\n          visible={isModalOpen}\n          onCancel={onCancelModal}\n          initialValue={modalData?.record?.comment ?? undefined}\n          availableEmptyComment={true}\n          onOk={(comment: string) => {\n            sendMentorRegistryComment(comment);\n          }}\n        />\n      )}\n      {modalData?.mode === ModalDataMode.BatchInvite && <InviteMentorsModal onCancel={onCancelModal} />}\n      {contextHolder}\n    </AdminPageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor]} anyCoursePowerUser>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/notifications.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { AdminPage } from '@client/modules/Notifications/pages/AdminNotificationsPage';\n\nexport default function () {\n  return (\n    <SessionProvider adminOnly>\n      <AdminPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/prompts.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { PromptsPage } from '@client/modules/Prompts/pages/PromptPage';\n\nexport default function () {\n  return (\n    <SessionProvider adminOnly>\n      <PromptsPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/registrations.tsx",
    "content": "import { DislikeOutlined, HourglassOutlined, LikeOutlined } from '@ant-design/icons';\nimport { Button, Col, Row, Select, Statistic, Table, Typography } from 'antd';\nimport axios from 'axios';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { stringSorter } from '@client/shared/components/Table';\nimport { useState } from 'react';\nimport { formatMonthFriendly } from '@client/services/formatter';\nimport { Course, CourseRole } from '@client/services/models';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\n\nconst defaultRowGutter = 24;\nconst PAGINATION = 200;\nconst DEFAULT_STATISTICS = { approved: 0, rejected: 0, pending: 0 };\n\ntype Stats = {\n  approved: number;\n  rejected: number;\n  pending: number;\n};\n\ninterface Registration {\n  id: number;\n  status: string;\n  lastName: string;\n  user: {\n    name: string;\n    profileUrl: string;\n  };\n  githubId: string;\n}\n\nfunction Page() {\n  const { courses } = useActiveCourseContext();\n  const [activeCourses] = useState((courses || []).filter((course: Course) => !course.completed && !course.inviteOnly));\n  const [selectedIds, setSelectedIds] = useState([]);\n  const [loading, setLoading] = useState(false);\n  const [data, setData] = useState([]);\n  const [stats, setStats] = useState(DEFAULT_STATISTICS as Stats);\n  const [courseId, setCourseId] = useState(null as number | null);\n\n  const changeSelection = (_: any, selectedRows: any) => {\n    setSelectedIds(selectedRows.map((row: any) => row.id));\n  };\n\n  const handleCourseChange = async (id: number | string) => {\n    const courseId = Number(id);\n    setCourseId(courseId);\n\n    const url = `/api/registry?type=mentor&courseId=${courseId}`;\n    const {\n      data: { data: registrations },\n    } = await axios.get(url);\n    const statistics = { ...DEFAULT_STATISTICS };\n\n    for (const registration of registrations) {\n      switch (registration.status) {\n        case 'approved':\n          statistics.approved += 1;\n          break;\n        case 'rejected':\n          statistics.rejected += 1;\n          break;\n        case 'pending':\n          statistics.pending += 1;\n          break;\n      }\n    }\n    setLoading(false);\n    const data = registrations.map((registration: any) => {\n      const {\n        user,\n        id,\n        status,\n        attributes: { maxStudentsLimit },\n      } = registration;\n      const {\n        firstName,\n        lastName,\n        githubId,\n        contactsEpamEmail,\n        locationName: city,\n      } = user || {\n        firstName: '',\n        lastName: '',\n        githubId: '',\n        contactsEpamEmail: '',\n        locationName: '',\n      };\n\n      return {\n        id,\n        status,\n        githubId,\n        maxStudentsLimit,\n        user: { name: `${firstName} ${lastName}`, profileUrl: `/profile?githubId=${githubId}` },\n        isFromEpam: !!contactsEpamEmail,\n        city,\n      };\n    });\n    setData(data);\n    setSelectedIds([]);\n    setStats(statistics);\n  };\n\n  const handleSubmit = async (_: any, status: string) => {\n    if (selectedIds.length) {\n      try {\n        setLoading(true);\n        await axios.put('/api/registry', { ids: selectedIds, status });\n        await handleCourseChange(courseId as number);\n      } catch (e) {\n        console.error(e);\n      } finally {\n        setLoading(false);\n      }\n    }\n  };\n\n  const handleApprove = (e: any) => handleSubmit(e, 'approved');\n\n  const handleReject = async (e: any) => handleSubmit(e, 'rejected');\n\n  const [description] = activeCourses.filter(c => c.id === courseId).map(c => c.description);\n  const rowSelection = { onChange: changeSelection };\n\n  return (\n    <AdminPageLayout title=\"Registrations\" loading={loading} courses={courses}>\n      <Col>\n        <Row gutter={defaultRowGutter}>\n          <Col>\n            <Select style={{ width: 300 }} placeholder=\"Select a course...\" onChange={handleCourseChange}>\n              {activeCourses.map(course => (\n                <Select.Option key={course.id} value={course.id}>\n                  {course.name}{' '}\n                  {`(${course.discipline?.name ? `${course.discipline.name}, ` : ''}${formatMonthFriendly(\n                    course.startDate,\n                  )})`}\n                </Select.Option>\n              ))}\n            </Select>\n            <Typography.Paragraph type=\"secondary\">{description}</Typography.Paragraph>\n          </Col>\n        </Row>\n        {courseId && (\n          <Row gutter={defaultRowGutter}>\n            <Col span={12}>\n              <Button type=\"primary\" onClick={handleApprove}>\n                Approve\n              </Button>\n              <span>&nbsp;</span>\n              <Button danger onClick={handleReject}>\n                Reject\n              </Button>\n            </Col>\n            <Col span={4}>\n              <Statistic\n                title=\"Approved\"\n                value={stats.approved}\n                valueStyle={{ color: '#3f8600' }}\n                prefix={<LikeOutlined />}\n              />\n            </Col>\n            <Col span={4}>\n              <Statistic\n                title=\"Rejected\"\n                value={stats.rejected}\n                valueStyle={{ color: '#cf1322' }}\n                prefix={<DislikeOutlined />}\n              />\n            </Col>\n            <Col span={4}>\n              <Statistic title=\"Pending\" value={stats.pending} prefix={<HourglassOutlined />} />\n            </Col>\n          </Row>\n        )}\n        {courseId && (\n          <Table<Registration>\n            bordered\n            pagination={{ pageSize: PAGINATION }}\n            size=\"small\"\n            rowKey=\"id\"\n            dataSource={data}\n            rowSelection={rowSelection}\n            columns={[\n              {\n                title: 'Name',\n                dataIndex: 'lastName',\n                key: 'lastName',\n                width: 150,\n                sorter: stringSorter('lastName'),\n                render: (_: any, record: Registration) => <a href={record.user.profileUrl}>{record.user.name}</a>,\n              },\n              {\n                title: 'GitHub',\n                dataIndex: 'githubId',\n                key: 'githubId',\n                width: 100,\n                sorter: stringSorter('githubId'),\n                render: (value: string) => <GithubUserLink value={value} />,\n              },\n              {\n                title: 'Status',\n                dataIndex: 'status',\n                key: 'status',\n                sorter: stringSorter('status'),\n                width: 50,\n              },\n              {\n                title: 'City',\n                dataIndex: 'city',\n                key: 'city',\n                width: 50,\n              },\n              {\n                title: 'Max students amount',\n                dataIndex: 'maxStudentsLimit',\n                key: 'maxStudentsLimit',\n                width: 100,\n              },\n              {\n                title: 'From EPAM',\n                dataIndex: 'isFromEpam',\n                key: 'isFromEpam',\n                width: 30,\n                render: (value: boolean) => (value ? 'Y' : 'N'),\n              },\n            ]}\n          />\n        )}\n      </Col>\n    </AdminPageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/students.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { Students } from '@client/modules/Students/Pages/Students';\n\nexport default function () {\n  return (\n    <SessionProvider hirerOnly>\n      <Students />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/tasks.tsx",
    "content": "import { CourseRole } from '@client/services/models';\nimport { SessionProvider } from '@client/modules/Course/contexts';\nimport { TasksPage } from '@client/modules/Tasks/pages';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]} anyCoursePowerUser>\n      <TasksPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/user-group.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { UserGroupsAdminPage } from '@client/modules/UserGroupsAdmin';\n\nexport default function () {\n  return (\n    <SessionProvider adminOnly>\n      <UserGroupsAdminPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/admin/users.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\nimport { UsersAdminPage } from '@client/modules/UsersAdmin';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]} anyCoursePowerUser>\n      <UsersAdminPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/applicants/index.tsx",
    "content": "import { Layout, Result, Table } from 'antd';\nimport { ApplicantResumeDto, OpportunitiesApi } from '@client/api';\nimport { Header } from '@client/shared/components/Header';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport { dateRenderer, getColumnSearchProps, stringSorter } from '@client/shared/components/Table';\nimport { SessionContext, SessionProvider } from '@client/modules/Course/contexts';\nimport { withRouter } from 'next/router';\nimport { useCallback, useContext, useEffect, useState } from 'react';\n\nconst { Content } = Layout;\n\nconst api = new OpportunitiesApi();\n\nfunction ApplicantsPage() {\n  const [loading, setLoading] = useState<boolean>(false);\n  const [applicants, setApplicants] = useState<ApplicantResumeDto[] | null>(null);\n  const { isAdmin, isHirer } = useContext(SessionContext);\n\n  const hasPriorityRole = isAdmin || isHirer;\n\n  const columns = [\n    {\n      title: 'Name',\n      sorter: stringSorter('name'),\n      render: (data: ApplicantResumeDto) => {\n        const { name, uuid } = data;\n\n        return (\n          <>\n            <a href={`/cv/${uuid}`}>{name ?? 'Unknown'}</a>\n          </>\n        );\n      },\n      ...getColumnSearchProps('name'),\n    },\n    {\n      title: 'Desired postion',\n      dataIndex: 'desiredPosition',\n      key: 'desiredPosition',\n      ...getColumnSearchProps('desiredPosition'),\n    },\n    {\n      title: 'Locations',\n      dataIndex: 'locations',\n      key: 'locations',\n      ...getColumnSearchProps('locations'),\n      render: (locationsRaw: string) => {\n        const locations = locationsRaw.split('\\n');\n        const locationsItems = locations.filter(l => l.trim()).map(location => <li key={location}>{location}</li>);\n        return <ul>{locationsItems}</ul>;\n      },\n    },\n    {\n      title: 'English level',\n      dataIndex: 'englishLevel',\n      key: 'englishLevel',\n      sorter: stringSorter('englishLevel'),\n      ...getColumnSearchProps('englishLevel'),\n    },\n    {\n      title: 'Full time',\n      dataIndex: 'fullTime',\n      key: 'fullTime',\n      ...getColumnSearchProps('fullTime'),\n    },\n    {\n      title: 'Start from',\n      dataIndex: 'startFrom',\n      key: 'startFrom',\n      sorter: stringSorter('startFrom'),\n      ...getColumnSearchProps('startFrom'),\n    },\n    {\n      title: 'CV expires',\n      dataIndex: 'expires',\n      key: 'expires',\n      sorter: stringSorter('expires'),\n      render: dateRenderer,\n      ...getColumnSearchProps('expires'),\n    },\n  ];\n\n  const fetchData = useCallback(async () => {\n    setLoading(true);\n    const { data } = await api.getApplicants();\n    setApplicants(data);\n    setLoading(false);\n  }, []);\n\n  const transformApplicants = (data: ApplicantResumeDto[]) => {\n    return data.map(item => {\n      const { fullTime, startFrom, englishLevel, desiredPosition, locations } = item;\n      return {\n        ...item,\n        desiredPosition: desiredPosition?.length ? desiredPosition : '<Not set>',\n        fullTime: fullTime ? 'Yes' : 'No',\n        locations: locations?.length ? locations : '<Not set>',\n        startFrom: startFrom?.length ? startFrom : '<Not set>',\n        englishLevel: englishLevel?.length ? englishLevel?.toUpperCase() : '<Not set>',\n      };\n    });\n  };\n\n  useEffect(() => {\n    if (hasPriorityRole) fetchData();\n  }, []);\n\n  if (!hasPriorityRole)\n    return (\n      <>\n        <Header />\n        <Result status=\"403\" title=\"Sorry, but you don't have access to this page\" />\n      </>\n    );\n\n  let data = null;\n\n  if (applicants) {\n    data = transformApplicants(applicants);\n  }\n\n  return (\n    <>\n      <Header />\n      <LoadingScreen show={loading}>\n        <Layout style={{ margin: 'auto' }}>\n          <Content style={{ minHeight: '60vh', margin: 'auto' }}>\n            <Table\n              pagination={{ pageSize: 100 }}\n              style={{ minWidth: '99vw' }}\n              columns={columns}\n              dataSource={data ?? undefined}\n            ></Table>\n          </Content>\n        </Layout>\n      </LoadingScreen>\n    </>\n  );\n}\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <ApplicantsPage />\n    </SessionProvider>\n  );\n}\n\nexport default withRouter(Page);\n"
  },
  {
    "path": "client/src/pages/course/403.tsx",
    "content": "import { CouseNoAccessPage } from '@client/modules/Course/pages/CouseNoAccess';\n\nexport default CouseNoAccessPage;\n"
  },
  {
    "path": "client/src/pages/course/admin/certified-students.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor]}>Certified Students</SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/cross-check-table.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { CrossCheckPairs } from '@client/modules/CrossCheckPairs/pages/CrossCheckPairs';\nimport { CourseRole } from '@client/services/models';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Dementor]}>\n      <CrossCheckPairs />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/events.tsx",
    "content": "import { Button, message, Select, Table, Typography } from 'antd';\nimport dayjs from 'dayjs';\nimport { useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { dateWithTimeZoneRenderer, idFromArrayRenderer } from '@client/shared/components/Table';\nimport { CourseEvent, CourseService } from '@client/services/course';\nimport { ALL_TIMEZONES } from '@client/configs/timezones';\nimport { getColumnSearchProps } from '@client/shared/components/Table';\nimport { CourseEventModal } from '@client/modules/CourseManagement/components/CourseEventModal';\nimport { EventDto, EventsApi } from '@client/api';\n\nimport tz from 'dayjs/plugin/timezone';\nimport utc from 'dayjs/plugin/utc';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\nimport { CustomPopconfirm } from '@client/components/common/CustomPopconfirm';\n\ndayjs.extend(utc);\ndayjs.extend(tz);\n\nconst eventsApi = new EventsApi();\n\nfunction Page() {\n  const { course, courses } = useActiveCourseContext();\n  const courseId = course.id;\n  const [timeZone, setTimeZone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);\n  const [modalData, setModalData] = useState<Partial<CourseEvent> | null>(null);\n  const service = useMemo(() => new CourseService(courseId), [courseId]);\n  const [data, setData] = useState([] as CourseEvent[]);\n  const [events, setEvents] = useState<EventDto[]>([]);\n\n  const { loading } = useAsync(async () => {\n    const [data, { data: events }] = await Promise.all([service.getCourseEvents(), eventsApi.getEvents()]);\n    setData(data);\n    setEvents(events);\n  }, [courseId]);\n\n  const handleTimeZoneChange = (timeZone: string) => {\n    setTimeZone(timeZone);\n  };\n\n  const refreshData = async () => {\n    const data = await service.getCourseEvents();\n    setData(data);\n  };\n\n  const handleDeleteItem = async (id: number) => {\n    try {\n      await service.deleteCourseEvent(id);\n      await refreshData();\n    } catch {\n      message.error('Failed to delete item. Please try later.');\n    }\n  };\n\n  const handleAddEvent = () => {\n    setModalData({});\n  };\n\n  const handleEditEvent = (event: Partial<CourseEvent>) => {\n    setModalData(event);\n  };\n\n  const handleEventSubmit = async () => {\n    setModalData(null);\n    refreshData();\n  };\n\n  return (\n    <AdminPageLayout showCourseName loading={loading} courses={courses}>\n      <Button type=\"primary\" onClick={handleAddEvent}>\n        Add Event\n      </Button>\n      <Select\n        style={{ marginLeft: 16, width: 200 }}\n        placeholder=\"Please select a timezone\"\n        defaultValue={timeZone}\n        onChange={handleTimeZoneChange}\n      >\n        {ALL_TIMEZONES.map(tz => (\n          <Select.Option key={tz} value={tz}>\n            {tz === 'Europe/Kiev' ? 'Europe/Kyiv' : tz}\n          </Select.Option>\n        ))}\n      </Select>\n      <Table\n        style={{ margin: '16px 0' }}\n        rowKey=\"id\"\n        bordered\n        pagination={false}\n        size=\"small\"\n        dataSource={data}\n        columns={getColumns(handleEditEvent, handleDeleteItem, { timeZone, events })}\n        scroll={{ x: 1020, y: 'calc(100vh - 265px)' }}\n      />\n      {modalData && (\n        <CourseEventModal\n          data={modalData}\n          onSubmit={handleEventSubmit}\n          onCancel={() => setModalData(null)}\n          courseId={courseId}\n        />\n      )}\n    </AdminPageLayout>\n  );\n}\n\nfunction getColumns(\n  handleEditItem: (event: Partial<CourseEvent>) => void,\n  handleDeleteItem: any,\n  { timeZone, events }: any,\n) {\n  return [\n    { title: 'Id', dataIndex: 'id' },\n    {\n      title: 'Name',\n      dataIndex: 'eventId',\n      render: idFromArrayRenderer(events),\n      ...getColumnSearchProps('event.name'),\n    },\n    { title: 'Type', dataIndex: ['event', 'type'] },\n    {\n      title: 'Start Date',\n      dataIndex: 'dateTime',\n      render: dateWithTimeZoneRenderer(timeZone, 'YYYY-MM-DD HH:mm'),\n    },\n    { title: 'End Date', dataIndex: 'endTime', render: dateWithTimeZoneRenderer(timeZone, 'YYYY-MM-DD HH:mm') },\n    { title: 'Place', dataIndex: 'place' },\n    {\n      title: 'Organizer',\n      dataIndex: ['organizer', 'githubId'],\n      render: (value: string) => (value ? <GithubUserLink value={value} /> : ''),\n    },\n    { title: 'Comment', dataIndex: 'comment' },\n    {\n      title: 'Actions',\n      dataIndex: 'actions',\n      width: 110,\n      render: (_: any, record: CourseEvent) => (\n        <>\n          <span>\n            <a onClick={() => handleEditItem(record)}>Edit</a>{' '}\n          </span>\n          <span style={{ marginLeft: 8 }}>\n            <CustomPopconfirm\n              onConfirm={() => handleDeleteItem(record.id)}\n              title=\"Are you sure you want to delete this item?\"\n            >\n              <Typography.Link>Delete</Typography.Link>\n            </CustomPopconfirm>\n          </span>\n        </>\n      ),\n    },\n  ];\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/interviews.tsx",
    "content": "import { Button, Row, Select, Table, Popconfirm } from 'antd';\nimport { StudentMentorModal } from '@client/shared/components/StudentMentorModal';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport {\n  getColumnSearchProps,\n  stringSorter,\n  boolIconRenderer,\n  PersonCell,\n  numberSorter,\n} from '@client/shared/components/Table';\nimport { useLoading } from '@client/components/useLoading';\nimport { useMemo, useState, useContext } from 'react';\nimport { CourseService } from '@client/services/course';\nimport { CourseRole } from '@client/services/models';\nimport { useAsync } from 'react-use';\nimport { isCourseManager } from '@client/domain/user';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CoursesInterviewsApi, InterviewDto, InterviewPairDto } from '@client/api';\n\nconst coursesInterviewsApi = new CoursesInterviewsApi();\n\nfunction Page() {\n  const session = useContext(SessionContext);\n  const { course, courses } = useActiveCourseContext();\n  const courseId = course.id;\n\n  const [loading, withLoading] = useLoading(false);\n  const [interviews, setInterviews] = useState<InterviewDto[]>([]);\n\n  const [data, setData] = useState([] as InterviewPairDto[]);\n  const [selected, setSelected] = useState<string | null>(null);\n  const [modal, setModal] = useState(false);\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n\n  const courseManagerRole = useMemo(() => isCourseManager(session, courseId), [course, session]);\n\n  const loadInterviews = async () => {\n    const { data: interviews } = await coursesInterviewsApi.getInterviews(courseId);\n    const filtered = interviews.filter(({ type }) => type === 'interview');\n    setInterviews(filtered);\n    setSelected(filtered[0]?.id.toString() ?? null);\n  };\n\n  const deleteInterview = withLoading(async (record: InterviewPairDto) => {\n    await courseService.cancelInterviewPair(selected!, String(record.id));\n    const filtered = data.filter(d => d.id !== record.id);\n    setData(filtered);\n  });\n\n  const loadData = async () => {\n    if (selected) {\n      const { data } = await coursesInterviewsApi.getInterviewPairs(Number(selected), courseId);\n      setData(data);\n    }\n  };\n\n  const createInterviews = withLoading(async () => {\n    if (selected) {\n      const courseTaskId = Number(selected);\n      const isInterviewsIncludesSelected = interviews.map(({ id }) => id).includes(courseTaskId);\n\n      if (isInterviewsIncludesSelected) {\n        await courseService.createInterviewDistribution(courseTaskId);\n        await loadData();\n      }\n    }\n  });\n\n  useAsync(withLoading(loadData), [selected]);\n\n  useAsync(withLoading(loadInterviews), []);\n\n  return (\n    <AdminPageLayout loading={loading} title=\"Interviews\" showCourseName courses={courses}>\n      <Row style={{ marginBottom: 16, gap: 16 }} justify=\"space-between\">\n        <Row style={{ gap: 16 }}>\n          <Select value={selected!} onChange={(value: string) => setSelected(value)} style={{ minWidth: 300 }}>\n            {interviews.map(interview => (\n              <Select.Option value={interview.id.toString()} key={interview.id.toString()}>\n                {interview.name}\n              </Select.Option>\n            ))}\n          </Select>\n          {courseManagerRole ? (\n            <div>\n              <Popconfirm\n                onConfirm={() => createInterviews()}\n                title=\"Do you want to create interview pairs for not distributed students?\"\n              >\n                <Button>Create Interview Pairs</Button>\n              </Popconfirm>\n            </div>\n          ) : null}\n        </Row>\n        <Button type=\"primary\" onClick={() => setModal(true)}>\n          Create\n        </Button>\n      </Row>\n\n      <Table\n        pagination={{ defaultPageSize: 50 }}\n        size=\"small\"\n        rowKey=\"id\"\n        dataSource={data}\n        columns={[\n          {\n            fixed: 'left',\n            title: 'Interviewer',\n            dataIndex: 'interviewer',\n            sorter: stringSorter('interviewer.githubId' as keyof InterviewPairDto),\n            render: value => <PersonCell value={value} />,\n            ...getColumnSearchProps('interviewer.githubId'),\n          },\n          {\n            title: 'Student',\n            dataIndex: 'student',\n            sorter: stringSorter('student.githubId' as keyof InterviewPairDto),\n            render: value => <PersonCell value={value} />,\n            ...getColumnSearchProps('student.githubId'),\n          },\n          {\n            title: 'Completed',\n            dataIndex: 'status',\n            sorter: stringSorter('status'),\n            render: boolIconRenderer,\n          },\n          {\n            title: 'Result',\n            dataIndex: 'result',\n            sorter: numberSorter('result'),\n          },\n          {\n            fixed: 'right',\n            title: 'Actions',\n            dataIndex: 'actions',\n            width: 80,\n            render: (_, record) => {\n              if (isCourseManager(session, course.id)) {\n                return (\n                  <Button type=\"link\" onClick={() => deleteInterview(record)}>\n                    Cancel\n                  </Button>\n                );\n              }\n              return null;\n            },\n          },\n        ]}\n      />\n\n      <StudentMentorModal\n        onOk={withLoading(async (studentGithubId, mentorGithubId) => {\n          await courseService.addInterviewPair(selected!, mentorGithubId, studentGithubId);\n          await loadData();\n          setModal(false);\n        })}\n        onCancel={() => setModal(false)}\n        visible={modal}\n        courseId={course.id}\n      />\n    </AdminPageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/mentor-tasks-review.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { MentorTasksReview } from '@client/modules/MentorTasksReview/pages/MentorTasksReview';\nimport { CourseRole } from '@client/services/models';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Dementor]}>\n      <MentorTasksReview />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/mentors.tsx",
    "content": "import { DownOutlined, FileExcelOutlined, SyncOutlined } from '@ant-design/icons';\nimport { Button, Divider, Dropdown, MenuProps, Modal, Row, Space, Statistic, Table, message } from 'antd';\nimport { CourseMentorsApi, CourseStatsApi, MentorDetailsDto } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { AssignStudentModal } from '@client/components/Student';\nimport { PersonCell, getColumnSearchProps, numberSorter, stringSorter } from '@client/shared/components/Table';\nimport { Session } from '@client/components/withSession';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useContext, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { relativeDays } from '@client/services/formatter';\nimport { CourseRole } from '@client/services/models';\n\ntype Stats = {\n  students: { studentsGroupName: string; totalCount: number }[];\n  recordCount: number;\n};\n\nconst courseMentorsApi = new CourseMentorsApi();\nconst courseStatsApi = new CourseStatsApi();\n\nfunction getItems(mentor: MentorDetailsDto, session: Session): MenuProps['items'] {\n  return [\n    {\n      label: 'Add student',\n      key: 'student',\n      disabled: !mentor.isActive,\n    },\n    {\n      label: 'Expel',\n      key: 'expel',\n      disabled: !mentor.isActive,\n    },\n    {\n      label: 'Restore',\n      key: 'restore',\n      disabled: mentor.isActive,\n    },\n    session.isAdmin\n      ? {\n          label: 'Endorsement',\n          key: 'endorsment',\n        }\n      : null,\n  ].filter(Boolean) as MenuProps['items'];\n}\n\nfunction Page() {\n  const session = useContext(SessionContext);\n  const { course, courses } = useActiveCourseContext();\n  const courseId = course.id;\n  const [loading, setLoading] = useState(false);\n  const [stats, setStats] = useState(null as Stats | null);\n  const [mentors, setMentors] = useState<MentorDetailsDto[]>([]);\n  const [currentMentor, setCurrentMentor] = useState<string | null>(null);\n  const [modal, contextHolder] = Modal.useModal();\n\n  const service = useMemo(() => new CourseService(courseId), [courseId]);\n\n  const studentsValueName = ['Students with a mentor', 'Students who can have a mentor'];\n\n  useAsync(async () => {\n    setLoading(true);\n    const [{ data: mentorsStats }, { data: records }] = await Promise.all([\n      courseStatsApi.getCourseMentors(courseId),\n      courseMentorsApi.getMentorsDetails(courseId),\n    ]);\n\n    const studentsGroupCount = records.reduce(\n      (acc, { studentsCount, maxStudentsLimit, isActive }) => {\n        acc[0] = (acc[0] ?? 0) + (studentsCount ? studentsCount : 0);\n        acc[1] = (acc[1] ?? 0) + (maxStudentsLimit && isActive ? maxStudentsLimit : 0);\n        return acc;\n      },\n      [0, 0] as [number, number],\n    );\n\n    setLoading(false);\n    setMentors(records);\n    setStats({\n      recordCount: mentorsStats.mentorsActiveCount,\n      students: studentsValueName.map((valueName, idx) => ({\n        studentsGroupName: valueName,\n        totalCount: studentsGroupCount[idx] ?? 0,\n      })),\n    });\n  }, []);\n\n  const handleExpel = async ({ githubId }: MentorDetailsDto) => {\n    try {\n      setLoading(true);\n      await service.expelMentor(githubId);\n      setMentors(prevRecords => prevRecords.map(r => (r.githubId === githubId ? { ...r, isActive: false } : r)));\n    } catch {\n      message.error('An error occured. Please try later.');\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleRestore = async ({ githubId }: MentorDetailsDto) => {\n    try {\n      setLoading(true);\n      await service.restoreMentor(githubId);\n      setMentors(prevRecords => prevRecords.map(r => (r.githubId === githubId ? { ...r, isActive: true } : r)));\n    } catch {\n      message.error('An error occured. Please try later.');\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const syncWithGitHubTeams = async () => {\n    try {\n      setLoading(true);\n      await service.postSyncRepositoriesMentors();\n    } catch {\n      message.error('An error occured. Please try later.');\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const handleMenuClick = async (menuItem: { key: string }, mentor: MentorDetailsDto) => {\n    switch (menuItem.key) {\n      case 'student': {\n        setCurrentMentor(mentor.githubId);\n        break;\n      }\n      case 'expel': {\n        modal.confirm({\n          onOk: () => handleExpel(mentor),\n          title: 'Are you sure you want to expel this mentor?',\n        });\n        break;\n      }\n      case 'restore':\n        modal.confirm({\n          onOk: () => handleRestore(mentor),\n          title: 'Do you want to restore the mentor?',\n        });\n        break;\n    }\n  };\n\n  const exportToCsv = () => (window.location.href = `/api/v2/course/${courseId}/mentors/details/csv`);\n\n  return (\n    <AdminPageLayout loading={loading} title=\"Course Mentors\" showCourseName courses={courses}>\n      <div style={{ maxWidth: 310, flex: 'auto', border: '1px rgba(0, 0, 0, 0.06) dashed', padding: '10px' }}>\n        <Statistic title=\"Active Mentors\" value={stats?.recordCount} />\n        <Statistic title=\"Max Students Count\" value={stats?.students[1]?.totalCount ?? 0} />\n        <Table\n          pagination={false}\n          size=\"small\"\n          rowKey=\"studentsGroupName\"\n          dataSource={stats?.students ?? []}\n          columns={[\n            { title: 'Group of students', dataIndex: 'studentsGroupName' },\n            { title: 'Count', dataIndex: 'totalCount' },\n          ]}\n        />\n      </div>\n      <Divider dashed />\n      <Row justify=\"end\" style={{ marginBottom: 16, marginTop: 16 }}>\n        <Button icon={<SyncOutlined />} style={{ marginRight: 8 }} onClick={syncWithGitHubTeams}>\n          Sync with GitHub Teams\n        </Button>\n        <Button icon={<FileExcelOutlined />} style={{ marginRight: 8 }} onClick={exportToCsv}>\n          Export CSV\n        </Button>\n      </Row>\n      <Table<MentorDetailsDto>\n        rowKey=\"githubId\"\n        rowClassName={record => (!record.isActive ? 'rs-table-row-cols-disabled' : '')}\n        pagination={{ pageSize: 100 }}\n        size=\"small\"\n        dataSource={mentors}\n        columns={[\n          {\n            title: 'Mentor',\n            dataIndex: 'githubId',\n            sorter: stringSorter('githubId'),\n            width: 200,\n            render: (_, record) => <PersonCell value={record} />,\n            ...getColumnSearchProps(['githubId', 'name']),\n          },\n          {\n            title: 'City',\n            dataIndex: 'cityName',\n            key: 'cityName',\n            width: 100,\n            sorter: stringSorter('cityName'),\n            ...getColumnSearchProps('cityName'),\n          },\n          {\n            title: 'Country',\n            dataIndex: 'countryName',\n            key: 'countryName',\n            width: 100,\n            sorter: stringSorter('countryName'),\n            ...getColumnSearchProps('countryName'),\n          },\n          {\n            title: 'Preference',\n            dataIndex: 'studentsPreference',\n            sorter: stringSorter('studentsPreference'),\n            width: 80,\n          },\n          {\n            title: 'Max Students',\n            dataIndex: 'maxStudentsLimit',\n            sorter: numberSorter('maxStudentsLimit'),\n            width: 80,\n          },\n          {\n            title: 'Screenings',\n            dataIndex: ['screenings'],\n            sorter: numberSorter('screenings.completed' as keyof MentorDetailsDto),\n            width: 80,\n            render: value => `${value.completed} / ${value.total}`,\n          },\n          {\n            title: 'Interviews',\n            dataIndex: ['interviews'],\n            sorter: numberSorter('interviews.completed' as keyof MentorDetailsDto),\n            width: 80,\n            render: value => `${value.completed} / ${value.total}`,\n          },\n          {\n            title: 'Students',\n            dataIndex: 'studentsCount',\n            sorter: numberSorter('studentsCount'),\n            width: 80,\n          },\n          {\n            title: 'Checked Tasks',\n            dataIndex: 'taskResultsStats',\n            sorter: numberSorter('taskResultsStats.checked' as keyof MentorDetailsDto),\n            render: value => `${value.checked} / ${value.total}`,\n          },\n          {\n            title: 'Last Checked Task',\n            dataIndex: ['taskResultsStats', 'lastUpdatedDate'],\n            sorter: numberSorter('taskResultsStats.lastUpdatedDate' as keyof MentorDetailsDto),\n            render: (value: string) => (value ? `${relativeDays(value)} days ago` : null),\n          },\n          {\n            dataIndex: 'actions',\n            width: 120,\n            render: (_: string, mentor: MentorDetailsDto) => {\n              const items = getItems(mentor, session);\n              return (\n                <Dropdown menu={{ items, onClick: e => handleMenuClick(e, mentor) }}>\n                  <Button>\n                    <Space>\n                      Actions\n                      <DownOutlined />\n                    </Space>\n                  </Button>\n                </Dropdown>\n              );\n            },\n          },\n        ]}\n      />\n      <AssignStudentModal\n        onClose={() => setCurrentMentor(null)}\n        courseId={courseId}\n        open={Boolean(currentMentor)}\n        mentorGithuId={currentMentor}\n      />\n      {contextHolder}\n    </AdminPageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/reports.tsx",
    "content": "import { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\nimport ExpelledStudentsStats from '@client/modules/CourseManagement/components/ExpelledStudentsStats';\n\nfunction Page() {\n  const { courses, course } = useActiveCourseContext();\n\n  return (\n    <AdminPageLayout loading={false} showCourseName courses={courses}>\n      <ExpelledStudentsStats courseId={course?.id} />\n    </AdminPageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/stage-interviews.tsx",
    "content": "import { Button, Row, Table, Checkbox, Popconfirm } from 'antd';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { StudentMentorModal } from '@client/shared/components/StudentMentorModal';\nimport {\n  boolIconRenderer,\n  getColumnSearchProps,\n  numberSorter,\n  stringSorter,\n  PersonCell,\n} from '@client/shared/components/Table';\nimport { useLoading } from '@client/components/useLoading';\nimport { useMemo, useState, useContext } from 'react';\nimport { CourseService } from '@client/services/course';\nimport { CourseRole } from '@client/services/models';\nimport { useAsync } from 'react-use';\nimport { isCourseManager, isCourseSupervisor } from '@client/domain/user';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\n\nfunction Page() {\n  const session = useContext(SessionContext);\n  const { course, courses } = useActiveCourseContext();\n  const courseId = course.id;\n\n  const [loading, withLoading] = useLoading(false);\n  const [interviews, setInterviews] = useState([] as any[]);\n  const [modal, setModal] = useState(false);\n  const [noRegistration, setNoRegistration] = useState(false);\n\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const courseManagerRole = useMemo(() => isCourseManager(session, courseId), [course, session]);\n  const courseSupervisorRole = useMemo(() => isCourseSupervisor(session, courseId), [course, session]);\n\n  const loadInterviews = async () => setInterviews(await courseService.getStageInterviews());\n\n  const createInterviews = async () => {\n    await courseService.createStageInterviews({ noRegistration });\n    await loadInterviews();\n  };\n\n  const deleteInterview = withLoading(async (record: any) => {\n    await courseService.deleteStageInterview(record.id);\n    await loadInterviews();\n  });\n\n  useAsync(withLoading(loadInterviews), []);\n\n  return (\n    <AdminPageLayout loading={loading} title=\"Technical Screening\" showCourseName courses={courses}>\n      <Row style={{ marginBottom: 16 }} justify=\"space-between\">\n        {courseManagerRole ? (\n          <div>\n            <Checkbox checked={noRegistration} onChange={e => setNoRegistration(e.target.checked)}>\n              No Registration\n            </Checkbox>\n            <Popconfirm\n              onConfirm={() => createInterviews()}\n              title=\"Do you want to create interview pairs for not distributed students?\"\n            >\n              <Button>Create Interview Pairs</Button>\n            </Popconfirm>\n          </div>\n        ) : null}\n        <Button type=\"primary\" onClick={() => setModal(true)}>\n          Create interview\n        </Button>\n      </Row>\n\n      <Table\n        pagination={{\n          defaultPageSize: 50,\n          showSizeChanger: true,\n          showTotal: (total, range) => `${range[0]}-${range[1]} of ${total} interviews`,\n        }}\n        size=\"small\"\n        rowKey=\"id\"\n        dataSource={interviews}\n        columns={[\n          {\n            fixed: 'left',\n            title: 'Interviewer',\n            dataIndex: 'interviewer',\n            sorter: stringSorter('interviewer.githubId'),\n            render: value => <PersonCell value={value} showCountry={true} />,\n            ...getColumnSearchProps('interviewer.githubId'),\n          },\n          {\n            title: 'Student',\n            dataIndex: 'student',\n            sorter: stringSorter('student.githubId'),\n            render: value => <PersonCell value={value} showCountry={true} />,\n            ...getColumnSearchProps('student.githubId'),\n          },\n          {\n            title: 'Preference',\n            dataIndex: ['interviewer', 'preference'],\n            sorter: stringSorter('interviewer.preference'),\n            width: 80,\n          },\n          {\n            title: 'Student Score',\n            dataIndex: ['student', 'totalScore'],\n            sorter: numberSorter('student.totalScore'),\n            width: 80,\n          },\n          {\n            title: 'Completed',\n            dataIndex: 'completed',\n            sorter: stringSorter('completed'),\n            render: boolIconRenderer,\n            width: 80,\n          },\n          {\n            fixed: 'right',\n            title: 'Actions',\n            dataIndex: 'actions',\n            width: 80,\n            render: (_, record) => {\n              if (courseManagerRole || courseSupervisorRole) {\n                return (\n                  <Button type=\"link\" onClick={() => deleteInterview(record)}>\n                    Cancel\n                  </Button>\n                );\n              }\n              return null;\n            },\n          },\n        ]}\n      />\n\n      <StudentMentorModal\n        onOk={withLoading(async (studentGithubId, mentorGithubId) => {\n          await courseService.createInterview(studentGithubId, mentorGithubId);\n          await loadInterviews();\n          setModal(false);\n        })}\n        onCancel={() => setModal(false)}\n        visible={modal}\n        courseId={course.id}\n      />\n    </AdminPageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/students.tsx",
    "content": "import { BranchesOutlined, CheckCircleTwoTone, ClockCircleTwoTone, MinusCircleOutlined } from '@ant-design/icons';\nimport { Button, Row, Space, Statistic, Switch, Table, Typography } from 'antd';\nimport { ColumnProps } from 'antd/lib/table/Column';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { DashboardDetails } from '@client/components/Student';\nimport {\n  boolIconRenderer,\n  boolSorter,\n  getColumnSearchProps,\n  numberSorter,\n  PersonCell,\n  stringSorter,\n} from '@client/shared/components/Table';\nimport { useLoading } from '@client/components/useLoading';\nimport { isAdmin, isCourseManager, isCourseSupervisor } from '@client/domain/user';\nimport { useMessage } from '@client/hooks';\nimport keys from 'lodash/keys';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CertificateCriteriaModal, ExpelCriteriaModal } from '@client/modules/CourseManagement/components';\nimport { useContext, useMemo, useState } from 'react';\nimport { useAsync, useToggle } from 'react-use';\nimport { CourseService, StudentDetails } from '@client/services/course';\nimport { CourseRole } from '@client/services/models';\n\nconst { Text } = Typography;\n\ntype Stats = { activeStudentsCount: number; studentsCount: number; countries: unknown[] };\n\ntype CertificateCriteria = {\n  courseTaskIds?: number[];\n  minScore?: number;\n  minTotalScore?: number;\n};\ntype ExpelCriteria = {\n  courseTaskIds?: number[];\n  minScore?: number;\n  keepWithMentor?: boolean;\n  reason: string;\n};\n\nfunction Page() {\n  const { message } = useMessage();\n  const { course, courses } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n\n  const courseId = course.id;\n  const hasAdminRole = isAdmin(session);\n\n  const [loading, withLoading] = useLoading(false);\n  const [hasCourseManagerRole] = useState(isCourseManager(session, courseId));\n  const hasCourseSupervisorRole = useMemo(() => isCourseSupervisor(session, course.id), [session, course.id]);\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const [students, setStudents] = useState([] as StudentDetails[]);\n  const [stats, setStats] = useState(null as Stats | null);\n  const [activeOnly, setActiveOnly] = useState(false);\n  const [details, setDetails] = useState<StudentDetails | null>(null);\n  const [isExpelModalOpen, toggleExpelModal] = useToggle(false);\n  const [isCertificateModalOpen, toggleCertificateModal] = useToggle(false);\n\n  useAsync(withLoading(loadStudents), [activeOnly, details]);\n\n  const issueCertificate = withLoading(async () => {\n    const githubId = details?.githubId;\n    if (githubId != null) {\n      await courseService.createCertificate(githubId);\n      message.info('The certificate has been requested.');\n    }\n  });\n\n  const removeCertificate = withLoading(async () => {\n    const studentId = details?.id;\n    if (studentId != null) {\n      await courseService.removeCertificate(studentId);\n      message.info('The certificate has been removed.');\n    }\n  });\n\n  const createRepository = withLoading(async () => {\n    const githubId = details?.githubId;\n    if (githubId != null) {\n      const { repository } = await courseService.createRepository(githubId);\n      const newStudents = students.map(s => (s.githubId === githubId ? { ...s, repository: repository } : s));\n      setStudents(newStudents);\n    }\n  });\n\n  const expelStudent = withLoading(async (text: string) => {\n    const githubId = details?.githubId;\n    if (githubId != null) {\n      await courseService.expelStudent(githubId, text);\n      message.info('Student has been expelled');\n      setDetails(null);\n    }\n  });\n\n  const restoreStudent = withLoading(async () => {\n    const githubId = details?.githubId;\n    if (githubId != null) {\n      await courseService.restoreStudent(githubId);\n      message.info('Student has been restored');\n      setDetails(null);\n    }\n  });\n\n  const updateMentor = withLoading(async (mentorGithuId: string | null = null) => {\n    const githubId = details?.githubId;\n    if (details != null && githubId != null) {\n      const student = await courseService.updateStudent(githubId, { mentorGithuId });\n      setDetails({ ...details, mentor: student.mentor });\n    }\n  });\n\n  const expelStudents = withLoading(async ({ minScore, keepWithMentor, courseTaskIds, reason }: ExpelCriteria) => {\n    await courseService.expelStudents({ courseTaskIds, minScore }, { keepWithMentor }, reason);\n    toggleExpelModal();\n    loadStudents();\n    message.success('Students successfully expelled');\n  });\n\n  const issueCertificates = withLoading(async (criteria: CertificateCriteria) => {\n    await courseService.postCertificateStudents(criteria);\n    toggleCertificateModal();\n    message.success('All certificates successfully issued');\n  });\n\n  return render();\n\n  function render() {\n    return (\n      <AdminPageLayout loading={loading} showCourseName courses={courses}>\n        <Statistic\n          title=\"Active Students\"\n          value={stats?.activeStudentsCount ?? 0}\n          suffix={`/ ${stats?.studentsCount ?? 0}`}\n        />\n        <Row justify=\"space-between\" style={{ marginBottom: 16, marginTop: 16 }}>\n          <div>\n            <span style={{ display: 'inline-block', lineHeight: '24px' }}>Active Students Only</span>{' '}\n            <Switch checked={activeOnly} onChange={setActiveOnly} />\n          </div>\n          <div>{renderToolbar()}</div>\n        </Row>\n        <Table\n          rowKey=\"id\"\n          pagination={{ pageSize: 100, showSizeChanger: false }}\n          size=\"small\"\n          dataSource={students}\n          columns={getColumns()}\n        />\n\n        <DashboardDetails\n          isLoading={loading}\n          isAdmin={hasAdminRole}\n          onUpdateMentor={updateMentor}\n          onRestoreStudent={restoreStudent}\n          onIssueCertificate={issueCertificate}\n          onRemoveCertificate={removeCertificate}\n          onExpelStudent={expelStudent}\n          onCreateRepository={createRepository}\n          onClose={() => {\n            setDetails(null);\n            loadStudents();\n          }}\n          details={details}\n          courseId={course.id}\n          courseManagerOrSupervisor={hasCourseManagerRole || hasCourseSupervisorRole}\n        />\n        <ExpelCriteriaModal\n          courseId={course.id}\n          onClose={toggleExpelModal}\n          onSubmit={expelStudents}\n          isModalOpen={isExpelModalOpen}\n        />\n        <CertificateCriteriaModal\n          courseId={course.id}\n          onClose={toggleCertificateModal}\n          onSubmit={issueCertificates}\n          isModalOpen={isCertificateModalOpen}\n        />\n      </AdminPageLayout>\n    );\n  }\n\n  function renderToolbar() {\n    return (\n      <Space wrap>\n        {hasCourseManagerRole || hasCourseSupervisorRole ? <Button onClick={exportStudents}>Export CSV</Button> : null}\n        {hasCourseManagerRole ? (\n          <>\n            <Button onClick={toggleExpelModal} danger type=\"default\">\n              Expel Students\n            </Button>\n            <Button onClick={toggleCertificateModal} type=\"primary\">\n              Issue Certificates\n            </Button>\n          </>\n        ) : null}\n      </Space>\n    );\n  }\n\n  function exportStudents() {\n    courseService.exportStudentsCsv(activeOnly);\n  }\n\n  async function loadStudents() {\n    const courseStudents = await courseService.getCourseStudentsWithDetails(activeOnly);\n    setStudents(courseStudents);\n    setStats(calculateStats(courseStudents));\n  }\n\n  function getColumns(): ColumnProps<StudentDetails>[] {\n    return [\n      {\n        title: 'Active',\n        dataIndex: 'isActive',\n        width: 50,\n        render: boolIconRenderer,\n        sorter: boolSorter('isActive'),\n      },\n      {\n        title: 'Student',\n        dataIndex: 'githubId',\n        sorter: stringSorter('githubId'),\n        key: 'githubId',\n        render: (_, record) => <PersonCell value={record} />,\n        ...getColumnSearchProps(['githubId', 'name']),\n      },\n      {\n        title: 'Mentor',\n        dataIndex: 'mentor',\n        sorter: stringSorter('mentor.githubId' as keyof StudentDetails),\n        render: value => (value ? <PersonCell value={value} /> : null),\n        ...getColumnSearchProps(['mentor.githubId', 'mentor.name']),\n      },\n      {\n        title: 'City',\n        dataIndex: 'cityName',\n        width: 80,\n        sorter: stringSorter('cityName'),\n        ...getColumnSearchProps('cityName'),\n      },\n      {\n        title: 'Country',\n        dataIndex: 'countryName',\n        key: 'countryName',\n        width: 80,\n        sorter: stringSorter('countryName'),\n        ...getColumnSearchProps('countryName'),\n      },\n      {\n        title: 'Screening',\n        dataIndex: 'interviews',\n        width: 60,\n        render: (value: StudentDetails['interviews']) => {\n          if (value.length === 0) {\n            return <MinusCircleOutlined title=\"No Interview\" />;\n          }\n          if (value.every(e => e.isCompleted)) {\n            return <CheckCircleTwoTone title=\"Completed\" twoToneColor=\"#52c41a\" />;\n          }\n          return <ClockCircleTwoTone title=\"Assigned\" />;\n        },\n      },\n      {\n        title: <BranchesOutlined />,\n        dataIndex: 'repository',\n        width: 80,\n        render: (value: string) => (value ? <a href={value}>Link</a> : null),\n      },\n      {\n        title: 'Total',\n        dataIndex: 'totalScore',\n        key: 'totalScore',\n        width: 80,\n        align: 'right',\n        sorter: numberSorter('totalScore'),\n        render: (value: number) => <Text strong>{value.toFixed(1)}</Text>,\n      },\n      {\n        title: '',\n        dataIndex: 'actions',\n        fixed: 'right',\n        width: 60,\n        render: (_, record) => (\n          <Button type=\"default\" onClick={() => setDetails(record)}>\n            More\n          </Button>\n        ),\n      },\n    ];\n  }\n}\n\nfunction calculateStats(students: StudentDetails[]) {\n  let activeStudentsCount = 0;\n  const countries: Record<string, { count: number; totalCount: number }> = {};\n\n  for (const student of students) {\n    const { countryName } = student;\n    if (!countries[countryName]) {\n      countries[countryName] = { count: 0, totalCount: 0 };\n    }\n    countries[countryName].totalCount++;\n    if (student.isActive) {\n      activeStudentsCount++;\n      countries[countryName].count++;\n    }\n  }\n  return {\n    activeStudentsCount,\n    studentsCount: students.length,\n    countries: keys(countries).map(k => ({\n      name: k,\n      count: countries[k]?.count,\n      totalCount: countries[k]?.totalCount,\n    })),\n  };\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager, CourseRole.Supervisor, CourseRole.Dementor]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/tasks.tsx",
    "content": "import { MoreOutlined } from '@ant-design/icons';\nimport { Button, Dropdown, Table } from 'antd';\nimport { ItemType } from 'antd/es/menu/interface';\nimport { ColumnsType } from 'antd/lib/table';\nimport { CoursesTasksApi, CourseTaskDto, CrossCheckStatusEnum } from '@client/api';\nimport { GithubUserLink } from '@client/shared/components/GithubUserLink';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport {\n  crossCheckDateRenderer,\n  crossCheckStatusRenderer,\n  dateRenderer,\n  getColumnSearchProps,\n  stringSorter,\n} from '@client/shared/components/Table';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CourseTaskModal } from '@client/modules/CourseManagement/components/CourseTaskModal';\nimport { useCallback, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { CourseRole } from '@client/services/models';\nimport { useMessage } from '@client/hooks';\n\nconst courseTasksApi = new CoursesTasksApi();\n\nfunction Page() {\n  const { message } = useMessage();\n  const { course, courses } = useActiveCourseContext();\n  const courseId = course.id;\n  const service = useMemo(() => new CourseService(courseId), [courseId]);\n  const [loading, setLoading] = useState(false);\n  const [data, setData] = useState([] as CourseTaskDto[]);\n  const [modalData, setModalData] = useState<Partial<CourseTaskDto> | null>(null);\n  const [modalAction, setModalAction] = useState('update');\n\n  const loadData = useCallback(async () => {\n    setLoading(true);\n    const { data } = await courseTasksApi.getCourseTasksDetailed(courseId);\n    setData(data);\n    setLoading(false);\n  }, [courseId]);\n\n  useAsync(loadData, [courseId]);\n\n  const handleAddItem = () => {\n    setModalData({});\n    setModalAction('create');\n  };\n\n  const handleEditItem = (record: CourseTaskDto) => {\n    setModalData(record);\n    setModalAction('update');\n  };\n\n  const handleDeleteItem = async (id: number) => {\n    try {\n      const result = confirm('Are you sure you want to delete this item?');\n      if (!result) {\n        return;\n      }\n      await courseTasksApi.deleteCourseTask(course.id, id);\n      await loadData();\n    } catch {\n      message.error('Failed to delete item. Please try later.');\n    }\n  };\n\n  const handleModalSubmit = async (record: any) => {\n    if (modalAction === 'update') {\n      await courseTasksApi.updateCourseTask(course.id, modalData!.id!, record);\n    } else {\n      const { ...rest } = record;\n      await courseTasksApi.createCourseTask(course.id, rest);\n    }\n    await loadData();\n\n    setModalData(null);\n  };\n\n  const handleTaskDistribute = async (record: CourseTaskDto) => {\n    setLoading(true);\n    await service.createTaskDistribution(record.id);\n    setLoading(false);\n  };\n\n  const handleInterviewDistribute = async (record: CourseTaskDto) => {\n    setLoading(true);\n    await service.createInterviewDistribution(record.id);\n    setLoading(false);\n  };\n\n  const getDropdownMenu = (record: CourseTaskDto) => {\n    const hasInterviewDistibute = record.type === 'interview';\n    const hasTaskDistibute = record.checker === 'assigned';\n    const hasCrossCheck = record.checker === 'crossCheck';\n\n    const currentTimestamp = Date.now();\n    const submitDeadlineTimestamp = new Date(record.studentEndDate).getTime();\n    const isSubmitDeadlinePassed = currentTimestamp > submitDeadlineTimestamp;\n\n    const items = [\n      {\n        key: 'edit',\n        label: 'Edit',\n        onClick: () => handleEditItem(record),\n      },\n      {\n        key: 'delete',\n        label: 'Delete',\n        onClick: () => handleDeleteItem(record.id),\n      },\n      hasTaskDistibute\n        ? {\n            key: 'distribute',\n            label: 'Distribute',\n            onClick: () => handleTaskDistribute(record),\n          }\n        : null,\n      hasInterviewDistibute\n        ? {\n            key: 'interviewDistribute',\n            label: 'Distribute',\n            onClick: () => handleInterviewDistribute(record),\n          }\n        : null,\n      hasCrossCheck\n        ? {\n            type: 'divider',\n          }\n        : null,\n      hasCrossCheck\n        ? {\n            key: 'crossCheckDistribute',\n            label: 'Cross-Check: Distribute',\n            disabled: !isSubmitDeadlinePassed,\n            onClick: () => handleCrossCheckDistribution(record),\n          }\n        : null,\n      hasCrossCheck\n        ? {\n            key: 'crossCheckComplete',\n            label: 'Cross-Check: Complete',\n            disabled: !isSubmitDeadlinePassed || record.crossCheckStatus === CrossCheckStatusEnum.Initial,\n            onClick: () => handleCrossCheckCompletion(record),\n          }\n        : null,\n    ].filter(Boolean) as ItemType[];\n\n    return (\n      <Dropdown trigger={['click']} menu={{ items }}>\n        <Button size=\"small\">\n          More <MoreOutlined />\n        </Button>\n      </Dropdown>\n    );\n  };\n\n  const handleCrossCheckDistribution = async (record: CourseTaskDto) => {\n    try {\n      const {\n        data: { crossCheckPairs },\n      } = await service.createCrossCheckDistribution(record.id);\n      if (crossCheckPairs.length) {\n        message.success('Cross-Check distrubtion has been created');\n      } else {\n        message.warning('Cross-check pairs were not created because there are no submitted solutions');\n      }\n    } catch {\n      message.error('An error occurred.');\n    } finally {\n      await loadData();\n    }\n  };\n\n  const handleCrossCheckCompletion = async (record: CourseTaskDto) => {\n    try {\n      await service.createCrossCheckCompletion(record.id);\n\n      message.success('Cross-Check completed has been created');\n    } catch {\n      message.error('An error occurred.');\n    } finally {\n      await loadData();\n    }\n  };\n\n  return (\n    <AdminPageLayout loading={loading} courses={courses}>\n      <Button type=\"primary\" onClick={handleAddItem}>\n        Add Task\n      </Button>\n      <Table\n        style={{ marginTop: 16 }}\n        rowKey=\"id\"\n        pagination={false}\n        size=\"small\"\n        dataSource={data}\n        columns={getColumns(getDropdownMenu)}\n        scroll={{ x: 1020, y: 'calc(100vh - 265px)' }}\n      />\n      {modalData ? (\n        <CourseTaskModal onCancel={() => setModalData(null)} onSubmit={handleModalSubmit} data={modalData} />\n      ) : null}\n    </AdminPageLayout>\n  );\n}\n\nfunction getColumns(getDropdownMenu: (record: CourseTaskDto) => any): ColumnsType<CourseTaskDto> {\n  return [\n    { title: 'Id', dataIndex: 'id', fixed: 'left' },\n    {\n      title: 'Name',\n      dataIndex: 'name',\n      fixed: 'left',\n      ...getColumnSearchProps('name'),\n    },\n    { title: 'Scores Count', dataIndex: 'resultsCount' },\n    {\n      title: 'Start Date',\n      dataIndex: 'studentStartDate',\n      render: dateRenderer,\n      sorter: stringSorter('studentStartDate'),\n    },\n    { title: 'End Date', dataIndex: 'studentEndDate', render: dateRenderer, sorter: stringSorter('studentEndDate') },\n    {\n      title: 'Cross-Check End Date',\n      dataIndex: 'crossCheckEndDate',\n      render: crossCheckDateRenderer,\n      sorter: stringSorter('crossCheckEndDate'),\n    },\n    {\n      title: 'Cross-Check Status',\n      dataIndex: 'crossCheckStatus',\n      render: crossCheckStatusRenderer,\n      sorter: stringSorter('crossCheckStatus'),\n    },\n    { title: 'Max Score', dataIndex: 'maxScore' },\n    {\n      title: 'Type',\n      dataIndex: 'type',\n    },\n    { title: 'Score Weight', dataIndex: 'scoreWeight' },\n    { title: 'Who Checks', dataIndex: 'checker' },\n    {\n      title: 'Task Owner',\n      dataIndex: ['taskOwner', 'githubId'],\n      render: (value: string) => (value ? <GithubUserLink value={value} /> : null),\n    },\n    {\n      title: 'Pairs',\n      dataIndex: 'pairsCount',\n    },\n    {\n      dataIndex: 'actions',\n      width: 80,\n      render: (_: any, record: CourseTaskDto) => {\n        return getDropdownMenu(record);\n      },\n    },\n  ];\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/admin/users.tsx",
    "content": "import { Button, Checkbox, Col, Form, Row, Table, Tag, Modal, message } from 'antd';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { ModalForm } from '@client/shared/components/Forms';\nimport { boolIconRenderer, PersonCell, getColumnSearchProps } from '@client/shared/components/Table';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { useCallback, useState, useContext } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseUserDto, CourseUsersApi } from '@client/api';\nimport { CourseRole } from '@client/services/models';\nimport { UserService } from '@client/services/user';\nimport { UserGroupApi, UserGroupDto } from '@client/api';\nimport { AdminPageLayout } from '@client/shared/components/PageLayout';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\n\nconst userGroupService = new UserGroupApi();\nconst userService = new UserService();\nconst courseUserService = new CourseUsersApi();\n\nconst rolesColors: Record<string, string> = {\n  supervisor: 'purple',\n  manager: 'volcano',\n};\n\nfunction Page() {\n  const { course, courses } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n  const courseId = course.id;\n\n  const [loading, setLoading] = useState(false);\n  const [courseUsers, setCourseUsers] = useState([] as CourseUserDto[]);\n  const [userGroups, setUserGroups] = useState<UserGroupDto[] | null>(null);\n  const [userModalData, setUserModalData] = useState(null as Partial<CourseUserDto> | null);\n  const [groupModalData, setGroupModalData] = useState(null as UserGroupDto[] | null);\n\n  const loadData = useCallback(async () => {\n    try {\n      setLoading(true);\n\n      const [users, { data: groups }] = await Promise.all([\n        courseUserService.getCourseUsers(courseId),\n        session.isAdmin ? userGroupService.getUserGroups() : Promise.resolve({ data: null }),\n      ]);\n      setCourseUsers(users.data as any);\n      setUserGroups(groups);\n    } catch {\n      message.error('Something went wrong, please try reloading the page later');\n    } finally {\n      setLoading(false);\n    }\n  }, [courseId]);\n\n  useAsync(loadData, [courseId]);\n\n  const handleAddUser = () => {\n    setUserModalData({});\n  };\n\n  const handleAddGroup = () => {\n    setGroupModalData(userGroups);\n  };\n\n  const handleEditItem = (record: CourseUserDto) => {\n    setUserModalData(record);\n  };\n\n  const loadUsers = async (searchText: string) => userService.searchUser(searchText);\n\n  const handleUserModalSubmit = async (values: any) => {\n    const record = createRecord(values);\n    await courseUserService.putCourseUser(courseId, values.githubId, record);\n\n    setUserModalData(null);\n    loadData();\n  };\n\n  const handleGroupModalSubmit = async (values: UserGroupDto[]) => {\n    const records = createRecords(values);\n    await courseUserService.putCourseUsers(courseId, records);\n\n    setGroupModalData(null);\n    loadData();\n  };\n\n  const renderUserModal = (modalData: Partial<CourseUserDto> | null) => {\n    if (!modalData) {\n      return null;\n    }\n    return (\n      <ModalForm\n        getInitialValues={getInitialValues}\n        data={modalData}\n        title=\"Course User\"\n        submit={handleUserModalSubmit}\n        cancel={() => setUserModalData(null)}\n      >\n        <Form.Item name=\"githubId\" label=\"User\" rules={[{ required: true, message: 'Please select an user' }]}>\n          <UserSearch keyField=\"githubId\" searchFn={loadUsers} />\n        </Form.Item>\n\n        <Row gutter={24}>\n          <Col span={8}>\n            <Form.Item name=\"isManager\" valuePropName=\"checked\">\n              <Checkbox>Manager</Checkbox>\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item name=\"isSupervisor\" valuePropName=\"checked\">\n              <Checkbox>Supervisor</Checkbox>\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item name=\"isDementor\" valuePropName=\"checked\">\n              <Checkbox>Dementor</Checkbox>\n            </Form.Item>\n          </Col>\n          <Col span={8}>\n            <Form.Item name=\"isActivist\" valuePropName=\"checked\">\n              <Checkbox>Activist</Checkbox>\n            </Form.Item>\n          </Col>\n        </Row>\n      </ModalForm>\n    );\n  };\n\n  const GroupModal = ({ modalData }: { modalData: UserGroupDto[] }) => {\n    const [selectedGroups, setSelectedGroups] = useState<UserGroupDto[] | null>(null);\n    return (\n      groupModalData && (\n        <Modal\n          width={800}\n          style={{ top: 20 }}\n          open={true}\n          onCancel={() => setGroupModalData(null)}\n          onOk={() => handleGroupModalSubmit(selectedGroups!)}\n          okButtonProps={{ disabled: !selectedGroups }}\n        >\n          <Table\n            rowSelection={{\n              onChange: (_: React.Key[], selectedRows: any[]) => {\n                setSelectedGroups(selectedRows);\n              },\n              type: 'checkbox',\n            }}\n            columns={[\n              {\n                title: 'Name',\n                dataIndex: 'name',\n              },\n              {\n                title: 'Users',\n                dataIndex: 'users',\n                render: (_: any, record: UserGroupDto) => (\n                  <>\n                    {record.users.map(user => (\n                      <Tag key={user.id} style={{ padding: 3, margin: 3 }}>\n                        <GithubAvatar size={24} githubId={user.githubId} />\n                        &nbsp;{user.name} ({user.githubId})\n                      </Tag>\n                    ))}\n                  </>\n                ),\n              },\n              {\n                title: 'Roles',\n                dataIndex: 'roles',\n                render: (_: any, record: UserGroupDto) => (\n                  <>\n                    {record.roles.map(role => (\n                      <Tag key={role} style={{ padding: 3, margin: 3 }} color={rolesColors[role]}>\n                        {role}\n                      </Tag>\n                    ))}\n                  </>\n                ),\n              },\n            ]}\n            dataSource={modalData}\n            rowKey=\"id\"\n            pagination={false}\n          />\n        </Modal>\n      )\n    );\n  };\n\n  return (\n    <AdminPageLayout loading={loading} courses={courses} showCourseName>\n      {session.isAdmin && (\n        <Button type=\"primary\" onClick={handleAddGroup}>\n          Add Group\n        </Button>\n      )}\n      <Button type=\"link\" onClick={handleAddUser}>\n        Add User\n      </Button>\n      <Table\n        rowKey=\"id\"\n        pagination={{ pageSize: 100 }}\n        size=\"small\"\n        dataSource={courseUsers}\n        columns={getColumns(handleEditItem)}\n      />\n      {renderUserModal(userModalData)}\n      <GroupModal modalData={groupModalData!} />\n    </AdminPageLayout>\n  );\n}\n\nfunction getColumns(handleEditItem: any) {\n  return [\n    { title: 'User Id', dataIndex: 'id' },\n    {\n      title: 'User',\n      dataIndex: 'name',\n      render: (_: any, record: any) => <PersonCell value={record} />,\n      ...getColumnSearchProps(['githubId', 'name']),\n    },\n\n    {\n      title: 'Manager',\n      dataIndex: 'isManager',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Supervisor',\n      dataIndex: 'isSupervisor',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Dementor',\n      dataIndex: 'isDementor',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Activist',\n      dataIndex: 'isActivist',\n      render: boolIconRenderer,\n    },\n    {\n      title: 'Actions',\n      dataIndex: 'actions',\n      render: (_: any, record: CourseUserDto) => (\n        <>\n          <a onClick={() => handleEditItem(record)}>Edit</a>{' '}\n        </>\n      ),\n    },\n  ];\n}\n\nfunction createRecord(values: any) {\n  const data = {\n    isManager: values.isManager,\n    isSupervisor: values.isSupervisor,\n    isDementor: values.isDementor,\n    isActivist: values.isActivist,\n  };\n  return data;\n}\n\nfunction createRecords(groups: UserGroupDto[]) {\n  const data = groups.reduce(\n    (users, group) => {\n      group.users.forEach(({ id }) => {\n        const user = users[id] ?? {\n          isManager: false,\n          isSupervisor: false,\n          isDementor: false,\n          isActivist: false,\n        };\n\n        users[id] = user;\n        users[id].isManager = user.isManager || group.roles.includes(CourseRole.Manager) || false;\n        users[id].isSupervisor = user.isSupervisor || group.roles.includes(CourseRole.Supervisor);\n        users[id].isDementor = user.isDementor || group.roles.includes(CourseRole.Dementor);\n        users[id].isActivist = user.isActivist || group.roles.includes(CourseRole.Activist);\n      });\n      return users;\n    },\n    {} as Pick<CourseUserDto, 'isManager' | 'isSupervisor' | 'isDementor' | 'isActivist'>[],\n  );\n  return Object.entries(data).map(([id, roles]) => ({ ...roles, userId: Number(id) }));\n}\n\nfunction getInitialValues(modalData: Partial<CourseUserDto> | UserGroupDto[]) {\n  return modalData;\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/interview/[type]/feedback.tsx",
    "content": "import { useRequest } from 'ahooks';\nimport { TaskDtoTypeEnum } from '@client/api';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { getInterviewData, getStageInterviewData, PageProps } from '@client/modules/Interviews/data';\nimport { StageInterviewFeedback } from '@client/modules/Interviews/pages/StageInterviewFeedback';\nimport { InterviewFeedback } from '@client/modules/Interviews/pages/InterviewFeedback';\nimport { useRouter } from 'next/router';\nimport { CourseRole } from '@client/services/models';\n\nexport default function (props: PageProps) {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Mentor]} course={props.course}>\n      <FeedbackWrapper />\n    </SessionProvider>\n  );\n}\n\nfunction FeedbackWrapper() {\n  const { course } = useActiveCourseContext();\n  const { query } = useRouter();\n\n  const type = query.type as string;\n\n  const { data } = useRequest(async () => {\n    const data = await (type === TaskDtoTypeEnum.StageInterview\n      ? getStageInterviewData({ courseId: course.id, query })\n      : getInterviewData({ courseId: course.id, query }));\n    return data;\n  });\n\n  if (!data) {\n    return null;\n  }\n\n  const props: PageProps = {\n    ...data,\n    course,\n  };\n\n  return props.type === TaskDtoTypeEnum.StageInterview ? (\n    <StageInterviewFeedback {...props} />\n  ) : (\n    <InterviewFeedback {...props} />\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/mentor/confirm.tsx",
    "content": "import { Button, Col, Form, message, Result, Row, Typography } from 'antd';\nimport { AuthApi, CourseDto as Course, DiscordServersApi } from '@client/api';\nimport { PageLayout, PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { useRouter } from 'next/router';\nimport { useContext, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { CoursesService } from '@client/services/courses';\nimport { MentorRegistryService, MentorResponse } from '@client/services/mentorRegistry';\nimport { Warning } from '@client/components/Warning';\nimport { MentorOptions } from '@client/components/MentorOptions';\nimport { SessionContext, SessionProvider } from '@client/modules/Course/contexts';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport { useAsyncEffect } from 'ahooks';\n\nconst { Link } = Typography;\n\ntype SuccessComponentProps = {\n  courseId: number;\n  discordServerId: number;\n};\n\nconst mentorRegistry = new MentorRegistryService();\nconst discordServer = new DiscordServersApi();\nconst authApi = new AuthApi();\n\nfunction Page() {\n  const session = useContext(SessionContext);\n  const [form] = Form.useForm();\n  const router = useRouter();\n  const [loading, setLoading] = useState(false);\n  const [noAccess, setNoAccess] = useState<boolean | null>(null);\n  const [isPreferredCourse, setIsPreferredCourse] = useState<boolean | null>(null);\n  const [success, setSuccess] = useState(false);\n  const [mentorData, setMentorData] = useState<MentorResponse | null>(null);\n  const [course, setCourse] = useState(null as Course | null);\n\n  const courseService = useMemo(() => {\n    if (!course) {\n      return null;\n    }\n    return new CourseService(course.id);\n  }, [course]);\n\n  const mapMentorData = (\n    mentor: MentorResponse | null,\n    course: Course | null,\n  ): Omit<MentorResponse, 'preferedStudentsLocation'> => {\n    const courseMinStudentsPerMentorValue = course?.minStudentsPerMentor || 0;\n    const shouldUseCourseMinimum = courseMinStudentsPerMentorValue > Number(mentor?.maxStudentsLimit || 0);\n    if (mentor) {\n      return {\n        ...mentor,\n        maxStudentsLimit: shouldUseCourseMinimum ? courseMinStudentsPerMentorValue : mentor.maxStudentsLimit,\n      };\n    }\n    return {\n      maxStudentsLimit: courseMinStudentsPerMentorValue,\n      preferredCourses: [],\n      preselectedCourses: [],\n    };\n  };\n\n  useAsync(async () => {\n    try {\n      setLoading(true);\n      const courseAlias = (router.query?.course as string).toLowerCase();\n      if (courseAlias == null) {\n        setIsPreferredCourse(false);\n        return;\n      }\n      const courses = await new CoursesService().getCourses();\n      const course = courses.find(c => c.alias.toLowerCase() === courseAlias) ?? null;\n      setCourse(course);\n      const mentor = await mentorRegistry.getMentor();\n      const mappedMentorData = mapMentorData(mentor, course);\n      const preferredCourse = course?.id ? mappedMentorData?.preferredCourses?.includes(course?.id) : null;\n      const preselectedCourses = course?.id ? mappedMentorData?.preselectedCourses?.includes(course?.id) : null;\n      setIsPreferredCourse(preferredCourse);\n      if (preselectedCourses === false) {\n        setNoAccess(true);\n        return;\n      }\n      setNoAccess(false);\n      setMentorData(mentor);\n      form.setFieldsValue(mentor);\n    } catch {\n      setNoAccess(null);\n    } finally {\n      setLoading(false);\n    }\n  }, [router.query.course]);\n\n  const handleSubmit = async (values: any) => {\n    if (loading) {\n      return;\n    }\n    try {\n      setLoading(true);\n\n      await courseService?.createMentor(session.githubId, {\n        maxStudentsLimit: values.maxStudentsLimit,\n        preferedStudentsLocation: values.preferedStudentsLocation,\n        students: values.students?.map((s: any) => Number(s.value)) ?? [],\n      });\n\n      await authApi.clearAuthUserSessionCache(session.id);\n      setSuccess(true);\n    } catch {\n      message.error('An error occurred. Please try later.');\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  if (loading || (noAccess === null && isPreferredCourse === null)) {\n    return null;\n  }\n\n  if (course == null) {\n    return <Warning imagePath=\"/svg/err.svg\" imageName=\"Course Not Found\" textMessage=\"Sorry, Course Not Found\" />;\n  }\n\n  const pageProps = {\n    loading,\n    title: 'Confirm Mentorship',\n    githubId: session.githubId,\n    courseName: course.name,\n  };\n\n  if (noAccess && !isPreferredCourse) {\n    const message = (\n      <div>\n        <Link href={`/registry/mentor?course=${router.query?.course}`}>\n          <a>Register as a Mentor for the Course</a>\n        </Link>\n      </div>\n    );\n    return <Warning imagePath=\"/svg/wanted-mentors.svg\" imageName=\"Not registered\" textMessage={message} />;\n  }\n\n  if (noAccess && isPreferredCourse) {\n    const message = (\n      <div>\n        <Row justify=\"center\">\n          <h1 style={{ fontSize: '32px', marginBottom: 15, maxWidth: 600, textAlign: 'center' }}>\n            Thank you for registration as a mentor for Rolling Scopes School\n          </h1>\n        </Row>\n        <Row justify=\"center\">\n          <h2 style={{ fontSize: '19px', marginBottom: 15, maxWidth: 600, textAlign: 'center', fontWeight: 100 }}>\n            <p>\n              Hello, our future mentor, we are very happy to see you in The Rolling Scopes School. But before you start\n              your journey we need to consider your application and submit you to a course.\n            </p>\n            <p style={{ marginBottom: 15 }}>\n              It can take a little time. We will send you another mail with next steps later\n            </p>\n            <p style={{ fontWeight: 500 }}>\n              We really appreciate your interest for school.\n              <br />\n              See you soon.\n            </p>\n          </h2>\n        </Row>\n      </div>\n    );\n    return <Warning imagePath=\"/images/rs-hero.png\" imageName=\"You are RS hero\" textMessage={message} />;\n  }\n\n  if (success) {\n    return (\n      <PageLayout {...pageProps}>\n        <SuccessComponent courseId={course.id} discordServerId={course.discordServerId} />\n      </PageLayout>\n    );\n  }\n\n  return (\n    <PageLayoutSimple {...pageProps}>\n      <Typography.Paragraph>\n        We kindly ask you confirm your desire to be mentor in {course.name} course. Just in case, you can change your\n        preference below and select student which you want to mentor\n      </Typography.Paragraph>\n      <MentorOptions form={form} mentorData={mentorData} handleSubmit={handleSubmit} course={course} />\n    </PageLayoutSimple>\n  );\n}\n\nconst SuccessComponent = ({ courseId, discordServerId }: SuccessComponentProps) => {\n  const [mentorsChat, setMentorsChat] = useState<string | null>(null);\n\n  useAsyncEffect(async () => {\n    const telegramInviteLinkResponse = await discordServer.getInviteLinkByDiscordServerId(courseId, discordServerId);\n    setMentorsChat(telegramInviteLinkResponse.data);\n  }, [discordServerId]);\n\n  if (!mentorsChat) return <LoadingScreen show={!mentorsChat} />;\n\n  const titleCmp = (\n    <Row gutter={24} justify=\"center\">\n      <Col xs={18} sm={16} md={12}>\n        <p>Thanks for the confirmation!</p>\n        <p>You are mentor now!</p>\n        <Typography.Paragraph type=\"secondary\">\n          Join our <a href={mentorsChat}>RSSchool Mentors FAQ</a> Telegram group.\n        </Typography.Paragraph>\n        <p>\n          <Button type=\"primary\" size=\"large\" href=\"/\">\n            Go to Home Page\n          </Button>\n        </p>\n      </Col>\n    </Row>\n  );\n  return <Result status=\"success\" title={titleCmp} />;\n};\n\nexport default function () {\n  return (\n    <SessionProvider>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/mentor/dashboard.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { MentorDashboard } from '@client/modules/Mentor/components';\nimport { CourseRole } from '@client/services/models';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Mentor]}>\n      <MentorDashboard />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/mentor/expel-student.tsx",
    "content": "import { Button, Form, Input, Radio, Typography } from 'antd';\nimport { CoursesApi, MentorsApi, MentorStudentDto } from '@client/api';\nimport { PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { getMentorId, isMentor } from '@client/domain/user';\nimport { useMessage } from '@client/hooks';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useContext, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { CourseRole } from '@client/services/models';\n\ntype ActionOnStudent = 'expel' | 'unassign' | 'self-study';\n\nconst coursesApi = new CoursesApi();\n\nfunction Page() {\n  const { message } = useMessage();\n  const { course } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n  const courseId = course.id;\n\n  const userGithubId = session.githubId;\n\n  const [form] = Form.useForm();\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const [loading, setLoading] = useState(false);\n  const [students, setStudents] = useState<Pick<MentorStudentDto, 'id' | 'githubId' | 'name'>[]>([]);\n  const [action, setAction] = useState<ActionOnStudent>('expel');\n\n  useAsync(async () => {\n    if (isMentor(session, courseId)) {\n      const mentorId = getMentorId(session, courseId);\n      if (!mentorId) {\n        return null;\n      }\n      const students = await new MentorsApi().getMentorStudents(mentorId);\n      const activeStudents = students.data.filter(student => student.active);\n      setStudents(activeStudents);\n    } else {\n      const student = await courseService.getStudentSummary(userGithubId);\n      if (student.isActive) {\n        setStudents([\n          Object.assign(student, {\n            id: session.id,\n            githubId: session.githubId,\n            name: session.githubId,\n          }),\n        ]);\n      }\n    }\n  }, [courseId]);\n\n  const expelStudent = async (githubId: string, comment: string) =>\n    githubId === userGithubId\n      ? await coursesApi.leaveCourse(courseId, { otherComment: comment })\n      : await courseService.expelStudent(githubId, comment);\n\n  const unassignStudent = async (githubId: string, comment: string) => {\n    const data = { mentorGithuId: null, unassigningComment: comment };\n    await courseService.unassignStudentFromMentor(githubId, data);\n  };\n\n  const setSelfStudy = async (githubId: string, comment: string) =>\n    githubId === userGithubId\n      ? await courseService.selfSetSelfStudy(githubId, comment)\n      : await courseService.setSelfStudy(githubId, comment);\n\n  const handleSubmit = async (values: any) => {\n    if (!values.githubId || loading) return;\n    try {\n      setLoading(true);\n      switch (action) {\n        case 'expel':\n          await expelStudent(values.githubId, values.comment);\n          break;\n        case 'unassign':\n          await unassignStudent(values.githubId, values.comment);\n          break;\n        case 'self-study':\n          await setSelfStudy(values.githubId, values.comment);\n          break;\n        default:\n          throw new Error(`Wrong action on student type: ${action}`);\n      }\n      const activeStudents = students.filter(s => s.githubId !== values.githubId);\n      setStudents(activeStudents);\n      form.resetFields();\n      message.success(actionMessages[action].success);\n    } catch {\n      message.error('An error occured. Please try later.');\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const noData = !students.length;\n\n  const actionMessages: {\n    [key in ActionOnStudent]: {\n      [key: string]: string;\n    };\n  } = {\n    expel: {\n      description: 'Selected student will be expelled from this course',\n      reasonPhrase: 'Reason for expelling:',\n      success: 'The student has been expelled',\n    },\n    unassign: {\n      description:\n        'Selected student will no longer be your mentee. They will be put to wait list, so another mentor could take them',\n      reasonPhrase: 'Reason for unassigning:',\n      success: 'The student has been unassigned',\n    },\n    'self-study': {\n      description: 'Selected student will no longer be your mentee and can continue course without a mentor',\n      reasonPhrase: 'Reason for unassigning:',\n      success: 'The student has been unassigned',\n    },\n  };\n\n  return (\n    <PageLayoutSimple loading={loading} title=\"Expel/Unassign Student\" showCourseName>\n      <Form form={form} onFinish={handleSubmit} layout=\"vertical\">\n        <Form.Item initialValue={action} name=\"action\" label=\"Action\">\n          <Radio.Group onChange={e => setAction(e.target.value)}>\n            <Radio value=\"expel\">Expel</Radio>\n            <Radio value=\"unassign\">Unassign</Radio>\n            <Radio value=\"self-study\">Self-study</Radio>\n          </Radio.Group>\n        </Form.Item>\n        <Typography.Paragraph type=\"warning\">{actionMessages[action].description}</Typography.Paragraph>\n        <Form.Item name=\"githubId\" label=\"Student\" rules={[{ required: true, message: 'Please select a student' }]}>\n          <UserSearch\n            keyField=\"githubId\"\n            defaultValues={students}\n            disabled={noData}\n            placeholder={noData ? 'No Students' : undefined}\n          />\n        </Form.Item>\n        <Form.Item\n          name=\"comment\"\n          label={actionMessages[action].reasonPhrase}\n          rules={[{ required: true, message: 'Please give us a couple words why you are expelling the student' }]}\n        >\n          <Input.TextArea rows={5} />\n        </Form.Item>\n        <Button size=\"large\" type=\"primary\" htmlType=\"submit\">\n          Submit\n        </Button>\n      </Form>\n    </PageLayoutSimple>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Mentor, CourseRole.Manager]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/mentor/feedback/index.tsx",
    "content": "import { CourseRole } from '@client/services/models';\nimport { SessionProvider } from '@client/modules/Course/contexts';\nimport { StudentFeedback } from '@client/modules/Mentor/pages/StudentFeedback';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Mentor]}>\n      <StudentFeedback />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/mentor/interview-technical-screening.tsx",
    "content": "import { Button, Divider, Form, Input, InputNumber, Radio, Rate, Space, Typography } from 'antd';\nimport { AxiosError } from 'axios';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { useLoading } from '@client/components/useLoading';\nimport { useMessage } from '@client/hooks';\nimport get from 'lodash/get';\nimport keys from 'lodash/keys';\nimport set from 'lodash/set';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { ChangeEvent, useContext, useEffect, useMemo, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { CourseRole, StudentBasic } from '@client/services/models';\n\ntype FormValues = typeof defaultInitialValues;\n\ntype HandleChangeValue = (skillName: string) => (value: any) => void;\n\nconst SKILLS_LEVELS = [\n  `Doesn't know`,\n  `Poor knowledge (almost doesn't know)`,\n  'Knows something (with tips)',\n  'Good knowledge (makes not critical mistakes)',\n  'Great knowledge',\n];\n\nconst CODING_LEVELS = [\n  `Isn't able to coding`,\n  `Poor coding ability (almost isn't able to)`,\n  'Can code with tips',\n  'Good coding ability (makes not critical mistakes)',\n  'Great coding ability',\n];\n\nconst ENGLISH_LEVELS = ['A0', 'A1', 'A1+', 'A2', 'A2+', 'B1', 'B1+', 'B2', 'B2+', 'C1', 'C1+', 'C2'];\n\nconst defaultInitialValues = {\n  githubId: null,\n  'skills-htmlCss-level': 0,\n  'skills-dataStructures-array': 0,\n  'skills-dataStructures-list': 0,\n  'skills-dataStructures-stack': 0,\n  'skills-dataStructures-queue': 0,\n  'skills-dataStructures-tree': 0,\n  'skills-dataStructures-hashTable': 0,\n  'skills-dataStructures-heap': 0,\n  'skills-common-binaryNumber': 0,\n  'skills-common-oop': 0,\n  'skills-common-bigONotation': 0,\n  'skills-common-sortingAndSearchAlgorithms': 0,\n  'skills-comment': '',\n  'programmingTask-task': '',\n  'programmingTask-codeWritingLevel': 0,\n  'programmingTask-resolved': null,\n  'programmingTask-comment': '',\n  'english-levelStudentOpinion': 0,\n  'english-levelMentorOpinion': 0,\n  'english-whereAndWhenLearned': '',\n  'english-comment': '',\n  'resume-verdict': null,\n  'resume-comment': '',\n  'resume-score': 0,\n};\n\nconst SKILLS = [\n  {\n    name: 'htmlCss',\n    label: 'HTML/CSS',\n    comment: `Position and display attributes' values, tags, weight of selectors, pseudo-classes and elements, box model, relative and absolute values, em vs rem, semantic, semantic tags, etc.`,\n    skills: [\n      {\n        name: 'level',\n        label: 'Average level',\n      },\n    ],\n  },\n  {\n    name: 'dataStructures',\n    label: 'Data structures',\n    comment: `Representation in computer memory. Operations' complexity. Difference between list and array, or between stack and queue.`,\n    skills: [\n      {\n        name: 'array',\n        label: 'Array',\n      },\n      {\n        name: 'list',\n        label: 'List',\n      },\n      {\n        name: 'stack',\n        label: 'Stack',\n      },\n      {\n        name: 'queue',\n        label: 'Queue',\n      },\n      {\n        name: 'tree',\n        label: 'Tree',\n      },\n      {\n        name: 'hashTable',\n        label: 'Hash table',\n      },\n      {\n        name: 'heap',\n        label: 'Heap',\n      },\n    ],\n  },\n  {\n    name: 'common',\n    label: 'Common of CS / Programming',\n    comment: null,\n    skills: [\n      {\n        name: 'oop',\n        label: 'OOP (Encapsulation, Polymorphism, and Inheritance)',\n      },\n      {\n        name: 'binaryNumber',\n        label: 'Binary number',\n      },\n      {\n        name: 'bigONotation',\n        label: 'Big O notation',\n      },\n      {\n        name: 'sortingAndSearchAlgorithms',\n        label: 'Sorting and search algorithms (Binary search, Bubble sort, Quick sort, etc.)',\n      },\n    ],\n  },\n];\n\nconst renderSkills = (handleSkillChange: HandleChangeValue) => (\n  <>\n    <Typography.Title level={3}>Skills</Typography.Title>\n    {SKILLS.map(category => (\n      <div key={`skills.${category.name}`}>\n        <Typography.Title level={4} key={category.name}>\n          {category.label}\n        </Typography.Title>\n        {category.comment && <Typography.Paragraph>{category.comment}</Typography.Paragraph>}\n        {category.skills.map((skill: { name: string; label: string }) => (\n          <Form.Item name={`skills-${category.name}-${skill.name}`} label={skill.label} key={skill.name}>\n            <Rate onChange={handleSkillChange(`skills-${category.name}-${skill.name}`)} tooltips={SKILLS_LEVELS} />\n          </Form.Item>\n        ))}\n      </div>\n    ))}\n    <Form.Item label=\"Comment\" name=\"skills-comment\" style={{ marginBottom: 40 }}>\n      <Input.TextArea\n        onChange={handleSkillChange('skills-comment')}\n        placeholder=\"Comments about student's skills\"\n        autoSize={{ minRows: 3, maxRows: 5 }}\n      />\n    </Form.Item>\n  </>\n);\n\nconst renderProgrammingTask = (handleSkillChange: HandleChangeValue) => (\n  <>\n    <Typography.Title level={3}>Code writing level</Typography.Title>\n    <Form.Item label=\"What tasks did the student have to solve?\" name=\"programmingTask-task\">\n      <Input.TextArea\n        onChange={handleSkillChange('programmingTask-task')}\n        placeholder=\"aaabbcc = 3a2b2c\"\n        autoSize={{ minRows: 3, maxRows: 5 }}\n      />\n    </Form.Item>\n    <Form.Item label=\"Has the student solved the task(s)?\" name=\"programmingTask-resolved\">\n      <Radio.Group onChange={handleSkillChange('programmingTask-resolved')}>\n        <Space direction=\"vertical\">\n          <Radio value={1}>Yes, he/she has</Radio>\n          <Radio value={2}>Yes, he/she has, but with tips</Radio>\n          <Radio value={3}>No, he/she hasn't</Radio>\n        </Space>\n      </Radio.Group>\n    </Form.Item>\n    <Form.Item label=\"Code writing confidence\" name=\"programmingTask-codeWritingLevel\">\n      <Rate onChange={handleSkillChange('programmingTask-codeWritingLevel')} tooltips={CODING_LEVELS} />\n    </Form.Item>\n    <Form.Item label=\"Comment\" name=\"programmingTask-comment\" style={{ marginBottom: 40 }}>\n      <Input.TextArea\n        onChange={handleSkillChange('programmingTask-comment')}\n        placeholder=\"Comments about student's code writing level\"\n        autoSize={{ minRows: 3, maxRows: 5 }}\n      />\n    </Form.Item>\n  </>\n);\n\nconst renderEnglishLevel = (handleSkillChange: HandleChangeValue) => (\n  <>\n    <Typography.Title level={3}>English level</Typography.Title>\n    <Form.Item label=\"English level by student's opinion\" name=\"english-levelStudentOpinion\">\n      <Rate onChange={handleSkillChange('english-levelStudentOpinion')} tooltips={ENGLISH_LEVELS} count={12} />\n    </Form.Item>\n    <Form.Item label=\"Where and when learned English?\" name=\"english-whereAndWhenLearned\">\n      <Input\n        placeholder=\"Example: Self-education / International House 2019\"\n        onChange={handleSkillChange('english-whereAndWhenLearned')}\n      />\n    </Form.Item>\n    <Typography.Paragraph>\n      Ask the student to tell about himself, hobby, favorite book or film, etc. (2-3 minutes). Use{' '}\n      <a\n        target=\"_blank\"\n        href=\"https://www.bellenglish.com/sites/default/files/public/uploads/General/LanguageLevels.png\"\n      >\n        this chart\n      </a>{' '}\n      in order to define the estimated English level\n    </Typography.Paragraph>\n    <Form.Item label=\"English level by mentor's opinion\" name=\"english-levelMentorOpinion\">\n      <Rate onChange={handleSkillChange('english-levelMentorOpinion')} tooltips={ENGLISH_LEVELS} count={12} />\n    </Form.Item>\n    <Form.Item label=\"Comment\" name=\"english-comment\" style={{ marginBottom: 40 }}>\n      <Input.TextArea\n        onChange={handleSkillChange('english-comment')}\n        placeholder=\"Comments / impressions about student's english level\"\n        autoSize={{ minRows: 3, maxRows: 5 }}\n      />\n    </Form.Item>\n  </>\n);\n\nconst renderResume = (handleSkillChange: HandleChangeValue) => (\n  <>\n    <Typography.Title level={3}>Resume</Typography.Title>\n    <Form.Item label=\"Do you want take the student in your group and be his/her mentor?\" name=\"resume-verdict\" required>\n      <Radio.Group onChange={handleSkillChange('resume-verdict')}>\n        <Space direction=\"vertical\">\n          <Radio value={'yes'}>Yes, I do.</Radio>\n          <Radio value={'no'}>No, I do not.</Radio>\n          <Radio value={'noButGoodCandidate'}>No, I do not, but he/she is a good candidate.</Radio>\n          <Radio value={'didNotDecideYet'}>I didn't decide yet. I'll submit the feedback later.</Radio>\n        </Space>\n      </Radio.Group>\n    </Form.Item>\n    <Form.Item name=\"resume-score\" label=\"Score (Max 50 points)\">\n      <InputNumber min={0} max={50} />\n    </Form.Item>\n    <Form.Item\n      label=\"Comment\"\n      name=\"resume-comment\"\n      rules={[{ required: true, message: 'Please choose your verdict' }]}\n      style={{ marginBottom: '20px' }}\n    >\n      <Input.TextArea\n        onChange={handleSkillChange('resume-comment')}\n        placeholder=\"Resume\"\n        autoSize={{ minRows: 3, maxRows: 5 }}\n      />\n    </Form.Item>\n  </>\n);\n\nfunction Page() {\n  const { message } = useMessage();\n  const { course } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n  const courseId = course.id;\n  const [githubId] = useState(window ? new URLSearchParams(window.location.search).get('githubId') : null);\n\n  const [form] = Form.useForm();\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const [loading, withLoading] = useLoading(false);\n  const [studentGitHubId, setStudentGitHubId] = useState<string>();\n  const [students, setStudents] = useState([] as StudentBasic[]);\n  const [interviews, setInterviews] = useState([] as { id: number; completed: boolean; student: StudentBasic }[]);\n\n  const [resume, setResume] = useState(defaultInitialValues);\n\n  const loadData = async () => {\n    const interviews = await courseService.getInterviewerStageInterviews(session.githubId);\n    setStudents(interviews.filter(i => !i.completed).map(i => i.student));\n    setStudents(students);\n    setInterviews(interviews);\n  };\n\n  useAsync(withLoading(loadData), []);\n\n  useEffect(() => {\n    form.setFieldsValue({ githubId });\n  }, [githubId]);\n\n  useEffect(() => {\n    if (interviews?.length && githubId) {\n      handleStudentSelect(githubId);\n    }\n  }, [interviews, githubId]);\n\n  const handleStudentSelect = async (githubId: string) => {\n    setStudentGitHubId(githubId);\n\n    const interview = interviews.find(i => i.student.githubId === githubId);\n    if (interview != null) {\n      const feedback = await courseService.getStageInterviewFeedback(interview.id);\n      const deserializeFeedback = deserializeFromJson(feedback);\n      setResume(deserializeFeedback);\n      form.setFieldsValue(deserializeFeedback);\n    }\n  };\n\n  const calculateResult = (result: any) => {\n    const { skills, programmingTask } = result;\n    const commonSkills = Object.values(skills.common).filter(Boolean) as number[];\n    const dataStructuresSkills = Object.values(skills.dataStructures).filter(Boolean) as number[];\n    const htmlCss = skills.htmlCss.level;\n\n    const common = commonSkills.length ? commonSkills.reduce((acc, cur) => acc + cur, 0) / commonSkills.length : 0;\n    const dataStructures = dataStructuresSkills.length\n      ? dataStructuresSkills.reduce((acc, cur) => acc + cur, 0) / dataStructuresSkills.length\n      : 0;\n\n    const ratingsCount = 4;\n    const ratings = [htmlCss, common, dataStructures, programmingTask.codeWritingLevel].filter(Boolean) as number[];\n\n    const rating = ratings.reduce((sum, num) => sum + num, 0) / ratingsCount;\n    return Math.floor(rating * 10);\n  };\n\n  const handleSubmit = withLoading(async (values: FormValues) => {\n    if (!githubId || !values['resume-verdict'] || loading) {\n      return;\n    }\n    const interview = interviews.find(i => i.student.githubId === githubId);\n    if (interview == null) {\n      return;\n    }\n\n    try {\n      const json = serializeToJson(values);\n      await courseService.postStageInterviewFeedback(interview?.id, {\n        json,\n        githubId: githubId ?? '',\n        isCompleted: values['resume-verdict'] !== 'didNotDecideYet',\n        isGoodCandidate: values['resume-verdict'] === 'noButGoodCandidate',\n        decision: values['resume-verdict'],\n      });\n      message.success('You interview feedback has been submitted. Thank you.');\n      form.resetFields();\n    } catch (e) {\n      const error = e as AxiosError<{ data?: { message?: string } }>;\n      const errorMessage = error?.response?.data?.data?.message ?? 'An error occurred. Please try later.';\n      message.error(errorMessage);\n    }\n  });\n\n  const handleTotalScoreChange = (skillName: string) => (value: ChangeEvent<HTMLInputElement> | number) => {\n    const comment = (value as ChangeEvent<HTMLInputElement>)?.target?.value;\n    let newResult;\n    if (comment !== undefined) {\n      newResult = { ...resume, [skillName]: comment };\n    } else {\n      const result = calculateResult(serializeToJson({ ...resume, [skillName]: value }));\n      newResult = { ...resume, [skillName]: value, 'resume-score': result };\n    }\n    setResume(newResult);\n    form.setFieldsValue({ ...newResult, githubId: studentGitHubId });\n  };\n\n  return (\n    <PageLayoutSimple loading={loading} title=\"Technical Screening Feedback\" showCourseName>\n      <Form\n        form={form}\n        initialValues={resume}\n        layout=\"vertical\"\n        onFinish={handleSubmit}\n        onFinishFailed={({ errorFields: [errorField] }) => form.scrollToField(errorField?.name)}\n      >\n        <Space align=\"baseline\">\n          <Typography.Title level={4}>Student: </Typography.Title>{' '}\n          <GithubAvatar githubId={githubId ?? undefined} size={24} />\n          <Typography.Link target=\"_blank\" href={`/profile?githubId=${githubId}`}>\n            <Typography.Title level={4}>\n              <Typography.Link>{githubId}</Typography.Link>\n            </Typography.Title>\n          </Typography.Link>\n        </Space>\n        {renderSkills(handleTotalScoreChange)}\n        <Divider dashed />\n        {renderProgrammingTask(handleTotalScoreChange)}\n        <Divider dashed style={{ height: 2 }} />\n        {renderEnglishLevel(handleTotalScoreChange)}\n        <Divider dashed />\n        {renderResume(handleTotalScoreChange)}\n        <Button type=\"primary\" htmlType=\"submit\">\n          Submit\n        </Button>\n      </Form>\n    </PageLayoutSimple>\n  );\n}\n\nfunction serializeToJson(values: FormValues): any {\n  return keys(values)\n    .filter(v => v !== 'githubId')\n    .reduce((acc, key) => {\n      return set(acc, key.split('-').join('.'), values[key as keyof FormValues]);\n    }, {});\n}\n\nfunction deserializeFromJson(json: Record<string, unknown>): any {\n  return keys(defaultInitialValues)\n    .filter(key => key !== 'githubId')\n    .reduce((acc, key) => {\n      (acc as any)[key] = get(json, key.split('-'));\n      return acc;\n    }, {} as FormValues);\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Mentor, CourseRole.Manager]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/mentor/interview-wait-list.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { InterviewWaitingList } from '@client/modules/Mentor/pages/InterviewWaitingList';\nimport { CourseRole } from '@client/services/models';\n\nfunction Page() {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Mentor, CourseRole.Manager]}>\n      <InterviewWaitingList />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/mentor/interviews.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { Interviews } from '@client/modules/Mentor/pages/Interviews';\n\nexport default function () {\n  return (\n    <SessionProvider>\n      <Interviews />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/mentor/students.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { Students } from '@client/modules/Mentor/pages/Students';\nimport { CourseRole } from '@client/services/models';\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Mentor]}>\n      <Students />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/schedule.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { SchedulePage } from '@client/modules/Schedule/pages/SchedulePage';\n\nexport default function () {\n  return (\n    <SessionProvider>\n      <SchedulePage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/score.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { ScorePage } from '@client/modules/Score/pages/ScorePage';\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <ScorePage />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/stats.tsx",
    "content": "import { CourseStatistics } from '@client/modules/CourseStatistics';\nimport { SessionProvider } from '@client/modules/Course/contexts';\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <CourseStatistics />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/student/auto-test/index.tsx",
    "content": "import { AutoTests } from '@client/modules/AutoTest/pages';\nimport { SessionProvider } from '@client/modules/Course/contexts';\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <AutoTests />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/student/auto-test/task.tsx",
    "content": "import { Task } from '@client/modules/AutoTest/pages';\nimport { SessionProvider } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\n\nfunction Page() {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Student]}>\n      <Task />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/student/cross-check-review.tsx",
    "content": "import { EyeFilled, EyeInvisibleFilled } from '@ant-design/icons';\nimport { Button, Checkbox, Col, Form, Modal, Row, Typography } from 'antd';\nimport {\n  CrossCheckCriteriaDataDto,\n  CrossCheckMessageDtoRoleEnum,\n  CrossCheckSolutionReviewDto,\n  CrossCheckStatusEnum,\n  TasksCriteriaApi,\n} from '@client/api';\nimport { CourseTaskSelect } from '@client/shared/components/Forms';\nimport MarkdownInput from '@client/shared/components/Forms/MarkdownInput';\nimport { markdownLabel } from '@client/shared/components/Forms/PreparedComment';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { useLoading } from '@client/components/useLoading';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport {\n  AssignmentLink,\n  CrossCheckAssignmentLink,\n} from '@client/modules/CrossCheck/components/CrossCheckAssignmentLink';\nimport { CrossCheckCriteriaForm } from '@client/modules/CrossCheck/components/CrossCheckCriteriaForm';\nimport { CrossCheckHistory } from '@client/modules/CrossCheck/components/CrossCheckHistory';\nimport { TaskType } from '@client/modules/CrossCheck/constants';\nimport { useRouter } from 'next/router';\nimport { useContext, useEffect, useMemo, useState } from 'react';\nimport { useAsync, useLocalStorage } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { CourseRole } from '@client/services/models';\nimport { getQueryString } from '@client/shared/utils/queryParams-utils';\nimport { useMessage } from '@client/hooks';\n\nenum LocalStorage {\n  IsUsernameVisible = 'crossCheckIsUsernameVisible',\n}\n\nconst { Text } = Typography;\n\nconst colSizes = { xs: 24, sm: 18, md: 12, lg: 12, xl: 10 };\n\nconst criteriaApi = new TasksCriteriaApi();\n\nfunction Page() {\n  const { message } = useMessage();\n  const { course } = useActiveCourseContext();\n  const session = useContext(SessionContext);\n  const router = useRouter();\n  const queryTaskId = router.query.taskId ? +router.query.taskId : null;\n  const queryGithubId = router.query.githubId ?? null;\n  const [form] = Form.useForm();\n\n  const [loading, withLoading] = useLoading(false);\n  const [modal, contextHolder] = Modal.useModal();\n  const [courseTaskId, setCourseTaskId] = useState<number | null>(queryTaskId);\n  const [criteriaId, setCriteriaId] = useState<number | null>(null);\n  const [githubId, setGithubId] = useState<string | null>(null);\n  const [assignments, setAssignments] = useState<AssignmentLink[]>([]);\n  const [submissionDisabled, setSubmissionDisabled] = useState<boolean>(true);\n  const [historicalCommentSelected, setHistoricalCommentSelected] = useState<string>(form.getFieldValue('comment'));\n  const [isUsernameVisible = false, setIsUsernameVisible] = useLocalStorage<boolean>(LocalStorage.IsUsernameVisible);\n  const [state, setState] = useState<{ loading: boolean; data: CrossCheckSolutionReviewDto[] }>({\n    loading: false,\n    data: [],\n  });\n\n  const [criteriaData, setCriteriaData] = useState<CrossCheckCriteriaDataDto[]>([]);\n  const [score, setScore] = useState(0);\n  const [isSkipped, setIsSkipped] = useState(false);\n\n  const courseService = useMemo(() => new CourseService(course.id), [course.id]);\n\n  const { value: courseTasks = [] } = useAsync(() => courseService.getCourseCrossCheckTasks(), [course.id]);\n\n  const loadStudentScoreHistory = async (githubId: string) => {\n    if (!criteriaId || !courseTaskId) return;\n\n    setState({ loading: true, data: [] });\n\n    const [{ data: taskCriteriaData }, result] = await Promise.all([\n      criteriaApi.getTaskCriteria(criteriaId),\n      courseService.getTaskSolutionResult(githubId, courseTaskId),\n    ]);\n\n    setCriteriaData(taskCriteriaData.criteria ?? []);\n    form.resetFields(['comment']);\n\n    if (!result) {\n      return setState({ loading: false, data: [] });\n    }\n\n    const sortedData = result.historicalScores.sort((a, b) => b.dateTime - a.dateTime);\n\n    const messages = result.anonymous\n      ? result.messages.map(message => ({\n          ...message,\n          author: message.role === CrossCheckMessageDtoRoleEnum.Reviewer ? null : message.author,\n        }))\n      : result.messages;\n\n    const solutionReviews = sortedData.map(({ dateTime, comment, score, anonymous, criteria }, index) => {\n      return {\n        dateTime,\n        comment,\n        score,\n        criteria,\n        id: result.id,\n        author: !anonymous ? result.author : null,\n        messages: index === 0 ? messages : [],\n      };\n    });\n\n    const [activeSolutionReview] = solutionReviews;\n\n    if (activeSolutionReview) {\n      form.setFieldValue('comment', activeSolutionReview.comment.slice(markdownLabel.length));\n      setScore(activeSolutionReview.score);\n      if (activeSolutionReview.criteria) {\n        setCriteriaData(activeSolutionReview.criteria);\n      }\n    }\n    setState({ loading: false, data: solutionReviews ?? [] });\n  };\n\n  const checkCriteriaWarning = () =>\n    modal.confirm({\n      okText: 'Back to review',\n      content: <Text>You have not checked all the criteria and leave comments</Text>,\n    });\n\n  useEffect(() => {\n    setup();\n\n    async function setup() {\n      if (queryTaskId && courseTasks.length) {\n        await handleTaskChange(queryTaskId);\n        if (queryGithubId) {\n          await handleStudentChange(queryGithubId as string);\n        }\n      }\n    }\n  }, [queryTaskId, courseTasks.length, queryGithubId]);\n\n  useEffect(() => {\n    if (historicalCommentSelected !== '') {\n      form.setFieldsValue({ comment: historicalCommentSelected });\n      setHistoricalCommentSelected('');\n    }\n  }, [historicalCommentSelected]);\n\n  const submitReview = withLoading(async (values: any) => {\n    try {\n      if (values.maxScore != null && values.maxScore < score) {\n        message.error(`The score (${score}) exceeds the maximum score (${values.maxScore}) for the task.`);\n        return;\n      }\n      await courseService.postTaskSolutionResult(values.githubId, values.courseTaskId, {\n        score,\n        comment: markdownLabel + values.comment,\n        anonymous: values.visibleName !== true,\n        comments: [],\n        review: [],\n        criteria: criteriaData,\n      });\n      message.success('The review has been submitted. Thanks!');\n      form.resetFields(['comment', 'githubId', 'visibleName']);\n      setScore(0);\n      setCriteriaData([]);\n      setState({ loading: false, data: [] });\n    } catch {\n      message.error('An error occurred. Please try later.');\n    }\n  });\n\n  const handleSubmit = async (values: any) => {\n    if (!values.githubId || loading) {\n      return;\n    }\n\n    const criteriaToSubmit = criteriaData.map(item => {\n      if (item.type !== TaskType.Title && !item.point) {\n        item.point = 0;\n      }\n      return item;\n    });\n\n    setCriteriaData(criteriaToSubmit);\n\n    const isCriteriaPointsAndCommentsVerified = criteriaToSubmit\n      .filter(criteria => criteria.type.toLowerCase() === TaskType.Subtask)\n      .every(item => {\n        return item.point === item.max ? true : item.textComment && item.textComment.length >= 10;\n      });\n\n    if (!isCriteriaPointsAndCommentsVerified && !isSkipped) {\n      checkCriteriaWarning();\n      return;\n    }\n\n    if (score !== 0) {\n      await submitReview(values);\n    } else {\n      modal.confirm({\n        onOk: () => submitReview(values),\n        okText: 'Yes, submit',\n        cancelText: 'Change score',\n        content: <Text>Are you sure you want to submit a review with a score of 0 points?</Text>,\n      });\n    }\n  };\n\n  function selectTask(value: number) {\n    const query = { ...router.query, taskId: value };\n    const url = `${router.route}${getQueryString(query)}`;\n    router.replace(url);\n  }\n\n  const handleTaskChange = async (value: number) => {\n    const courseTaskId = Number(value);\n    const courseTask = courseTasks.find(t => t.id === courseTaskId);\n    if (courseTask == null) {\n      return;\n    }\n    const assignments = await courseService.getCrossCheckAssignments(session.githubId, courseTask.id);\n    const submissionDisabled = courseTask.crossCheckStatus !== CrossCheckStatusEnum.Distributed;\n    setAssignments(assignments);\n    setCourseTaskId(courseTask.id);\n    setCriteriaId(courseTask.taskId);\n    setSubmissionDisabled(submissionDisabled);\n    setGithubId(null);\n    setState({ loading: false, data: [] });\n    form.resetFields(['comment', 'githubId']);\n  };\n\n  const handleStudentChange = (githubId: string) => {\n    setGithubId(githubId as string);\n    setIsSkipped(false);\n    form.setFieldsValue({ githubId });\n    loadStudentScoreHistory(githubId);\n  };\n\n  const handleUsernameVisibilityChange = () => {\n    setIsUsernameVisible(!isUsernameVisible);\n  };\n\n  const courseTask = courseTasks.find(t => t.id === courseTaskId);\n  const maxScore = courseTask?.maxScore;\n  const assignment = assignments.find(({ student }) => student.githubId === form.getFieldValue('githubId'));\n\n  return (\n    <PageLayout loading={loading} title=\"Cross-Check Review\" showCourseName>\n      {contextHolder}\n      <Row gutter={24}>\n        <Col {...colSizes}>\n          <Form form={form} onFinish={handleSubmit} layout=\"vertical\">\n            <CourseTaskSelect\n              data={courseTasks}\n              groupBy=\"crossCheckDeadline\"\n              onChange={selectTask}\n              defaultValue={courseTaskId}\n            />\n            <Form.Item name=\"githubId\" label=\"Student\" rules={[{ required: true, message: 'Please select a student' }]}>\n              <UserSearch\n                keyField=\"githubId\"\n                onChange={handleStudentChange}\n                disabled={!courseTaskId}\n                defaultValues={assignments.map(({ student }) => student)}\n                value={githubId}\n              />\n              <CrossCheckAssignmentLink assignment={assignment} />\n            </Form.Item>\n            {!!githubId && (\n              <CrossCheckCriteriaForm\n                maxScore={maxScore}\n                score={score}\n                setScore={setScore}\n                criteriaData={criteriaData}\n                setCriteriaData={setCriteriaData}\n                initialData={state.data[0]}\n                isSkipped={isSkipped}\n                setIsSkipped={setIsSkipped}\n              />\n            )}\n            <MarkdownInput historicalCommentSelected={historicalCommentSelected} />\n            <Form.Item name=\"visibleName\" valuePropName=\"checked\" initialValue={isUsernameVisible}>\n              <Checkbox onChange={handleUsernameVisibilityChange}>Make my name visible in feedback</Checkbox>\n            </Form.Item>\n            {isUsernameVisible ? (\n              <Button size=\"large\" type=\"primary\" htmlType=\"submit\" icon={<EyeFilled />} disabled={submissionDisabled}>\n                Submit review as {session.githubId}\n              </Button>\n            ) : (\n              <Button\n                size=\"large\"\n                type=\"primary\"\n                htmlType=\"submit\"\n                icon={<EyeInvisibleFilled />}\n                disabled={submissionDisabled}\n              >\n                Submit review as Reviewer1\n              </Button>\n            )}\n          </Form>\n        </Col>\n        <Col {...colSizes}>\n          <CrossCheckHistory\n            state={state}\n            courseTaskId={courseTaskId}\n            courseId={course.id}\n            sessionId={session.id}\n            sessionGithubId={session.githubId}\n            maxScore={maxScore}\n            setHistoricalCommentSelected={setHistoricalCommentSelected}\n          />\n        </Col>\n      </Row>\n    </PageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Student]}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/student/cross-check-submit.tsx",
    "content": "import { CrossCheckSubmit } from '@client/modules/Course/pages/Student/CrossCheckSubmit';\nimport { SessionProvider } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\n\nfunction Page() {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Student]}>\n      <CrossCheckSubmit />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/student/dashboard.module.css",
    "content": ".masonry {\n  display: flex;\n  margin-left: -24px;\n  width: auto;\n  min-height: 85vh;\n}\n\n.masonryColumn {\n  padding-left: 24px;\n  background-clip: padding-box;\n}\n"
  },
  {
    "path": "client/src/pages/course/student/dashboard.tsx",
    "content": "import { PageLayout } from '@client/shared/components/PageLayout';\nimport { useContext, useMemo } from 'react';\nimport Masonry from 'react-masonry-css';\n\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport styles from './dashboard.module.css';\nimport {\n  AvailableReviewCard,\n  MainStatsCard,\n  MentorCard,\n  NextEventCard,\n  RepositoryCard,\n  TasksStatsCard,\n  useDashboardData,\n} from '@client/modules/StudentDashboard';\nimport { CourseService } from '@client/services/course';\n\nconst gapSize = 24;\n\nfunction Page() {\n  const { githubId } = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n\n  const { fullName, usePrivateRepositories, alias } = course;\n\n  const courseService = useMemo(() => new CourseService(course.id), [course.id]);\n\n  const { data, loading, run } = useDashboardData(course.id, githubId);\n\n  const studentPosition = data?.studentSummary?.rank ?? 0;\n  const maxCourseScore = data?.maxCourseScore ?? 0;\n\n  const { isActive = false, totalScore = 0 } = data?.studentSummary ?? {};\n\n  const cards = [\n    data?.studentSummary && (\n      <MainStatsCard\n        isActive={isActive}\n        totalScore={totalScore}\n        position={studentPosition}\n        maxCourseScore={maxCourseScore}\n        totalStudentsCount={data?.courseStats?.activeStudentsCount ?? 0}\n      />\n    ),\n    data?.tasksByStatus && <TasksStatsCard tasksByStatus={data?.tasksByStatus} courseName={fullName} />,\n    <NextEventCard key=\"next-event-card\" nextEvents={data?.nextEvents ?? []} courseAlias={alias} />,\n    <AvailableReviewCard\n      key=\"available-review-card\"\n      availableReviews={data?.availableReviews ?? []}\n      courseAlias={alias}\n    />,\n    <MentorCard key=\"mentor-card\" courseId={course.id} mentor={data?.studentSummary?.mentor} />,\n    usePrivateRepositories && (\n      <RepositoryCard\n        githubId={githubId}\n        url={data?.studentSummary.repository ?? ''}\n        onSendInviteRepository={courseService.sendInviteRepository.bind(courseService)}\n        onUpdateUrl={() => run()}\n      />\n    ),\n  ].filter(Boolean) as JSX.Element[];\n\n  return (\n    <PageLayout loading={loading} title=\"Student dashboard\" showCourseName>\n      <>\n        <Masonry\n          breakpointCols={{ default: 3, 1180: 2, 800: 1 }}\n          className={styles.masonry as string}\n          columnClassName={styles.masonryColumn as string}\n        >\n          {cards.map((card, idx) => (\n            <div style={{ marginBottom: gapSize }} key={`card-${idx}`}>\n              {card}\n            </div>\n          ))}\n        </Masonry>\n      </>\n    </PageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider allowedRoles={['student']}>\n      <Page />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/student/interviews.tsx",
    "content": "import { message, Row, Spin, Modal } from 'antd';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { useMemo, useState, useContext } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { InterviewDetails } from '@client/domain/interview';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport {\n  CoursesInterviewsApi,\n  CourseTaskDtoTypeEnum,\n  InterviewCommentDto,\n  InterviewDto,\n  TaskDtoTypeEnum,\n} from '@client/api';\nimport { InterviewCard, NoInterviewsAlert } from '@client/modules/Interview/Student';\n\nconst coursesInterviewApi = new CoursesInterviewsApi();\n\nfunction StudentInterviewPage() {\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const courseService = useMemo(() => new CourseService(course.id), [course.id]);\n  const [studentInterviews, setStudentInterviews] = useState<InterviewDetails[]>([]);\n  const [commentsToStudent, setCommentsToStudent] = useState<InterviewCommentDto[]>([]);\n  const [interviews, setInterviews] = useState<InterviewDto[]>([]);\n  const [registeredInterviews, setRegisteredInterviews] = useState<string[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [modal, contextHolder] = Modal.useModal();\n\n  useAsync(async () => {\n    try {\n      setLoading(true);\n      const [studentInterviews, { data: interviews }] = await Promise.all([\n        courseService.getStudentInterviews(session.githubId),\n        coursesInterviewApi.getInterviews(course.id, false, [\n          TaskDtoTypeEnum.Interview,\n          TaskDtoTypeEnum.StageInterview,\n        ]),\n      ] as const);\n      const registeredInterviews = await getRegisteredInterviews(interviews);\n\n      setStudentInterviews(studentInterviews);\n      setInterviews(interviews);\n      setRegisteredInterviews(registeredInterviews);\n\n      const stageInterview = interviews.find(i => i.type === TaskDtoTypeEnum.StageInterview);\n\n      if (stageInterview) {\n        const { data: stageInterviewsCommentToStudent } = await coursesInterviewApi.getStageInterviewsCommentToStudent(\n          course.id,\n        );\n        setCommentsToStudent(stageInterviewsCommentToStudent);\n      }\n    } catch {\n      message.error('An error occurred. Please try later.');\n    } finally {\n      setLoading(false);\n    }\n  }, [course.id]);\n\n  const handleRegister = async (interviewId: string) => {\n    modal.confirm({\n      title: 'Are you ready to participate in the interview?',\n      content: (\n        <>\n          You are committing to do the following:\n          <ul>\n            <li>Contact assigned interviewer in time</li>\n            <li>Participate in the interview</li>\n          </ul>\n          NOTE: We DO NOT GUARANTEE you will get an interviewer. It depends on your score and how many mentors will be\n          available.\n        </>\n      ),\n      okText: 'Yes',\n      onOk: async () => {\n        try {\n          await coursesInterviewApi.registerToInterview(course.id, Number(interviewId));\n          setRegisteredInterviews(registeredInterviews.concat([interviewId]));\n        } catch {\n          message.error('An error occurred. Please try later.');\n        }\n      },\n    });\n  };\n\n  const getRegisteredInterviews = async (interviews: InterviewDto[]) => {\n    try {\n      const requests = interviews.map(async ({ type, id }) => {\n        const data =\n          type === CourseTaskDtoTypeEnum.StageInterview\n            ? await courseService.getInterviewStudent(session.githubId, 'stage').catch(() => null)\n            : await courseService.getInterviewStudent(session.githubId, id.toString()).catch(() => null);\n        return data ? id.toString() : null;\n      });\n\n      const result = await Promise.all(requests);\n      return result.filter(id => id != null);\n    } catch {\n      message.error('Something went wrong, please try reloading the page later');\n      return [];\n    }\n  };\n\n  const hasInterview = (id: number) => registeredInterviews.includes(id.toString());\n\n  const getStudentInterviewItems = (interviewName: string) => studentInterviews.filter(i => i.name === interviewName);\n\n  return (\n    <PageLayout loading={loading} title=\"Interviews\" showCourseName>\n      {contextHolder}\n      <Spin spinning={loading}>\n        {interviews.length === 0 ? (\n          <NoInterviewsAlert />\n        ) : (\n          <Row gutter={[12, 12]} justify=\"start\">\n            {interviews.map(interview => {\n              const { name, id } = interview;\n              const items = getStudentInterviewItems(name);\n\n              if (items.length > 0) {\n                return items.map((item, index) => {\n                  const interviewComment = commentsToStudent.find(comment => comment.id === item.id);\n                  return (\n                    <InterviewCard\n                      key={item.id + index}\n                      interview={interview}\n                      comment={interviewComment?.commentToStudent}\n                      item={item}\n                      isRegistered={true}\n                      onRegister={handleRegister}\n                    />\n                  );\n                });\n              }\n\n              const registered = hasInterview(id);\n              return (\n                <InterviewCard\n                  key={id}\n                  interview={interview}\n                  item={null}\n                  isRegistered={registered}\n                  onRegister={handleRegister}\n                />\n              );\n            })}\n          </Row>\n        )}\n      </Spin>\n    </PageLayout>\n  );\n}\n\nexport default function () {\n  return (\n    <SessionProvider>\n      <StudentInterviewPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/course/submit-scores.tsx",
    "content": "import UploadOutlined from '@ant-design/icons/UploadOutlined';\nimport { Button, Form, List, Table, Typography, Upload } from 'antd';\nimport type { UploadChangeParam, UploadFile } from 'antd/lib/upload/interface';\nimport { PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { CourseTaskSelect } from '@client/shared/components/Forms';\nimport csv from 'csvtojson';\nimport isUndefined from 'lodash/isUndefined';\nimport { useMemo, useState, useContext } from 'react';\nimport { useAsync } from 'react-use';\nimport { CourseService } from '@client/services/course';\nimport { filterLogin } from '@client/shared/utils/text-utils';\nimport { isCourseManager } from '@client/domain/user';\nimport { CoursesTasksApi, CourseTaskDto } from '@client/api';\nimport { SessionContext, SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { CourseRole } from '@client/services/models';\nimport { useMessage } from '@client/hooks';\n\ninterface SubmitResult {\n  status: string;\n  count: number;\n  messages?: string[];\n}\n\ninterface SubmitFormValues {\n  files: {\n    fileList: UploadFile[];\n  };\n  courseTaskId: number;\n}\n\ninterface IncomingFiles {\n  fileList: UploadFile[];\n}\n\ninterface StudentScore {\n  score: number;\n  studentGithubId: string;\n}\n\nconst courseTasksApi = new CoursesTasksApi();\n\nexport function SubmitScorePage() {\n  const { message } = useMessage();\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const [form] = Form.useForm();\n  const courseId = course?.id ?? 0;\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const [courseTasks, setCourseTasks] = useState([] as CourseTaskDto[]);\n  const [loading, setLoading] = useState(false);\n  const [submitResults, setSubmitResults] = useState([] as SubmitResult[]);\n  const [selectedFileList, setSelectedFileList] = useState(new Map() as Map<string, UploadFile>);\n\n  useAsync(async () => {\n    const { data } = await courseTasksApi.getCourseTasks(courseId);\n    setCourseTasks(data.filter(item => item.taskOwner?.id === session.id || isCourseManager(session, courseId)));\n  }, [courseService]);\n\n  const handleTaskChange = () => setSubmitResults([]);\n\n  const handleFileChose = (info: UploadChangeParam<UploadFile>) => {\n    const newSelectedFileList = new Map<string, UploadFile>();\n\n    info.fileList.forEach(file => {\n      newSelectedFileList.set(file.uid, file);\n    });\n\n    setSelectedFileList(newSelectedFileList);\n    setSubmitResults([]);\n  };\n\n  const handleSubmit = async (values: SubmitFormValues) => {\n    try {\n      setLoading(true);\n      const results = await parseFiles(values.files);\n      const submitResults = await uploadResults(courseService, values.courseTaskId, results);\n      setSubmitResults(submitResults);\n      setSelectedFileList(new Map());\n      message.success('Score has been submitted.');\n    } catch (err) {\n      const error = err as Error;\n      if (error.message.match(/^Incorrect data/)) {\n        message.error(error.message);\n      } else {\n        message.error('An error occurred. Please try later.');\n      }\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const [skipped] = submitResults.filter(({ status }) => status === 'skipped');\n  const skippedStudents = skipped && skipped.messages ? skipped.messages : [];\n\n  return (\n    <PageLayoutSimple loading={loading} title=\"Submit Scores\" showCourseName>\n      <Form form={form} onFinish={handleSubmit} layout=\"vertical\">\n        <CourseTaskSelect data={courseTasks} onChange={handleTaskChange} />\n        <h3>Uploading rules</h3>\n        <List\n          size=\"small\"\n          bordered\n          dataSource={[\n            'CSV-file should contain columns \"Score\" and \"GitHub\".',\n            '\"GitHub\" fields could be links or plain names.',\n            'For duplicated \"GitHub\" fields the best score would be counted.',\n            'You should upload several files, if you need scoring the best result from two or more tests.',\n            'Only students in the file would be scored. By the way, you can update just several scores.',\n          ]}\n          renderItem={(item, idx) => (\n            <List.Item>\n              <Typography.Text strong>{idx + 1}</Typography.Text> {item}\n            </List.Item>\n          )}\n          style={{ marginBottom: '1em' }}\n        />\n        <Form.Item label=\"CSV File\" name=\"files\" rules={[{ required: true, message: 'Please select csv-file' }]}>\n          <Upload\n            beforeUpload={() => false}\n            fileList={Array.from(selectedFileList).map(([, file]) => file)}\n            onChange={handleFileChose}\n            accept=\".csv\"\n            multiple\n          >\n            <Button>\n              <UploadOutlined /> Select files\n            </Button>\n          </Upload>\n        </Form.Item>\n        <Button size=\"large\" type=\"primary\" htmlType=\"submit\" style={{ marginRight: '1.5em' }}>\n          Submit\n        </Button>\n        {submitResults.length ? (\n          <Form.Item>\n            <h3>Summary</h3>\n            <Table\n              pagination={false}\n              dataSource={submitResults}\n              size=\"small\"\n              rowKey=\"status\"\n              columns={[\n                {\n                  title: 'Status',\n                  dataIndex: 'status',\n                },\n                {\n                  title: 'Count',\n                  dataIndex: 'count',\n                },\n              ]}\n            />\n          </Form.Item>\n        ) : (\n          ''\n        )}\n        {skippedStudents.length ? (\n          <Form.Item>\n            <h3>Skipped students</h3>\n            <List\n              size=\"small\"\n              bordered\n              dataSource={skippedStudents}\n              renderItem={item => <List.Item>{item}</List.Item>}\n              style={{ marginBottom: '1em' }}\n            />\n          </Form.Item>\n        ) : (\n          ''\n        )}\n      </Form>\n    </PageLayoutSimple>\n  );\n}\n\nasync function parseFiles(incomingFiles: IncomingFiles): Promise<StudentScore[]> {\n  const files = incomingFiles.fileList;\n\n  const filesContent: string[] = await Promise.all(\n    files.map(\n      file =>\n        new Promise<string>((res, rej) => {\n          const reader = new FileReader();\n          reader.readAsText(file.originFileObj as Blob, 'utf-8');\n          reader.onload = ({ target }) => res((target?.result as string) || '');\n          reader.onerror = e => rej(e);\n        }),\n    ),\n  );\n\n  const parsedRecords = await Promise.all(filesContent.map(content => csv().fromString(content)));\n  const scores = parsedRecords.flat().map(item => {\n    if (isUndefined(item.GitHub) || isUndefined(item.Score)) {\n      throw new Error('Incorrect data: CSV file should contain the headers named \"GitHub\" and \"Score\"!');\n    }\n\n    const parsedScore = parseInt(item.Score, 10);\n    if (isNaN(parsedScore)) {\n      throw new Error(`Incorrect data: Cannot parse \"Score\" for GitHub ${item.GitHub}`);\n    }\n\n    return {\n      score: parsedScore,\n      github: filterLogin(item.GitHub).toLowerCase(),\n    };\n  });\n\n  const uniqueStudents = new Map<string, number>();\n  scores.forEach(({ github, score }) => {\n    const current = uniqueStudents.get(github);\n    if (!current || current < score) {\n      uniqueStudents.set(github, score);\n    }\n  });\n\n  return Array.from(uniqueStudents).map(([studentGithubId, score]) => ({\n    studentGithubId,\n    score,\n  }));\n}\n\nasync function uploadResults(\n  courseService: CourseService,\n  courseTaskId: number,\n  data: StudentScore[],\n): Promise<SubmitResult[]> {\n  const results = await courseService.postMultipleScores(courseTaskId, data);\n  const groupedByStatus = new Map<string, SubmitResult>();\n\n  results.forEach(({ status, value }: { status: string; value: string | number }) => {\n    const current = groupedByStatus.get(status);\n\n    if (current) {\n      const newStatus: SubmitResult = {\n        status,\n        count: current.count + 1,\n        messages:\n          status === 'skipped' && typeof value === 'string' ? (current.messages ?? []).concat(value) : current.messages,\n      };\n      groupedByStatus.set(status, newStatus);\n    } else {\n      const newStatus: SubmitResult = {\n        status,\n        count: 1,\n        messages: status === 'skipped' && typeof value === 'string' ? [value] : undefined,\n      };\n      groupedByStatus.set(status, newStatus);\n    }\n  });\n\n  return Array.from(groupedByStatus.values());\n}\n\nfunction Page() {\n  return (\n    <SessionProvider allowedRoles={[CourseRole.Manager]}>\n      <SubmitScorePage />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/team-distributions.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { TeamDistributions } from '@client/modules/TeamDistribution/pages/TeamDistributions';\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <TeamDistributions />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/course/teams.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { Teams } from '@client/modules/Teams';\n\nexport function Page() {\n  return (\n    <SessionProvider>\n      <Teams />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/cv/[uuid].tsx",
    "content": "import { PublicPage } from '@client/modules/Opportunities/pages/PublicPage';\nimport { getServerSideProps } from '@client/modules/Opportunities/pages/PublicPage/getServerSideProps';\n\nexport { getServerSideProps };\n\nexport default PublicPage;\n"
  },
  {
    "path": "client/src/pages/cv/edit.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { EditPage } from '@client/modules/Opportunities/pages/EditPage';\n\n// force the page to render on the server to fix issue with getting githubId from url\nexport const getServerSideProps = async () => {\n  return { props: {} };\n};\n\nexport default function () {\n  return (\n    <SessionProvider>\n      <EditPage />\n    </SessionProvider>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/gratitude.tsx",
    "content": "import { Alert, Button, Form, Input, Select } from 'antd';\nimport { BadgeDto, BadgeEnum, GratitudesApi } from '@client/api';\nimport { AxiosError } from 'axios';\nimport { PageLayoutSimple } from '@client/shared/components/PageLayout';\nimport { UserSearch } from '@client/shared/components/UserSearch';\nimport { useMessage } from '@client/hooks';\nimport { SessionProvider, useActiveCourseContext } from '@client/modules/Course/contexts';\nimport { useRouter } from 'next/router';\nimport { useEffect, useState } from 'react';\nimport { useAsync } from 'react-use';\nimport { type UserBasic, UserService } from '@client/services/user';\n\ninterface IGratitude {\n  userIds: number[];\n  courseId: number;\n  badgeId: BadgeEnum;\n  comment: string;\n}\n\nconst gratitudesApi = new GratitudesApi();\nconst userService = new UserService();\n\nfunction GratitudePage() {\n  const { message } = useMessage();\n  const { course, courses } = useActiveCourseContext();\n  const [badges, setBadges] = useState<BadgeDto[]>([]);\n  const [loading, setLoading] = useState(false);\n  const [loadingUser, setLoadingUser] = useState(false);\n  const [preselectedUser, setPreselectedUser] = useState<UserBasic | null>(null);\n  const [form] = Form.useForm();\n  const router = useRouter();\n  const githubId = router.query.githubId;\n\n  useAsync(async () => {\n    const { data: badges } = await gratitudesApi.getBadges(course.id);\n    setBadges(badges);\n  }, []);\n\n  useEffect(() => {\n    if (githubId && typeof githubId === 'string') {\n      loadUserByGithubId(githubId);\n    }\n  }, [githubId]);\n\n  const loadUserByGithubId = async (githubId: string) => {\n    try {\n      setLoadingUser(true);\n      const users = await userService.searchUser(githubId);\n      const user = users.find(u => u.githubId === githubId);\n      if (user) {\n        setPreselectedUser(user);\n        form.setFieldsValue({ userIds: [user.id] });\n      }\n    } catch (error) {\n      console.error('Failed to load user by githubId:', error);\n    } finally {\n      setLoadingUser(false);\n    }\n  };\n\n  const loadUsers = async (searchText: string) => {\n    return userService.searchUser(searchText);\n  };\n\n  const handleSubmit = async (values: IGratitude) => {\n    try {\n      setLoading(true);\n      await gratitudesApi.createGratitude(values);\n      form.resetFields();\n      message.success('Your feedback has been submitted.');\n    } catch (e) {\n      const error = e as AxiosError<{ message: string }>;\n      const response = error.response;\n      const errorMessage = response?.data?.message ?? 'An error occurred. Please try later.';\n      message.error(errorMessage);\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  const onCourseChange = async (value: number) => {\n    const { data } = await gratitudesApi.getBadges(value);\n    setBadges(data);\n  };\n\n  const shouldWaitForUser = githubId && typeof githubId === 'string';\n  const showForm = !shouldWaitForUser || (shouldWaitForUser && !loadingUser);\n\n  return (\n    <PageLayoutSimple loading={loading || loadingUser} title=\"#gratitude\">\n      <Alert message=\"Your feedback will be posted to #gratitude channel in Discord\" style={{ marginBottom: 16 }} />\n\n      {showForm && (\n        <Form layout=\"vertical\" form={form} onFinish={handleSubmit}>\n          <Form.Item\n            name=\"userIds\"\n            label=\"Person\"\n            rules={[\n              {\n                required: true,\n                message: 'Please select a person',\n              },\n              {\n                type: 'array',\n                max: 5,\n                message: 'Please select no more than 5 people',\n              },\n            ]}\n          >\n            <UserSearch\n              mode=\"multiple\"\n              searchFn={loadUsers}\n              defaultValues={preselectedUser ? [preselectedUser] : undefined}\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"courseId\"\n            label=\"Course\"\n            initialValue={course.id}\n            rules={[{ required: true, message: 'Please select a course' }]}\n          >\n            <Select placeholder=\"Select a course\" onChange={onCourseChange}>\n              {courses.map(course => (\n                <Select.Option key={course.id} value={course.id}>\n                  {course.name}\n                </Select.Option>\n              ))}\n            </Select>\n          </Form.Item>\n          <Form.Item\n            initialValue={BadgeEnum.ThankYou}\n            name=\"badgeId\"\n            label=\"Badge\"\n            rules={[\n              {\n                required: true,\n              },\n            ]}\n          >\n            <Select placeholder=\"Select a badge\">\n              {badges.map(badge => (\n                <Select.Option key={badge.id} value={badge.id}>\n                  {badge.name}\n                </Select.Option>\n              ))}\n            </Select>\n          </Form.Item>\n          <Form.Item\n            name=\"comment\"\n            label=\"Comment\"\n            rules={[\n              {\n                required: true,\n                min: 20,\n                whitespace: true,\n                message: 'The comment must contain at least 20 characters',\n              },\n              {\n                validator: (_, value) => {\n                  if (value.includes('@')) {\n                    return Promise.reject();\n                  }\n                  return Promise.resolve();\n                },\n                message: 'The comment can\\'t include \"@\" symbol',\n              },\n            ]}\n          >\n            <Input.TextArea rows={8} />\n          </Form.Item>\n          <Button size=\"large\" type=\"primary\" htmlType=\"submit\">\n            Submit\n          </Button>\n        </Form>\n      )}\n    </PageLayoutSimple>\n  );\n}\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <GratitudePage />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/heroes.tsx",
    "content": "import { PageLayout } from '@client/shared/components/PageLayout';\nimport { HeroesForm } from '@client/shared/components/Forms/Heroes';\nimport { useState } from 'react';\nimport { SessionProvider } from '@client/modules/Course/contexts';\nimport { Tabs } from 'antd';\nimport HeroesRadarTab from '@client/components/Heroes/HeroesRadarTab';\n\nfunction Page() {\n  const [loading, setLoading] = useState(false);\n\n  const tabs = [\n    { label: 'Gratitudes', key: '1', children: <HeroesForm setLoading={setLoading} /> },\n    { label: 'Heroes Radar', key: '2', children: <HeroesRadarTab setLoading={setLoading} /> },\n  ];\n\n  return (\n    <SessionProvider>\n      <PageLayout loading={loading} title=\"Heroes\">\n        <Tabs items={tabs} />\n      </PageLayout>\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/index.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { HomePage } from '@client/modules/Home/pages/HomePage';\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <HomePage />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/login/index.module.css",
    "content": ".loginImage {\n  background-repeat: no-repeat;\n  background-size: contain;\n  background-image: url('/static/images/logo-rsschool3.png');\n  margin: 0 auto 50px;\n}\n\n.loginForm {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -65%);\n  text-align: center;\n}\n\n@media (max-width: 767px) {\n  .loginForm {\n    top: 20px;\n    transform: translate(-50%, 0);\n  }\n}\n\n.loginCardImg {\n  width: 100px !important;\n  height: 83.32px;\n  margin: 30px auto 20px;\n}\n"
  },
  {
    "path": "client/src/pages/login/index.tsx",
    "content": "import { GithubOutlined } from '@ant-design/icons';\nimport { Button, Card, Flex } from 'antd';\nimport ThemeSwitch from '@client/shared/components/ThemeSwitch';\nimport styles from './index.module.css';\n\nconst { Meta } = Card;\n\nexport default function LoginPage() {\n  return (\n    <main>\n      <div className={styles.loginForm}>\n        <div style={{ width: 320, height: 120 }} className={styles.loginImage} />\n        <Card\n          style={{ width: 320 }}\n          cover={\n            <img\n              className={styles.loginCardImg}\n              alt=\"example\"\n              src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPYAAADNCAMAAAC8cX2UAAABSlBMVEX///8gHx/0yrGc2vB8uuYAAACsXFEbGhqe3fPzx6weHR30ybCa2fARAAAaGRn70Lai4/rZ2dmkpKQvLi4SEBAXDAc4ODhnjZvGnIfi4uKIvM8nKy0NExZ5eHguNjgODQ2T0e309PRYWFgAAAtCQUEaFBKJx+rIyMjv7++0tLSqVkrS0tKnT0JTR0GGhoYADBHasZplZWVPaXKZmZmSdWb659z2077O7PdOTU28vLyvr69zc3OOjo5dXV2tinj88uz43c235PTm9ftMQTt/Z1vQp5HjuaK2cmnHlpDo1NK/hn7ZubXTrKi+noucgnOJc2Y5R0x5p7dqkJ7F6fZ0rdVnVUvr29mjRTe0bmW7f3few8DcqJPIiXjSmYbisJpKYWkYJSpBU1m1zNVNZm9beoZdhJw9UmNJZ31mlrlTeJJFYHR8sMmkz+262vG2lajvAAASD0lEQVR4nO2d/UPayLrHiXRMCCGAvFQURAQEAUFKqyBoVZR22yq0Xbeu2929nrq9u+e0//+vdyYJZCbvUMzEc/n+sGshCfOZ55nnmZlMJj7fQgsttJCR6gAAsV0/jD7or6QO620W/lL3QX/FuXKAZRhezEL2TOmBfqOUYQDIijz8IVB+oN+YUh3AKOLjkHz+Nk/VGRDnxz8CVub+AzOpGWdU8VnQ7sz18p02yPLYL4jtuV5+ZgGGlAiYw7ldvBMDoub63vDykhabYVjA7Mzl2hCa1V0dzNebZlQvqysYAk/+eHSrFgygGSaemUOpf1gFrROOXf0oZXNmLpez+Lbc1Lm3UqWxeRZ/Vul9fGwV0NMdnKp2WvVuO8mDibJM4SjTWylFyTpogbjZhYFVbbmklCk2LF+sqh7X6TVjEmY2LrJYaOZ5UYxnpW/amZWSjFRNWl22alYY99SxKB9s4qgdlju9dhxSxY1aKiZehEeBZPcw1TPxbwV7foliZrWssBkmG+sllf6VQ7GohqyvqW88rqtuXUSGzdrYeHrFPdAtb5pGngeTWKAN7fO1rVrhw8gLGcwkbT+keJY2NB3sOG1oOtgibWgqbZtnaEP7fFsUQlqSNrTP16WQwI5oQ8Nemk135QGUrdOG9vkOrTunDyEvzKZFKWA/1PzsFMpRwPbCZJrridsLfVMKMc0LEY1C4/bIRHly7gNqG2W9wJ1yv78CmrShfWXRbWMj7hZt7IL7xmbop+6M+2kbifLtXsvp4odUnGZYy2WdTwTPWTQny5vuD7/G4rPUbglVabk4UpbajU8K82iYgN0d1QcStXgmK06p05Kkamxa5jZYveGu6AzFKEwVawQoUFvdz3cLez4Lg6YShSlTrcQt97FjFEZeWrnfM/eAj9NYorbiBWz3l6hRWMWgl/t3w9apjb1wud24PdG03Z9lod5Fk+X27TAnEY0XxTQmUWRZi5bBsyJ5fNrB5KTbMc1uORqfTudju9fD52MNz693d/uxdAMqrZH0Way/u3t9Lh17dXUF/3t+3YcfW4cQ0eVRmHUgZxux86tVMx3LWMPz8+FQYjw2P/S837Dq+rsdyq3GIXxjVwa5uNhTdHFxYYpG6EI9Z08+5fg6bf5bbi9asrgDlE4i6L1BcEmjYDAwGAx0dSCRwi8CQd0ZSwHEfrzbMP0xlwdh5j3yxjUkGegAZlZwb3V12DBr4i5jm/ZW8s9XLwLzg5Z0sXpsFtpcxmZMitF4vro3Z2iogSm3y08SxIxLAann6N8Et3H79kTbbpxrqIOBQEAfqBxIe2Jg9bkht8vYhpE8vUtSB4InL16c7E8PHgzsozODWJAIrJ6n6WMb3RngY0S7DgZfyMe+PJkyxgVOlF95geW0wWrf4DddxjbqrjSOL3DqffXoF1NxB16oZ+6r3HvHBuZ2GftIjy1em1FDg0/BHXiJn7mkcl8YuLnL2AZ98vQx3rAD5PHO7Y3bGkk9MbiqS5tur7HWj8DE6z2jwueU+Y99h3Ft7CXlcULGKmygM7fbQxH941+N46C+8D+9qtUqrxGBUzeXXPzlm1qt9usvugpb5TXmdnuN2o4Wm03uaQvv872rLUNVKr/4nJo7eIIqq1ZBp9XeShfBKmxwrQkpbt/11E0qpc8xcwaXpIPefkLFRwQ5p+ZG9fVLrSKf9+lWugx23StNn8Xt2RXdFGLjOIwV/gQd8/LTu7evJIDKG6L0VoIH3lRuXqvV5fNhaf9C00ty+7nHXFzTyhpEGJd8/A1yUrn8NejmJw68HPn4be0VvL5cXe+k6lOxA5qxgOsLdzS9U7aP+7CUvXKfUJN+I5f/tbMchhLArzUE+0quL+m3sBymadyu3w3SJG5xF/NxOY7fflKtjcrvpHFDN8nVlifWlrwEbx5hTQpz/VFuzYyxeK3Dfl2pvXt7owS12kvcaObYKIwvw7Y9joVSMFeTQPi5BtvtVVqaxVnpcxz7BB0B3VuJyAj7J5/PSQaDTiIlL+U01DjwqBAmQ7n7D8LlLLDlQP5qGVPt1lkoh7m+gp2mxyasTeG+PrlOKX39MNhvLLEpLNoh91shQ5qE/StefudO/tbS2kMCm8LiFbJxs0mDkIZj/zJFSMOwpcSNhTQyktNYmQaInkMax5b6pqTZHCewMo6NnIRIYETediWi5crl1FjlXM5XJzI3ICb80PF4+VEbddpducFrS0pQ6onhPt5LkvamwIpVLlvu2+MQM1XtrPTq3aN2jM+COJMsFNpbktrtQjLGMkRMy+NTK3Ln9FWFsJqTIRiKCm/V6pKbNu4mxEQDL0rFahPFAiAeK2x1M72VTik6xWKHcnWn1d1KxpJbmdbKTimaKptUITFp3LjShXKsld74nEU0aSiC+bhUfVhnfkCkbbMlHDlo/2rnsFVvFmLJdrN+WEpZeUGqtNJNJtvd1k7VDBUX0VEjEveSPPB8MzY3CmjOB5634+qSOudE077M49nDyUYksAo6Kxloxa3eTtQAKpXMtuuH1SkaRw4QWyOFNaX3TdxcGjc7nGZASeDdJ5laStp4TCDT9nQrOFKdVpNhdKfkpk8GROrOG82bvq5BLaNwPNWk0m2lVqkptsbnTslAPkOHfAZI6bRytLSz0st0j7Zg/EjisTw9JMytTCHmbm/lKTEjYwfDBp/JFfbT7U8KFJ4AgvgNQJ5JonB21ITR67BTTc0himtUjsKY3oScha1mvXXYQXEOJYsewEuht5oqgxsjvy0vfzaoi8ktEVm4l5BNG0R9OZi9ylEYvVZ6MOGg4mVgXLKMX854qzu95jpbQNHQKCDgO2M1LnHjKdNpigxy9gC2+8pnA3uTE+WYi0Mfx5p23GBjilyqhLJQTGzXV0qz+XN0p94GyW6rY7VxKf64CBsL48YLLqn2Nuqp/C6FO/3nBPfLJeKSeOoAVuWCOUkq/UrJeebORQ+7hfVmy0l9dbGolj8mbBccu+vLfaNw9gfCvjGwNuTeV2qMbBpEHM862D2sHD2sJ2NHLXv2cqlXSDZXHOexHKP2Wfi0pqkGAvsnJydLJjd6P1cqyyZrAIKBJXjifoCsLjygTfGQZ2qnXkBb7pkilOrryXpnutWr+ECMGHTLJQ0arT8a2/s381RucCIx6JxyyjRXbW2JTUP0LTFTmiEG4huv5K8MnXYuCu9hYTw7y92QaKtgMFKddY0ynsXyl864w4Z/WgsbAoizbsYxz9ReV7l5h9yff1P/dLTGJ4gnLzZL6TlHUgT3sRPuyjhzhX+v/WZ55Pi4oerivGXuclEtbFAChvbcEFbuqIQHFcPUrTvhXKVmvbC9q6wOth9ro79nD35TewWdO/h7pfKHA+jgtTrOjosesTVSCtt9V8wPjQYZpD7XKss3ldryHw5844JR2zUoeGFHJVUZbFftBnNlBx7+4/PN8qvf7Qfh4cF5fnJlHnhil25c1ZjawvlGejgIW5PDr20tHQ4Phg3V1Nm4B/YM06mFvSWAT4P+FST/gf4LPPfyGqjQceCJTaT0ytUBFtPZRr4/vAzOhA5PGlyeM3kcuumJbG2ocg/gm2yz6Xy6f351gczuCF8+brB3POzDU1XfyYKmhwK4kQ6TgNilmk038g0xeT28urzcGwTDpgoO9i4uj4fnuww8IY1dIg5Anbqlc51ept6yGrVE6wzIkqsteFZMQ/x8HgDouLFkv7+rqN/vJ2MxJg2/Afk8elqKeFqMh8xHHni3RBTto482zd+yKky1VwDAZPt5nudZKFEW+hN+YrgsXwSA7U45En4gjQdcfAjELK1Q3snEptuIniBGNXvU8kw/tKyErIQgbIAjm5FcudQ7QuXPxp3S88r7F9r1HW/FMNgPRQiJp8KfRZB1ULZy9bDVLSQdccdihW7vsOoJv9aqJb0TIREqss82HD9K3JPfrmEpAAoeeGeIiQ5QtwR5OjuKcBsOh4FbwKGoZyoznR5ATy+gyJZYi9wnHBW0IL9IJQ4jt0HQZsdvVPEyt/8U/VeaUgmtF5m4g4mtLeAgpKE3iiB5smH7zgRB+r90e5tniiP7EYLjbTuktOWJzXp1OhCEM+kPCaZ4H1mzm9sqT7GjAeyheHOgdSoIH+W/0LaXEDtyZ2OfqTYrgaMOTzZvwS+cKn+ilYih99zI4BVYmKbdOzALKOyVZCsMW3qGIJRgrTcCmXpnGuCVGWFck7btm3TQi1brAadp2bI8OGOmkXTDE4W1hGnamWH3pTiNDeCm0Y4SzSMfTB3T/M0j5rncO3cATCQ92gt7ax9CZvdeTY0tHpnu8EB/S2obSWGaT4xYhgVbBg28ZLo/Juj4cmab1tDazNO55DjNMvz6/Qh0SeeM9oqAMDaPmRe1CrMNYr3wfhxrjdciJt5HIvcA8PUdtMA1hxZyZkH6/VMcm2faKrcWG98G2gvvvrKRslqp+CUS2QwxiY2RPKD4H5DlEx8iBDaoYmsfSGw2STxI6PVQ7ptszZ4YPb0rineRiLDB8hubEWHEsqO1ZwmsxXbxbKbBjhFrOT3ZP9VI2YifTRQZdsRFIn9K9JFn8J8h/GYBBLXCxh82egzYuZja/Swy93dF+L+7p39Kdsbmy9HrX6yw8V6sF7unOuWSqn/yRQlFhKaXsNUYhh5fssTeIb98BDoy65Rg2OjxJUvs1KPD9rVMXquLY1dtsH2PD9uXahuC49gpO2y1s/posGE/tK294YfKr36CVvtrsNWnwCVsdTeXR4QNLY5u+IG4dD9PmQNOYjXgs8NW35f4qLB96K5XK3NUiMWS6B3qO9UcRmaPnXm02DotsH0L7AX2AnuBTbvcP6gFtm+BvcBeYC+waZf7B2WO/a+Dg/8X2Kenkafq/HFoTeA21Mch1iMCmmv978MOCQJHYHMkth/H/teZ/aU9qrODUy6ygWH7HWNDVxC404NHx372kROg/DNjc34/ugBk9+56TFIysl/Sj2BLQlf66H2rn536x8hzwVbQ0YpPz0rDPC9shdyjNj/QMs8RWyL3oLefneqZEba659VU2O/12BK5t0xuAu33c6EJWNFvh30/wS7eRwwv5y3wjybQfm5tgs1ucDbY3IfJtzxjgi2BeyOlnZlB+wkDfonYYat1ZOblCrgXwvqpKbSf20wQINbYfn9xEgiQb5hL4GhD+zhzaj93NzE2z/jt2jb0DfXrhFnrVgxOt4VbOLif475glB84Pbb6xiUJGzqHukor9N6K20+X28LSkc2RClkccQbWXsecGkFGPqitmwk9jVg6OkVucw+PrN2Hiir1ut8IW9u2oZvj3BsWcQ1yU6P+aErNPQMqNB+6EyQAAnuT40YqNqPU1lOsstjQnQX3ZDW/6zKnxryVTbAfOLn43H0C54z8qSImNjnFS0YYeOipFTclNz8wd/FnIR49sswWE6GNv/yR8TD0y4RIvNOlcaXGIv/5GkoUi+gxOT7xxSKu0TK3ecbmYDgrFovro//96+cnT55sjpHUxpyAkR3vn0wSlrANT/j7n68bfLGY+LppTg1FB9sqY0c2n+CSDsUw+QT6hBupeX1dbv3kaZtWMY1WULPARtrWcmNtGWYnsh5gYkeZe9s5NDVsm1KRlhM4LDsVlb4n9hGfWItwupryIrZ5/jLg3l7bmASwIiMoVRb5onKHPgg4tR00tZ75mZ09COsxEw9PrKvuC+2tTkT8hXu4PTatgZjF4EspmYrx99jWxdC/MVNuPvl7fdwTZ0eYc9i0ICRK1FOZ+6+E3HcJfUUpbXt8BPrqn/WQNORMfJ2iYdPslJt3WCbGnGg9FEokNv75mWi5SrX8/W8+EQpt/KwebWvs8XPFVGQX1fxYa117j2Fta7/++T9r27qvLaip9cgl2tibw7A5IsJxGmd4QgRAu4BGmdp6ooHEFghKqZdG1oNzbC/MplnFcxybCOwyGPbvbb9jbIHzxKTxgbnBSWyOxNRWgzNsgWowI2Q+UU5iaxq3xumdYAvUWzUhE3ASmxid+GfA9tRNEVlGd/5ssM3/aYQteMi9caE7YcIDYXv4Ti/UgYbcAltwjo1Wc3h9MYd0b1+YG7bg9bUMqs4kows/jC0t2vHevXwroeVZkHxmbFRtj3CJlqSzg2/bJmDW2MJjJVaVOzv4juih/SywlW+/ffv+6IE1yp2dff/+/du3b9sI0D8ZkW1/k2DP/stwF1poIff1f/96fiFYd7JbAAAAAElFTkSuQmCC\"\n            />\n          }\n          actions={[\n            <Button\n              key={'github'}\n              onClick={() => (window.location.href = `/api/v2/auth/github/login${location.search}`)}\n              size=\"large\"\n              icon={<GithubOutlined />}\n              type=\"primary\"\n            >\n              Sign up with GitHub\n            </Button>,\n          ]}\n        >\n          <Meta\n            title=\"Please login via GitHub\"\n            description=\"In order to access the RS School App, you need to login with your GitHub account\"\n          />\n        </Card>\n        <Flex align=\"center\" justify=\"center\" style={{ marginTop: 20 }}>\n          <ThemeSwitch />\n        </Flex>\n      </div>\n    </main>\n  );\n}\n"
  },
  {
    "path": "client/src/pages/mentors-hall-of-fame.tsx",
    "content": "import { MentorsHallOfFamePage } from '@client/modules/MentorsHallOfFame';\nimport Head from 'next/head';\n\nfunction Page() {\n  return (\n    <>\n      <Head>\n        <title>Mentors Hall of Fame</title>\n      </Head>\n      <MentorsHallOfFamePage />\n    </>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/profile/connection-confirmed.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { ConnectionConfirmed } from '@client/modules/Notifications/pages/ConnectionConfirmedPage';\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <ConnectionConfirmed />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/profile/index.module.css",
    "content": ".masonry {\n  display: flex;\n  margin-left: -16px;\n  width: auto;\n}\n\n.masonryColumn {\n  padding-left: 16px;\n  background-clip: padding-box;\n}\n"
  },
  {
    "path": "client/src/pages/profile/index.tsx",
    "content": "import { useContext, useState } from 'react';\nimport { useRouter } from 'next/router';\nimport { Result, Spin, theme } from 'antd';\nimport { dynamicWithSkeleton } from '@client/utils/dynamicWithSkeleton';\nimport { ProfileApi, UpdateProfileInfoDto, UpdateUserDtoLanguagesEnum } from '@client/api';\nimport { Header } from '@client/shared/components/Header';\nimport { LoadingScreen } from '@client/shared/components/LoadingScreen';\nimport { withGoogleMaps } from '@client/components/withGoogleMaps';\nimport { NotificationChannel, NotificationsService } from '@client/modules/Notifications/services/notifications';\nimport { ProfileInfo, ProfileMainCardData, UserService } from '@client/services/user';\nimport { SessionContext, SessionProvider } from '@client/modules/Course/contexts';\nimport { useAsync } from 'react-use';\nimport { checkIsProfileOwner, getStudentCoreJSInterviews } from '@client/utils/profilePageUtils';\nimport { useMessage } from '@client/hooks';\nimport Masonry from 'react-masonry-css';\nimport styles from './index.module.css';\n\nconst MainCard = dynamicWithSkeleton(() => import('@client/components/Profile/MainCard'));\nconst AboutCard = dynamicWithSkeleton(() => import('@client/components/Profile/AboutCard'));\nconst DiscordCard = dynamicWithSkeleton(() => import('@client/components/Profile/DiscordCard'));\nconst EducationCard = dynamicWithSkeleton(() => import('@client/components/Profile/EducationCard'));\nconst ContactsCard = dynamicWithSkeleton(() => import('@client/components/Profile/ContactsCard'));\nconst PublicFeedbackCard = dynamicWithSkeleton(() => import('@client/components/Profile/PublicFeedbackCard'));\nconst StudentStatsCard = dynamicWithSkeleton(() => import('@client/components/Profile/StudentStatsCard'));\nconst MentorStatsCard = dynamicWithSkeleton(() =>\n  import('@client/components/Profile/MentorStatsCard').then(m => m.MentorStatsCard),\n);\nconst LanguagesCard = dynamicWithSkeleton(() => import('@client/components/Profile/LanguagesCard'));\nconst InterviewCard = dynamicWithSkeleton(() => import('@client/components/Profile/InterviewCard'));\n\ntype ConnectionValue = {\n  value: string;\n  enabled: boolean;\n  lastLinkSentAt?: string;\n};\n\ntype Connections = Partial<Record<NotificationChannel, ConnectionValue>>;\n\nexport type ChangedPermissionsSettings = {\n  permissionName: string;\n  role: string;\n};\n\nconst profileApi = new ProfileApi();\nconst userService = new UserService();\nconst notificationsService = new NotificationsService();\n\nconst Profile = () => {\n  const { message } = useMessage();\n  const router = useRouter();\n  const session = useContext(SessionContext);\n  const [profile, setProfile] = useState<ProfileInfo | null>(null);\n  const [isProfileOwner, setIsProfileOwner] = useState(false);\n  const [isSaving, setIsSaving] = useState(false);\n  const [connections, setConnections] = useState<Connections>({});\n\n  const { token } = theme.useToken();\n\n  const fetchData = async () => {\n    try {\n      const githubId = router.query ? (router.query.githubId as string) : undefined;\n      const [profile, connections, { data }] = await Promise.all([\n        userService.getProfileInfo(githubId?.toLowerCase()),\n        notificationsService.getUserConnections().catch(() => ({})),\n        profileApi.getProfile(githubId?.toLowerCase() ?? session.githubId),\n      ]);\n\n      const updateProfile = {\n        ...profile,\n        ...data,\n      };\n\n      let isProfileOwner = false;\n      if (profile?.generalInfo) {\n        const userId = session.githubId;\n        const profileId = profile.generalInfo.githubId;\n        isProfileOwner = checkIsProfileOwner(userId, profileId);\n      }\n\n      setProfile(updateProfile);\n      setIsProfileOwner(isProfileOwner);\n      setConnections(connections as Connections);\n    } catch {\n      setProfile(null);\n    }\n  };\n\n  const sendEmailConfirmationLink = async () => {\n    try {\n      await userService.sendEmailConfirmationLink();\n    } catch {\n      message.error('Error has occurred. Please try again later');\n    }\n  };\n\n  const updateProfile = async (data: UpdateProfileInfoDto) => {\n    setIsSaving(true);\n    let isUpdated: boolean;\n    try {\n      await profileApi.updateProfileInfoFlat(data);\n      setIsSaving(false);\n      message.success('Profile was successfully saved');\n      isUpdated = true;\n    } catch {\n      setIsSaving(false);\n      message.error('Error has occurred. Please check your connection and try again');\n      isUpdated = false;\n    }\n\n    return isUpdated;\n  };\n\n  const authorizeDiscord = async () => {\n    const discord = await userService.getDiscordIds();\n    if (discord) {\n      setProfile(profile => ({\n        ...profile,\n        publicCvUrl: profile?.publicCvUrl ?? null,\n        discord,\n      }));\n\n      await updateProfile({ discord });\n      router.replace('/profile');\n    }\n  };\n\n  const mainInfo: ProfileMainCardData = {\n    location: profile?.generalInfo?.location ?? null,\n    name: profile?.generalInfo?.name ?? '',\n    githubId: profile?.generalInfo?.githubId ?? null,\n    publicCvUrl: profile?.publicCvUrl ?? null,\n  };\n  const aboutMyself = profile?.generalInfo?.aboutMyself ?? '';\n  const languages = profile?.generalInfo?.languages ?? [];\n\n  const githubId = profile?.generalInfo?.githubId;\n  const isAdmin = session.isAdmin;\n\n  const hasEducation = Array.isArray(profile?.generalInfo?.educationHistory);\n  const hasContacts = profile?.contacts !== undefined;\n  const hasDiscord = profile?.discord !== undefined;\n  const hasPublicFeedback = Array.isArray(profile?.publicFeedback) && profile.publicFeedback.length > 0;\n  const hasStudentStats = Array.isArray(profile?.studentStats) && profile.studentStats.length > 0;\n  const hasMentorStats = Array.isArray(profile?.mentorStats) && profile.mentorStats.length > 0 && !!githubId;\n\n  const cards = [\n    profile?.generalInfo\n      ? {\n          key: 'main',\n          node: (\n            <MainCard\n              isAdmin={isAdmin}\n              data={mainInfo}\n              isEditingModeEnabled={isProfileOwner}\n              updateProfile={updateProfile}\n            />\n          ),\n        }\n      : null,\n    {\n      key: 'about',\n      node: <AboutCard data={aboutMyself} isEditingModeEnabled={isProfileOwner} updateProfile={updateProfile} />,\n    },\n    {\n      key: 'languages',\n      node: (\n        <LanguagesCard\n          data={languages as UpdateUserDtoLanguagesEnum[]}\n          isEditingModeEnabled={isProfileOwner}\n          updateProfile={updateProfile}\n        />\n      ),\n    },\n    hasEducation\n      ? {\n          key: 'education',\n          node: (\n            <EducationCard\n              data={profile!.generalInfo?.educationHistory || []}\n              isEditingModeEnabled={isProfileOwner}\n              updateProfile={updateProfile}\n            />\n          ),\n        }\n      : null,\n    hasContacts\n      ? {\n          key: 'contacts',\n          node: (\n            <ContactsCard\n              data={profile!.contacts!}\n              isEditingModeEnabled={isProfileOwner}\n              connections={connections}\n              sendConfirmationEmail={sendEmailConfirmationLink}\n              updateProfile={updateProfile}\n            />\n          ),\n        }\n      : null,\n    hasDiscord\n      ? { key: 'discord', node: <DiscordCard data={profile!.discord!} isProfileOwner={isProfileOwner} /> }\n      : null,\n    hasPublicFeedback ? { key: 'publicFeedback', node: <PublicFeedbackCard data={profile!.publicFeedback!} /> } : null,\n    {\n      key: 'interview',\n      node: (\n        <InterviewCard\n          coreJsInterview={getStudentCoreJSInterviews(profile?.studentStats)}\n          prescreeningInterview={profile?.stageInterviewFeedback}\n        />\n      ),\n    },\n    hasStudentStats\n      ? {\n          key: 'studentStats',\n          node: (\n            <StudentStatsCard\n              username={session.githubId}\n              data={profile!.studentStats!}\n              isProfileOwner={isProfileOwner}\n            />\n          ),\n        }\n      : null,\n    hasMentorStats\n      ? {\n          key: 'mentorStats',\n          node: <MentorStatsCard isAdmin={isAdmin} githubId={githubId!} data={profile!.mentorStats!} />,\n        }\n      : null,\n  ].filter(Boolean) as { key: string; node: JSX.Element }[];\n\n  const preloadData = useAsync(async () => {\n    await fetchData();\n    await authorizeDiscord();\n  });\n\n  return (\n    <LoadingScreen show={preloadData.loading}>\n      <Header />\n      <Spin spinning={isSaving} delay={200}>\n        {profile ? (\n          <div style={{ padding: 10, background: token.colorBgContainer }}>\n            <Masonry\n              breakpointCols={{\n                default: 4,\n                1100: 3,\n                700: 2,\n                500: 1,\n              }}\n              className={styles.masonry as string}\n              columnClassName={styles.masonryColumn as string}\n            >\n              {cards.map(({ key, node }) => (\n                <div style={{ marginBottom: 16 }} key={key}>\n                  {node}\n                </div>\n              ))}\n            </Masonry>\n          </div>\n        ) : (\n          <Result status={'403'} title=\"No access or user does not exist\" />\n        )}\n      </Spin>\n    </LoadingScreen>\n  );\n};\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <Profile />\n    </SessionProvider>\n  );\n}\n\nexport default withGoogleMaps(Page);\n"
  },
  {
    "path": "client/src/pages/profile/notifications.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { UserNotificationsPage } from '@client/modules/Notifications/pages/UserNotificationsSettingsPage';\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <UserNotificationsPage />\n    </SessionProvider>\n  );\n}\n\nexport default Page;\n"
  },
  {
    "path": "client/src/pages/registry/epamlearningjs.tsx",
    "content": "import { Button, Col, Form, Input, message, Result, Row, Typography } from 'antd';\nimport axios from 'axios';\nimport { PageLayout } from '@client/shared/components/PageLayout';\nimport { GdprCheckbox, LocationSelect } from '@client/shared/components/Forms';\nimport { withGoogleMaps } from '@client/components/withGoogleMaps';\nimport { useState, useEffect } from 'react';\nimport { useAsync, useUpdate } from 'react-use';\nimport { CoursesService } from '@client/services/courses';\nimport { Course } from '@client/services/models';\nimport { UserFull, UserService } from '@client/services/user';\nimport { emailPattern, englishNamePattern } from '@client/services/validators';\nimport { TYPES } from './../../configs/registry';\nimport { Location } from '@common/models/profile';\nimport { SessionProvider } from '@client/modules/Course/contexts';\n\nconst defaultColumnSizes = { xs: 18, sm: 10, md: 8, lg: 6 };\nconst defaultRowGutter = 24;\n\nconst courseAlias = 'epamlearningjs';\n\ntype FormData = {\n  firstName: string;\n  lastName: string;\n  primaryEmail: string;\n  location: Location;\n  gdpr: boolean;\n};\n\nfunction EpamLearningJSPage() {\n  const [form] = Form.useForm<FormData>();\n\n  const update = useUpdate();\n  const [submitted, setSubmitted] = useState(false);\n  const [activeCourse, setActiveCourse] = useState(null as Course | null);\n  const [location, setLocation] = useState(null as Location | null);\n  const [initialData, setInitialData] = useState(null as Partial<UserFull> | null);\n\n  useAsync(async () => {\n    const userService = new UserService();\n    const courseService = new CoursesService();\n    const [profile, courses] = await Promise.all([userService.getMyProfile(), courseService.getCourses()]);\n    const course = courses.find(course => course.alias === courseAlias) ?? null;\n    setActiveCourse(course);\n    setInitialData(profile);\n  }, []);\n\n  useEffect(() => {\n    setLocation({\n      countryName: initialData?.countryName,\n      cityName: initialData?.cityName,\n    } as Location);\n  }, [initialData]);\n\n  const handleSubmit = async (model: FormData) => {\n    const { location } = model;\n    const registryModel = {\n      type: TYPES.STUDENT,\n      courseId: activeCourse!.id,\n    };\n    const userModel = {\n      cityName: location.cityName,\n      countryName: location.countryName,\n      primaryEmail: model.primaryEmail,\n      firstName: model.firstName,\n      lastName: model.lastName,\n    };\n\n    try {\n      const userResponse = await axios.post('/api/profile/me', userModel);\n      const githubId = userResponse && userResponse.data ? userResponse.data.data.githubId : '';\n      if (githubId) {\n        await axios.post('/api/registry', registryModel);\n        setSubmitted(true);\n      } else {\n        message.error('Invalid github id');\n      }\n    } catch {\n      message.error('An error occured. Please try later.');\n    }\n  };\n\n  let content: React.ReactNode;\n  if (activeCourse == null) {\n    return null;\n  }\n  if (submitted) {\n    content = (\n      <Result\n        status=\"success\"\n        title=\"You have successfully registered.\"\n        extra={\n          <Button type=\"primary\" href=\"/epamlearningjs\">\n            Continue\n          </Button>\n        }\n      />\n    );\n  } else if (initialData) {\n    content = (\n      <Form\n        layout=\"vertical\"\n        form={form}\n        initialValues={getInitialValues(initialData)}\n        onChange={update}\n        className=\"m-2\"\n        onFinish={(values: FormData) => handleSubmit({ ...values, location: location! })}\n      >\n        <Col style={{ margin: '0 20px' }}>\n          <Row>\n            <Typography.Title level={4}>My Profile</Typography.Title>\n          </Row>\n          <Row gutter={defaultRowGutter}>\n            <Col {...defaultColumnSizes}>\n              <Form.Item\n                name=\"firstName\"\n                label=\"First Name (in English, as in passport)\"\n                rules={[{ pattern: englishNamePattern, message: 'First name should be in English' }]}\n              >\n                <Input placeholder=\"Dzmitry\" />\n              </Form.Item>\n            </Col>\n            <Col {...defaultColumnSizes}>\n              <Form.Item\n                name=\"lastName\"\n                label=\"Last Name (in English, as in passport)\"\n                rules={[{ pattern: englishNamePattern, message: 'Last name should be in English' }]}\n              >\n                <Input placeholder=\"Varabei\" />\n              </Form.Item>\n            </Col>\n          </Row>\n          <Row gutter={defaultRowGutter}>\n            <Col {...defaultColumnSizes}>\n              <Form.Item\n                help=\"We need your location for understanding audience and use it for mentor distribution.\"\n                name=\"location\"\n                label=\"Location\"\n                rules={[{ required: true, message: 'Please select city' }]}\n                valuePropName={'location'}\n              >\n                <LocationSelect onChange={setLocation} location={location} />\n              </Form.Item>\n            </Col>\n          </Row>\n          <Row gutter={defaultRowGutter}>\n            <Col {...defaultColumnSizes}>\n              <Form.Item\n                help=\"We will use your email only for course purposes. No spam emails.\"\n                name=\"primaryEmail\"\n                label=\"Primary Email\"\n                rules={[{ required: true, pattern: emailPattern, message: 'Email is required' }]}\n              >\n                <Input placeholder=\"user@example.com\" />\n              </Form.Item>\n            </Col>\n          </Row>\n          <Row>\n            <GdprCheckbox />\n          </Row>\n          <Button size=\"large\" type=\"primary\" disabled={!form.getFieldValue('gdpr')} htmlType=\"submit\">\n            Submit\n          </Button>\n        </Col>\n      </Form>\n    );\n  }\n\n  return (\n    <PageLayout loading={false} title=\"Registration\">\n      {content}\n    </PageLayout>\n  );\n}\n\nfunction getInitialValues({ countryName, cityName, ...initialData }: Partial<UserFull>) {\n  const location =\n    countryName &&\n    cityName &&\n    ({\n      countryName,\n      cityName,\n    } as Location | null);\n  return {\n    ...initialData,\n    location,\n  };\n}\n\nfunction Page() {\n  return (\n    <SessionProvider>\n      <EpamLearningJSPage />\n    </SessionProvider>\n  );\n}\n\nexport default withGoogleMaps(Page);\n"
  },
  {
    "path": "client/src/pages/registry/mentor.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { MentorRegistry } from '@client/modules/Registry/pages';\n\nfunction MentorRegistryPage() {\n  return (\n    <SessionProvider>\n      <MentorRegistry />\n    </SessionProvider>\n  );\n}\n\nexport default MentorRegistryPage;\n"
  },
  {
    "path": "client/src/pages/registry/student.tsx",
    "content": "import { SessionProvider } from '@client/modules/Course/contexts';\nimport { StudentRegistry } from '@client/modules/Registry/pages';\n\nfunction StudentRegistryPage() {\n  return (\n    <SessionProvider>\n      <StudentRegistry />\n    </SessionProvider>\n  );\n}\n\nexport default StudentRegistryPage;\n"
  },
  {
    "path": "client/src/providers/DevToolsProvider.tsx",
    "content": "import { ReactNode } from 'react';\nimport { DevToolsContainer } from '@client/components/DevTools';\n\nexport function DevToolsProvider({ children }: { children: ReactNode }) {\n  const devToolsToggle = process.env.RSSCHOOL_DEV_TOOLS === 'true';\n  if (!devToolsToggle) return <>{children}</>;\n  return <DevToolsContainer>{children}</DevToolsContainer>;\n}\n"
  },
  {
    "path": "client/src/providers/MessageProvider.tsx",
    "content": "import { message, notification } from 'antd';\nimport { MessageInstance } from 'antd/es/message/interface';\nimport { NotificationInstance } from 'antd/es/notification/interface';\nimport { createContext, ReactNode } from 'react';\n\ntype MessageProviderType = {\n  message: MessageInstance;\n  notification: NotificationInstance;\n};\n\nconst MessageContext = createContext<MessageProviderType>({\n  message,\n  notification,\n});\n\nfunction MessageProvider({ children }: { children: ReactNode }) {\n  const [messageApi, messageContext] = message.useMessage();\n  const [notificationApi, notificationContext] = notification.useNotification();\n  return (\n    <MessageContext.Provider\n      value={{\n        message: messageApi,\n        notification: notificationApi,\n      }}\n    >\n      {messageContext}\n      {notificationContext}\n      {children}\n    </MessageContext.Provider>\n  );\n}\n\nexport { MessageContext, MessageProvider };\n"
  },
  {
    "path": "client/src/providers/ThemeProvider.tsx",
    "content": "import { createContext, ReactNode, useEffect, useState } from 'react';\nimport { ConfigProvider, theme } from 'antd';\n\nenum AppTheme {\n  Light = 'light',\n  Dark = 'dark',\n}\n\ntype ThemeProviderType = {\n  theme: AppTheme;\n  themeChange: (theme: AppTheme) => void;\n  autoTheme: boolean;\n  changeAutoTheme: () => void;\n};\n\nconst ThemeContext = createContext<ThemeProviderType>({\n  theme: AppTheme.Light,\n  themeChange: () => {},\n  autoTheme: true,\n  changeAutoTheme: () => {},\n});\n\nconst ThemeProvider = ({ children }: { children: ReactNode }) => {\n  const [appTheme, setAppTheme] = useState<AppTheme>(AppTheme.Light);\n  const [auto, setAuto] = useState<boolean>(false);\n  const DARK_MODE_MEDIA_QUERY = '(prefers-color-scheme: dark)';\n\n  function getSystemTheme(): AppTheme {\n    return window.matchMedia(DARK_MODE_MEDIA_QUERY).matches ? AppTheme.Dark : AppTheme.Light;\n  }\n\n  function applyTheme(newTheme: AppTheme) {\n    const body = document.querySelector('body');\n    if (body) {\n      body.classList.remove(AppTheme.Light, AppTheme.Dark);\n      body.classList.add(newTheme);\n    }\n    setAppTheme(newTheme);\n  }\n\n  function toggleAppTheme(newTheme: AppTheme) {\n    applyTheme(newTheme);\n    setAuto(false);\n    localStorage.setItem('app-theme', newTheme);\n  }\n\n  function toggleAutoTheme() {\n    setAuto(prev => {\n      const newAutoState = !prev;\n      if (newAutoState) {\n        localStorage.setItem('app-theme', 'auto');\n        // FIXME: remove the line above and uncomment the line bellow\n        //  after enabling auto-theme\n        // localStorage.removeItem('app-theme');\n        applyTheme(getSystemTheme());\n      }\n      return newAutoState;\n    });\n  }\n\n  useEffect(() => {\n    if (!auto) return;\n\n    const mediaQuery = window.matchMedia(DARK_MODE_MEDIA_QUERY);\n    applyTheme(getSystemTheme());\n\n    const handleThemeChange = () => auto && applyTheme(getSystemTheme());\n    mediaQuery.addEventListener('change', handleThemeChange);\n\n    return () => mediaQuery.removeEventListener('change', handleThemeChange);\n  }, [auto]);\n\n  useEffect(() => {\n    const storedTheme = localStorage.getItem('app-theme') as AppTheme;\n    const isValidTheme = Object.values(AppTheme).includes(storedTheme);\n\n    if (isValidTheme) {\n      setAuto(false);\n      applyTheme(storedTheme);\n    } else {\n      // setAuto(true); // FIXME: temporary disable set auto theme by default\n    }\n\n    // FIXME: remove the if statement after enabling auto-theme above\n    if ((storedTheme as string) === 'auto') {\n      setAuto(true);\n    }\n  }, []);\n\n  const lightTheme = {\n    // Link color variants\n    colorLink: '#4466b3',\n    colorLinkHover: '#006bff',\n  };\n\n  const darkTheme = {\n    // Text\n    colorTextBase: '#ffffff',\n    colorText: '#ffffff',\n    colorTextSecondary: '#bfffffff',\n    colorTextLabel: '#c2d8d8',\n    colorTextDescription: '#b5ccfb',\n    colorTextPlaceholder: '#6c9cdf',\n    colorTextDisabled: '#546883',\n\n    // Link color variants\n    colorLink: '#5897ee',\n    colorLinkHover: '#9fc2f3',\n\n    // Background\n    colorBgContainer: '#151515',\n    colorBgContainerDisabled: '#142525ff',\n\n    // Border\n    colorBorder: '#434343',\n  };\n\n  return (\n    <ThemeContext.Provider\n      value={{\n        theme: appTheme,\n        themeChange: toggleAppTheme,\n        autoTheme: auto,\n        changeAutoTheme: toggleAutoTheme,\n      }}\n    >\n      <ConfigProvider\n        theme={{\n          token: appTheme === AppTheme.Dark ? darkTheme : lightTheme,\n          algorithm: appTheme === AppTheme.Dark ? theme.darkAlgorithm : theme.defaultAlgorithm,\n        }}\n      >\n        {children}\n      </ConfigProvider>\n    </ThemeContext.Provider>\n  );\n};\n\nexport { ThemeContext, ThemeProvider, AppTheme };\n"
  },
  {
    "path": "client/src/providers/index.ts",
    "content": "export * from './ThemeProvider';\nexport * from './MessageProvider';\nexport * from './DevToolsProvider';\n"
  },
  {
    "path": "client/src/reset.d.ts",
    "content": "import '@total-typescript/ts-reset';\n\ndeclare global {\n  interface URLSearchParams {\n    append(name: string, value: string | unknown): void;\n  }\n}\n"
  },
  {
    "path": "client/src/services/cdn.ts",
    "content": "import axios from 'axios';\nimport { CoursesApi } from '@client/api';\nimport type { CoursesResponse } from './courses';\n\nconst baseURL = process.env.CDN_HOST || '';\n\nconst coursesService = new CoursesApi();\n\nexport class CdnService {\n  constructor(private client = axios.create({ baseURL, withCredentials: !!baseURL })) {}\n\n  public async getCourses() {\n    try {\n      const result = await coursesService.getCourses();\n      return result.data;\n    } catch {\n      return [];\n    }\n  }\n\n  public async registerStudent(payload: unknown) {\n    const result = await this.client.post<CoursesResponse>(`/api/registry`, payload);\n    return result.data.data;\n  }\n\n  public async registerMentor(payload: unknown) {\n    const result = await this.client.post<CoursesResponse>(`/api/registry/mentor`, payload);\n    return result.data.data;\n  }\n}\n"
  },
  {
    "path": "client/src/services/check.ts",
    "content": "import globalAxios, { AxiosInstance } from 'axios';\nimport { message } from 'antd';\nimport { IBadReview, checkType } from '@client/modules/CrossCheckPairs/components/BadReview/BadReviewControllers';\n\ntype routesType = Exclude<checkType, 'No type'>;\n\nconst ROUTES: Record<routesType, string> = {\n  'Bad comment': 'badcomment',\n  'Did not check': 'maxscore',\n};\n\nexport class CheckService {\n  private axios: AxiosInstance;\n  private cache: Record<number, Record<routesType, IBadReview[]>>;\n\n  constructor() {\n    this.axios = globalAxios.create({ baseURL: `/api/` });\n    this.cache = {};\n  }\n\n  async getData(taskId: number, type: checkType, courseId: number) {\n    let dataFromService: IBadReview[] = [];\n    try {\n      switch (type) {\n        case 'Bad comment':\n        case 'Did not check':\n          dataFromService = await this.getDataFromServer(taskId, type, courseId);\n          break;\n        case 'No type':\n          break;\n        default:\n          throw new Error('Unsupported type');\n      }\n    } catch {\n      message.error('Something went wrong');\n    }\n    return dataFromService;\n  }\n\n  private async getDataFromServer(taskId: number, type: routesType, courseId: number) {\n    if (this.cache?.[taskId]?.[type]) return this.cache[taskId][type];\n    const result = await this.axios.get(`checks/${ROUTES[type]}/${courseId}/${taskId}`);\n    this.saveToCache(taskId, type, result.data.data);\n    return result.data.data;\n  }\n\n  private saveToCache(taskId: number, type: routesType, data: IBadReview[]) {\n    if (!this.cache[taskId]) {\n      this.cache[taskId] = {} as Record<routesType, IBadReview[]>;\n    }\n    this.cache[taskId][type] = data;\n  }\n}\n"
  },
  {
    "path": "client/src/services/course.ts",
    "content": "import globalAxios, { AxiosInstance } from 'axios';\nimport { UserBasic, MentorBasic, StudentBasic, InterviewDetails, InterviewPair } from '@common/models';\nimport { ScoreOrder, ScoreTableFilters } from '@client/modules/Score/hooks/types';\nimport { IPaginationInfo } from '@client/shared/utils/pagination';\n\nimport {\n  CoursesTasksApi,\n  CoursesEventsApi,\n  UpdateCourseEventDto,\n  CreateCourseEventDto,\n  StudentsScoreApi,\n  Discord,\n  CourseTaskDto,\n  EventDto,\n  CriteriaDto,\n  CrossCheckMessageDto,\n  CrossCheckCriteriaDataDto,\n  StudentsApi,\n  StudentSummaryDto,\n  CertificateApi,\n  CoursesInterviewsApi,\n  MentorDetailsDtoStudentsPreferenceEnum,\n} from '@client/api';\nimport { optionalQueryString } from '@client/utils/optionalQueryString';\nimport { Decision } from '@client/data/interviews/technical-screening';\nimport { InterviewStatus } from '@client/domain/interview';\n\nexport type CrossCheckCriteriaType = 'title' | 'subtask' | 'penalty';\n\nexport interface CrossCheckMessageAuthor {\n  id: number;\n  githubId: string;\n}\n\nexport interface Verification {\n  id: number;\n  createdDate: string;\n  courseTaskId: number;\n  courseTask: {\n    id: number;\n    task: {\n      name: string;\n    };\n    type: string;\n  };\n  details: string;\n  metadata: unknown[];\n  score: number;\n  status: string;\n  studentId: number;\n}\n\nexport interface SelfEducationPublicAttributes {\n  maxAttemptsNumber: number;\n  numberOfQuestions: number;\n  tresholdPercentage: number;\n  strictAttemptsMode?: boolean;\n  oneAttemptPerNumberOfHours?: number;\n  questions: SelfEducationQuestion[];\n}\n\nexport interface SelfEducationQuestion {\n  question: string;\n  answers: string[];\n  multiple: boolean;\n  questionImage?: string;\n  answersType?: 'image';\n}\n\nexport interface SelfEducationQuestionWithIndex extends SelfEducationQuestion {\n  index: number;\n}\n\nexport interface CourseTaskDetails extends CourseTaskDto {\n  description: string | null;\n  resultsCount: number;\n  taskOwner: { id: number; githubId: string; name: string } | null;\n}\n\nexport interface CourseEvent {\n  id: number;\n  event: EventDto & { disciplineId: number | null };\n  date?: string;\n  time?: string;\n  dateTime: string;\n  endTime?: string;\n  place: string;\n  comment: string;\n  eventId: number;\n  owner: string;\n  coordinator: string;\n  organizerId: number;\n  organizer: UserBasic | null;\n  detailsUrl: string;\n  broadcastUrl: string;\n  special?: string;\n  duration?: number;\n  score?: string;\n  done?: number;\n}\n\nexport type CrossCheckCriteria = {\n  type: CrossCheckCriteriaType;\n  title?: string;\n  text?: string;\n  max: number;\n  criteriaId: string;\n};\nexport type CrossCheckReview = { percentage: number; criteriaId: string };\nexport type CrossCheckComment = {\n  text: string;\n  criteriaId: string;\n  timestamp: number;\n  authorId?: number;\n  authorGithubId?: string;\n};\n\nexport type SearchStudent = UserBasic & { mentor: UserBasic | null };\n\nconst courseTasksApi = new CoursesTasksApi();\nconst courseEventsApi = new CoursesEventsApi();\nconst studentsScoreApi = new StudentsScoreApi();\nconst studentsApi = new StudentsApi();\nconst certificateApi = new CertificateApi();\nconst coursesInterviewsApi = new CoursesInterviewsApi();\n\nexport class CourseService {\n  private axios: AxiosInstance;\n\n  constructor(private courseId: number) {\n    this.axios = globalAxios.create({ baseURL: `/api/course/${this.courseId}` });\n  }\n\n  async getCourseCrossCheckTasks(status?: 'started' | 'inprogress' | 'finished') {\n    const { data } = await courseTasksApi.getCourseTasks(this.courseId, status);\n    return data.filter(t => t.checker === 'crossCheck');\n  }\n\n  async getCourseTasksDetails() {\n    type Response = { data: CourseTaskDetails[] };\n    const result = await this.axios.get<Response>('/tasks/details');\n    return result.data.data.sort(sortTasksByEndDate);\n  }\n\n  async getCourseEvents() {\n    const result = await this.axios.get<{ data: CourseEvent[] }>(`/events`);\n    return result.data.data;\n  }\n\n  async createCourseEvent(data: CreateCourseEventDto) {\n    const { organizer, ...rest } = data;\n    await courseEventsApi.createCourseEvent(this.courseId, {\n      organizer: organizer\n        ? {\n            id: organizer.id,\n          }\n        : undefined,\n      ...rest,\n    });\n  }\n\n  async updateCourseEvent(courseEventId: number, data: Partial<UpdateCourseEventDto>) {\n    const { organizer, ...rest } = data;\n    await courseEventsApi.updateCourseEvent(this.courseId, courseEventId, {\n      organizer: organizer\n        ? {\n            id: organizer.id,\n          }\n        : undefined,\n      ...rest,\n    });\n  }\n\n  async deleteCourseEvent(courseTaskId: number) {\n    await courseEventsApi.deleteCourseEvent(courseTaskId, this.courseId);\n  }\n\n  async getCourseStudents(activeOnly?: boolean) {\n    const result = await this.axios.get<{ data: StudentDetails[] }>(\n      `/students?status=${activeOnly ? 'active' : 'all'}`,\n    );\n    return result.data.data;\n  }\n\n  async getCourseStudentsWithDetails(activeOnly?: boolean) {\n    const result = await this.axios.get<{ data: StudentDetails[] }>(\n      `/students/details?status=${activeOnly ? 'active' : 'all'}`,\n    );\n    return result.data.data;\n  }\n\n  async searchStudents(query: string | null, onlyStudentsWithoutMentorShown = false) {\n    try {\n      if (!query) {\n        return [];\n      }\n      const response = await this.axios.get<{ data: SearchStudent[] }>(`/students/search/${query}`, {\n        params: { onlyStudentsWithoutMentorShown },\n      });\n      return response.data.data;\n    } catch {\n      return [];\n    }\n  }\n\n  async getCourseScore(\n    pagination: IPaginationInfo,\n    filter: ScoreTableFilters = { activeOnly: false },\n    orderBy: ScoreOrder = { field: 'totalScore', order: 'descend' },\n  ) {\n    const result = await studentsScoreApi.getScore(\n      String(filter.activeOnly),\n      orderBy.field,\n      orderBy.order === 'descend' ? 'desc' : 'asc',\n      String(pagination.current),\n      String(pagination.pageSize),\n      this.courseId,\n      optionalQueryString(filter.githubId),\n      optionalQueryString(filter.name),\n      optionalQueryString(filter['mentor.githubId']),\n      optionalQueryString(filter.cityName),\n    );\n    return result.data;\n  }\n\n  async getStudentCourseScore(githubId: string) {\n    const result = await studentsScoreApi.getStudentScore(this.courseId, githubId);\n    return result.data;\n  }\n\n  async postStudentScore(githubId: string, courseTaskId: number, data: PostScore) {\n    await this.axios.post(`/student/${githubId}/task/${courseTaskId}/result`, data);\n  }\n\n  async postMultipleScores(courseTaskId: number, data: unknown) {\n    const result = await this.axios.post(`/scores/${courseTaskId}`, data);\n    return result.data.data;\n  }\n\n  async getInterviewStudents(courseTaskId: number) {\n    const result = await this.axios.get<{ data: StudentBasic[] }>(`/mentor/me/interview/${courseTaskId}`);\n    return result.data.data;\n  }\n\n  async postStudentInterviewResult(githubId: string, courseTaskId: number, data: unknown) {\n    const result = await this.axios.post(`/student/${githubId}/interview/${courseTaskId}/result`, data);\n    return result.data.data;\n  }\n\n  async postPublicFeedback(data: { toUserId: number; badgeId?: string; comment: string }) {\n    type Response = { data: { heroesUrl: string } };\n    const result = await this.axios.post<Response>(`/feedback`, data);\n    return result.data.data;\n  }\n\n  async expelStudent(githubId: string, comment = '') {\n    await this.axios.post(`/student/${githubId}/status`, { comment, status: 'expelled' });\n  }\n\n  async setSelfStudy(githubId: string, comment = '') {\n    await this.axios.post(`/student/${githubId}/status`, { comment, status: 'self-study' });\n  }\n\n  async selfSetSelfStudy(githubId: string, comment = '') {\n    await this.axios.post(`/student/${githubId}/status-self`, { comment, status: 'self-study' });\n  }\n\n  async expelStudents(\n    criteria: { courseTaskIds?: number[]; minScore?: number },\n    options: { keepWithMentor?: boolean },\n    expellingReason: string,\n  ) {\n    await studentsApi.expelStudents(this.courseId, {\n      criteria,\n      options,\n      expellingReason,\n    });\n  }\n\n  async postCertificateStudents(criteria: { courseTaskIds?: number[]; minScore?: number; minTotalScore?: number }) {\n    await this.axios.post(`/certificates`, { criteria });\n  }\n\n  async restoreStudent(githubId: string) {\n    await this.axios.post(`/student/${githubId}/status`, { status: 'active' });\n  }\n\n  async postTaskSolution(\n    githubId: string,\n    courseTaskId: number,\n    url: string,\n    review?: CrossCheckReview[],\n    comments?: CrossCheckComment[],\n  ) {\n    await this.axios.post(`/student/${githubId}/task/${courseTaskId}/cross-check/solution`, {\n      url,\n      review,\n      comments,\n    });\n  }\n\n  async deleteTaskSolution(githubId: string, courseTaskId: number) {\n    await this.axios.delete(`/student/${githubId}/task/${courseTaskId}/cross-check/solution`);\n  }\n\n  async getCrossCheckTaskSolution(githubId: string, courseTaskId: number) {\n    const apiUrl = `/student/${githubId}/task/${courseTaskId}/cross-check/solution`;\n    const result = await this.axios.get(apiUrl);\n    return result.data.data as TaskSolution;\n  }\n\n  async postTaskSolutionResult(\n    githubId: string,\n    courseTaskId: number,\n    data: {\n      score: number;\n      comment: string;\n      anonymous: boolean;\n      review: CrossCheckReview[];\n      comments: CrossCheckComment[];\n      criteria: CrossCheckCriteriaDataDto[];\n    },\n  ) {\n    await this.axios.post(`/student/${githubId}/task/${courseTaskId}/cross-check/result`, data);\n  }\n\n  async getTaskSolutionResult(githubId: string, courseTaskId: number) {\n    const result = await this.axios.get(`/student/${githubId}/task/${courseTaskId}/cross-check/result`);\n    return result.data.data as {\n      id: number;\n      comments: CrossCheckComment[];\n      review: CrossCheckReview[];\n      anonymous: boolean;\n      studentId: number;\n      checkerId: number;\n      historicalScores: {\n        score: number;\n        comment: string;\n        dateTime: number;\n        anonymous: boolean;\n        criteria: CrossCheckCriteriaDataDto[];\n      }[];\n      author: {\n        id: number;\n        name: string;\n        discord: Discord | null;\n        githubId: string;\n      };\n      messages: CrossCheckMessageDto[];\n    } | null;\n  }\n\n  async postTaskSolutionResultMessages(\n    taskSolutionResultId: number,\n    courseTaskId: number,\n    data: {\n      content: string;\n      role: string;\n    },\n  ) {\n    await this.axios.post(\n      `/taskSolutionResult/${taskSolutionResultId}/task/${courseTaskId}/cross-check/messages`,\n      data,\n    );\n  }\n\n  async updateTaskSolutionResultMessages(\n    taskSolutionResultId: number,\n    courseTaskId: number,\n    data: {\n      role: string;\n    },\n  ) {\n    await this.axios.put(`/taskSolutionResult/${taskSolutionResultId}/task/${courseTaskId}/cross-check/messages`, data);\n  }\n\n  async getCrossCheckTaskDetails(courseTaskId: number) {\n    const result = await this.axios.get(`/task/${courseTaskId}/cross-check/details`);\n    return result.data.data as {\n      criteria: CrossCheckCriteria[];\n      studentEndDate: string | undefined;\n    } | null;\n  }\n\n  async getTaskVerifications() {\n    const result = await this.axios.get(`/student/me/tasks/verifications`);\n    return result.data.data;\n  }\n\n  async getStageInterviews() {\n    const result = await this.axios.get(`/interviews/stage`);\n    return result.data.data;\n  }\n\n  async createStageInterviews(params: { noRegistration: boolean }) {\n    const result = await this.axios.post(`/interviews/stage`, params);\n    return result.data.data;\n  }\n\n  async createInterview(githubId: string, mentorGithubId: string) {\n    const result = await this.axios.post(`/interview/stage/interviewer/${mentorGithubId}/student/${githubId}`);\n    return result.data.data;\n  }\n\n  async updateMentoringAvailability(githubId: string, mentoring: boolean) {\n    const result = await this.axios.post(`/student/${githubId}/availability`, { mentoring });\n    return result.data.data;\n  }\n\n  async deleteStageInterview(interviewId: number) {\n    const result = await this.axios.delete(`/interview/stage/${interviewId}`);\n    return result.data.data;\n  }\n\n  async updateStageInterview(interviewId: number, data: { githubId: string }) {\n    const result = await this.axios.put(`/interview/stage/${interviewId}`, data);\n    return result.data.data;\n  }\n\n  /**\n   * @deprecated. should be removed after feedbacks are migrated to new template\n   */\n  async getInterviewerStageInterviews(githubId: string) {\n    const result = await this.axios.get(`/interview/stage/interviewer/${githubId}/students`);\n    return result.data.data as { id: number; completed: boolean; student: StudentBasic }[];\n  }\n\n  /**\n   * @deprecated. should be removed after feedbacks are migrated to new template\n   */\n  async postStageInterviewFeedback(\n    interviewId: number,\n    data: { json: unknown; githubId: string; isGoodCandidate: boolean; isCompleted: boolean; decision: string },\n  ) {\n    const result = await this.axios.post(`/interview/stage/${interviewId}/feedback`, data);\n    return result.data.data;\n  }\n\n  /**\n   * @deprecated. should be removed after feedbacks are migrated to new template\n   */\n  async getStageInterviewFeedback(interviewId: number) {\n    const result = await this.axios.get(`/interview/stage/${interviewId}/feedback`);\n\n    return result.data.data;\n  }\n\n  async createRepository(githubId: string) {\n    type Response = { data: { repository: string } };\n    const result = await this.axios.post<Response>(`/student/${githubId}/repository`);\n    return result.data.data;\n  }\n\n  async postSyncRepositoriesMentors() {\n    await this.axios.post(`/repositories/mentors`);\n  }\n\n  async expelMentor(githubId: string) {\n    await this.axios.post(`/mentor/${githubId}/status/expelled`);\n  }\n\n  async restoreMentor(githubId: string) {\n    await this.axios.post(`/mentor/${githubId}/status/restore`);\n  }\n\n  async getCrossCheckAssignments(githubId: string, courseTaskId: number) {\n    const result = await this.axios.get<{\n      data: {\n        student: StudentBasic;\n        url: string;\n      }[];\n    }>(`/student/${githubId}/task/${courseTaskId}/cross-check/assignments`);\n    return result.data.data;\n  }\n\n  async createCrossCheckDistribution(courseTaskId: number) {\n    const result = await this.axios.post(`/task/${courseTaskId}/cross-check/distribution`);\n    return result.data;\n  }\n\n  async createInterviewDistribution(courseTaskId: number) {\n    const result = await coursesInterviewsApi.distributeInterviewPairs(this.courseId, courseTaskId, {\n      clean: false,\n      registrationEnabled: true,\n    });\n    return result.data;\n  }\n\n  async createTaskDistribution(courseTaskId: number) {\n    const result = await this.axios.post(`/task/${courseTaskId}/distribution`);\n    return result.data;\n  }\n\n  async createCrossCheckCompletion(courseTaskId: number) {\n    const result = await this.axios.post(`/task/${courseTaskId}/cross-check/completion`);\n    return result.data;\n  }\n\n  async getStudentSummary(githubId: string) {\n    const result = await studentsApi.getStudentSummary(this.courseId, githubId);\n    return result.data as StudentSummaryDto;\n  }\n\n  async getStudentScore(githubId: string) {\n    const result = await this.axios.get(`/student/${githubId}/score`);\n    return result.data.data as { totalScore: number; results: { courseTaskId: number; score: number }[] };\n  }\n\n  async getStudentInterviews(githubId: string) {\n    const result = await this.axios.get(`/student/${githubId}/interviews`);\n    return result.data.data as InterviewDetails[];\n  }\n\n  async createCertificate(githubId: string) {\n    const result = await this.axios.post(`/student/${githubId}/certificate`);\n    return result.data.data;\n  }\n\n  async removeCertificate(studentId: number) {\n    await certificateApi.removeCertificate(studentId);\n  }\n\n  async getMentorInterviews(githubId: string) {\n    const result = await this.axios.get<{ data: MentorInterview[] }>(`/mentor/${githubId}/interviews`);\n    return result.data.data;\n  }\n\n  async createMentor(\n    githubId: string,\n    data: {\n      students: string[];\n      maxStudentsLimit: number;\n      preferedStudentsLocation: MentorDetailsDtoStudentsPreferenceEnum;\n    },\n  ) {\n    const result = await this.axios.post(`/mentor/${githubId}`, data);\n    return result.data.data;\n  }\n\n  async updateStudent(githubId: string, data: { mentorGithuId: string | null }) {\n    const result = await this.axios.put(`/student/${githubId}`, data);\n    return result.data.data as StudentBasic;\n  }\n\n  async unassignStudentFromMentor(githubId: string, data: { mentorGithuId: null; unassigningComment: string }) {\n    const result = await this.axios.put(`/student/${githubId}`, data);\n    return result.data.data;\n  }\n\n  async getInterviewStudent(githubId: string, interviewId: string) {\n    const result = await this.axios.get(`/student/${githubId}/interview/${interviewId}`);\n    return result.data.data as { id: number } | null;\n  }\n\n  async getInterviewPairs(interviewId: string) {\n    const result = await this.axios.get(`/interviews/${interviewId}`);\n    return result.data.data as InterviewPair[];\n  }\n\n  async cancelInterviewPair(interviewId: string, pairId: string) {\n    const result = await this.axios.delete(`/interviews/${interviewId}/${pairId}`);\n    return result.data.data;\n  }\n\n  async addInterviewPair(interviewId: string, interviewerGithubId: string, studentGithubId: string) {\n    const result = await this.axios.post(\n      `/interview/${interviewId}/interviewer/${interviewerGithubId}/student/${studentGithubId}`,\n    );\n    return result.data.data as { id: string };\n  }\n\n  async sendInviteRepository(githubId: string) {\n    const result = await this.axios.post(`/student/${githubId}/repository`);\n    return result.data.data;\n  }\n\n  exportStudentsCsv(activeOnly?: boolean) {\n    window.open(`${this.axios.defaults.baseURL}/students/csv?status=${activeOnly ? 'active' : ''}`, '_blank');\n  }\n}\n\nexport interface StudentDetails extends StudentBasic {\n  countryName: string;\n  cityName: string;\n  totalScore: number;\n  repository: string;\n  interviews: { id: number; isCompleted: boolean }[];\n}\n\nexport interface PostScore {\n  score: number;\n  comment?: string;\n  githubPrUrl?: string;\n}\n\nexport interface StudentSummary {\n  totalScore: number;\n  results: { score: number; courseTaskId: number }[];\n  isActive: boolean;\n  mentor:\n    | (MentorBasic & {\n        contactsEmail?: string;\n        contactsPhone?: string;\n        contactsSkype?: string;\n        contactsTelegram?: string;\n        contactsNotes?: string;\n        contactsWhatsApp?: string;\n      })\n    | null;\n  rank: number;\n  repository?: string | null;\n}\n\nexport interface TaskSolution {\n  url: string;\n  updatedDate: string;\n  id: string;\n  review?: CrossCheckReview[];\n  comments?: CrossCheckComment[];\n  studentId: number;\n}\n\nexport interface IAddCriteriaForCrossCheck {\n  onCreate: (data: CriteriaDto) => void;\n}\n\nconst sortTasksByEndDate = (a: CourseTaskDetails, b: CourseTaskDetails) => {\n  if (!b.studentEndDate && a.studentEndDate) {\n    return -1;\n  }\n  if (!a.studentEndDate && b.studentEndDate) {\n    return 1;\n  }\n  if (!a.studentEndDate && !b.studentEndDate) {\n    return 0;\n  }\n  return new Date(a.studentEndDate!).getTime() - new Date(b.studentEndDate!).getTime();\n};\n\nexport type MentorInterview = {\n  name: string;\n  endDate: string;\n  completed: boolean;\n  interviewer: unknown;\n  status: InterviewStatus;\n  student: UserBasic;\n  decision?: Decision;\n  id: number;\n};\n"
  },
  {
    "path": "client/src/services/courses.ts",
    "content": "import { CourseDto as Course, CoursesApi, CreateCourseDto } from '@client/api';\n\nexport type CoursesResponse = { data: Course[] };\n\nexport class CoursesService {\n  private coursesApi: CoursesApi;\n\n  constructor() {\n    this.coursesApi = new CoursesApi();\n  }\n\n  async createCourse(data: CreateCourseDto) {\n    const result = await this.coursesApi.createCourse(data);\n    return result.data;\n  }\n\n  async createCourseCopy(data: CreateCourseDto, id: number): Promise<Course> {\n    const result = await this.coursesApi.copyCourse(id, data);\n    return result.data;\n  }\n\n  async getCourses() {\n    const result = await this.coursesApi.getCourses();\n    return result.data;\n  }\n\n  async getCourse(id: number) {\n    const result = await this.coursesApi.getCourse(id);\n    return result.data;\n  }\n}\n"
  },
  {
    "path": "client/src/services/features.tsx",
    "content": "type Toggles = ReturnType<typeof getInitialFeatureToggles>;\nexport type FeatureName = keyof Toggles;\n\nexport let featureToggles: Toggles = getInitialFeatureToggles();\n\nexport function initializeFeatures(query: Record<string, string | string[] | undefined>): void {\n  featureToggles = getInitialFeatureToggles();\n  for (const key in featureToggles) {\n    const featureName = key as FeatureName;\n    const value = query[featureName] as string;\n\n    featureToggles[featureName] = value ? value === 'on' : featureToggles[key as FeatureName];\n  }\n}\n\nfunction getInitialFeatureToggles() {\n  return {\n    feedback: true,\n  };\n}\n"
  },
  {
    "path": "client/src/services/files.ts",
    "content": "import globalAxios, { AxiosInstance } from 'axios';\n\nexport class FilesService {\n  private axios: AxiosInstance;\n\n  constructor() {\n    this.axios = globalAxios.create({ baseURL: `/api` });\n  }\n\n  async uploadFile(key: string, data: string) {\n    const result = await this.axios.post(`/file/upload?key=${key}`, JSON.parse(data));\n    return result.data.data as { s3Key: string };\n  }\n}\n"
  },
  {
    "path": "client/src/services/formatter.ts",
    "content": "import dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport timezone from 'dayjs/plugin/timezone';\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\nexport function formatDate(value: string) {\n  return dayjs(value).format('YYYY-MM-DD');\n}\n\nexport function formatShortDate(value: string) {\n  return dayjs(value).format('MMM DD');\n}\n\nexport function relativeDays(value: string) {\n  return dayjs().diff(dayjs(value), 'day');\n}\n\nexport function formatDateTime(value: string | number) {\n  return dayjs(value).format('YYYY-MM-DD HH:mm');\n}\n\nexport function formatTimezoneToUTC(value: dayjs.ConfigType, zone = 'UTC') {\n  return dayjs(value).tz(zone, true).utc().format();\n}\n\nexport function formatTime(value: string) {\n  return dayjs(value).format('HH:mm:ssZ');\n}\n\nexport function formatDateFriendly(value: string) {\n  return dayjs(value).format('DD MMM YYYY');\n}\n\nexport function formatMonth(value: string) {\n  return dayjs(value).format('YYYY-MM');\n}\n\nexport function formatMonthFriendly(value: string) {\n  return dayjs(value).format('MMM YYYY');\n}\n"
  },
  {
    "path": "client/src/services/gratitude.ts",
    "content": "import { IPaginationInfo } from '@client/shared/utils/pagination';\nimport axios from 'axios';\n\nexport type HeroesFormData = {\n  name?: string;\n  githubId?: string;\n  courseId?: number;\n};\n\nexport interface IGratitudeGetResponse {\n  activist: boolean;\n  cityName: string;\n  countryName: string;\n  comment: string;\n  badgeId: string;\n  date: string;\n  id: number;\n  firstName: string;\n  githubId: string;\n  lastName: string;\n  user_id: number;\n  from: {\n    firstName: string;\n    githubId: string;\n    lastName: string;\n  };\n}\n\nexport type IGratitudeGetRequest = HeroesFormData & Partial<IPaginationInfo>;\n\nexport class GratitudeService {\n  async getGratitude(data?: IGratitudeGetRequest): Promise<{ content: IGratitudeGetResponse[]; count: number }> {\n    const result = await axios.get(`/api/feedback/gratitude`, { params: data });\n    return result.data.data;\n  }\n}\n"
  },
  {
    "path": "client/src/services/mentorRegistry.ts",
    "content": "import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';\nimport { MentorRegistryDto, RegistryApi, InviteMentorsDto, MentorDetailsDtoStudentsPreferenceEnum } from '@client/api';\nimport { MentorRegistryTabsMode } from '@client/modules/MentorRegistry/constants';\n\nexport type MentorResponse = {\n  preselectedCourses: number[];\n  maxStudentsLimit: number;\n  preferedStudentsLocation: MentorDetailsDtoStudentsPreferenceEnum;\n  preferredCourses: number[];\n};\n\nexport interface MentorRegistry {\n  maxStudentsLimit: number;\n  name: string;\n  githubId: string;\n  cityName: string;\n  updatedDate: Date;\n}\n\nexport interface GetMentorRegistriesDto {\n  status: MentorRegistryTabsMode;\n  currentPage: number;\n  pageSize: number;\n  githubId?: string;\n  cityName?: string;\n  preferedCourses?: number[];\n  preselectedCourses?: number[];\n  technicalMentoring?: string[];\n}\n\nexport interface GetMentorRegistriesResponse {\n  mentors: MentorRegistryDto[];\n  total: number;\n}\n\nexport interface GetMentorRegistriesOptions {\n  currentPage?: number;\n  pageSize?: number;\n  githubId?: string;\n  cityName?: string;\n  preferedCourses?: number[];\n  preselectedCourses?: number[];\n  technicalMentoring?: string[];\n}\n\nexport class MentorRegistryService {\n  private axios: AxiosInstance;\n  private registryApi: RegistryApi;\n\n  constructor() {\n    this.axios = axios.create({ baseURL: `/api/registry` });\n    this.registryApi = new RegistryApi();\n  }\n\n  public async getMentors(options?: GetMentorRegistriesDto): Promise<GetMentorRegistriesResponse> {\n    if (!options) {\n      const response = await this.registryApi.getMentorRegistries();\n      return response.data;\n    }\n    const response = await this.registryApi.getMentorRegistries(\n      options.status,\n      options.pageSize,\n      options.currentPage,\n      options.githubId,\n      options.cityName,\n      options.preferedCourses,\n      options.preselectedCourses,\n      options.technicalMentoring,\n    );\n    return response.data;\n  }\n\n  public async updateMentor(githubId: string, data: { preselectedCourses: string[] }) {\n    await this.registryApi.approveMentor(githubId, {\n      preselectedCourses: data.preselectedCourses,\n    });\n  }\n\n  public async cancelMentorRegistry(githubId: string) {\n    await this.registryApi.cancelMentorRegistry(githubId);\n  }\n\n  public async sendCommentMentorRegistry(githubId: string, comment: string) {\n    await this.registryApi.commentMentorRegistry(githubId, { comment: comment });\n  }\n\n  public async getMentor() {\n    try {\n      const response = await this.axios.get<AxiosResponse<MentorResponse>>(`/mentor`);\n      return response.data.data;\n    } catch (e) {\n      if (e instanceof AxiosError && e.response?.status === 404) {\n        console.info('Mentor is not found in the mentor registry.');\n        return null;\n      }\n      throw e;\n    }\n  }\n\n  public async inviteMentors(data: InviteMentorsDto) {\n    await this.registryApi.inviteMentors(data);\n  }\n}\n"
  },
  {
    "path": "client/src/services/models.ts",
    "content": "import { Session } from '@client/components/withSession';\nimport { StudentBasic as CommonStudentBasic } from '@common/models';\nimport { ProfileCourseDto, UserGroupDtoRolesEnum as CourseRole } from '@client/api';\n\nexport type Course = ProfileCourseDto;\nexport type StudentBasic = CommonStudentBasic;\n\nexport { CourseRole };\n\nexport interface CoursePageProps {\n  session?: Session;\n  course: Course;\n  params?: Record<string, string>;\n}\n\nexport type CourseOnlyPageProps = {\n  course: Course;\n  params?: Record<string, string>;\n};\n"
  },
  {
    "path": "client/src/services/reference-data/stageInterview.ts",
    "content": "export const SKILLS_LEVELS = [\n  `Doesn't know`,\n  `Poor knowledge (almost doesn't know)`,\n  'Knows something (with tips)',\n  'Good knowledge (makes not critical mistakes)',\n  'Great knowledge',\n];\n\nexport const CODING_LEVELS = [\n  `Isn't able to coding`,\n  `Poor coding ability (almost isn't able to)`,\n  'Can code with tips',\n  'Good coding ability (makes not critical mistakes)',\n  'Great coding ability',\n];\n\nexport enum SKILL_NAME {\n  htmlCss = 'HTML/CSS',\n  dataStructures = 'Data structures',\n  common = 'Common of CS / Programming',\n}\n"
  },
  {
    "path": "client/src/services/routes.ts",
    "content": "import type { UrlObject } from 'url';\n\nexport const getStudentFeedbackRoute = (course: string, studentId: number): UrlObject => {\n  return {\n    pathname: `/course/mentor/feedback`,\n    query: {\n      course,\n      studentId,\n    },\n  };\n};\n\nexport const getExpelRoute = (course: string): UrlObject => {\n  return {\n    pathname: `/course/mentor/expel-student`,\n    query: {\n      course,\n    },\n  };\n};\n\nexport const getAutoTestTaskRoute = (course: string, courseTaskId: number): UrlObject => ({\n  pathname: '/course/student/auto-test/task',\n  query: {\n    course,\n    courseTaskId,\n  },\n});\n\nexport const getAutoTestRoute = (alias: string): string => `/course/student/auto-test?course=${alias}`;\n"
  },
  {
    "path": "client/src/services/user.ts",
    "content": "import { EnglishLevel } from '@common/models';\nimport { ProfileApi, ProfileDto, UsersNotificationsApi, UpdateUserDtoLanguagesEnum } from '@client/api';\nimport discordIntegration from '../configs/discord-integration';\nimport type {\n  ConfigurableProfilePermissions,\n  Contacts,\n  Discord,\n  GeneralInfo,\n  Location,\n  MentorStats,\n  PublicFeedback,\n  StageInterviewDetailedFeedback,\n  StudentStats,\n} from '@common/models/profile';\nimport { Rule } from 'antd/lib/form';\nimport axios, { AxiosInstance } from 'axios';\n\nexport interface UserBasic {\n  name: string;\n  githubId: string;\n  id: number;\n}\n\ntype SearchResponse = { data: UserBasic[] };\n\nconst profileApi = new ProfileApi();\nconst usersApi = new UsersNotificationsApi();\n\nexport class UserService {\n  private axios: AxiosInstance;\n\n  constructor() {\n    this.axios = axios.create();\n  }\n\n  async getDiscordIds() {\n    const fragment = new URLSearchParams(window.location.hash.slice(1));\n\n    if (fragment.has('access_token')) {\n      const accessToken = fragment.get('access_token');\n      const tokenType = fragment.get('token_type');\n\n      const response = await fetch(discordIntegration.api.me, {\n        headers: {\n          authorization: `${tokenType} ${accessToken}`,\n        },\n      });\n\n      const { username, discriminator, id } = (await response.json()) as {\n        username: string;\n        discriminator: string;\n        id: string;\n      };\n\n      return {\n        username,\n        discriminator,\n        id,\n      };\n    }\n\n    return null;\n  }\n\n  async getCourses() {\n    const { data } = await profileApi.getUserCourses('me');\n    return data;\n  }\n\n  async searchUser(query: string | null) {\n    try {\n      if (!query) {\n        return [];\n      }\n      const response = await this.axios.get<SearchResponse>(`/api/users/search/${query}`);\n      return response.data.data;\n    } catch {\n      return [];\n    }\n  }\n\n  async getMyProfile() {\n    const response = await this.axios.get<{ data: UserFull }>(`/api/profile/me`);\n    return response.data.data;\n  }\n\n  async getProfileInfo(githubId?: string) {\n    const response = await this.axios.get<{ data: ProfileInfo }>(`/api/profile/info`, {\n      params: { githubId },\n    });\n    return response.data.data;\n  }\n\n  async sendEmailConfirmationLink() {\n    return usersApi.sendEmailConfirmationLink();\n  }\n}\n\nexport type ResponseStudent = {\n  id: number;\n  totalScore: number;\n  certificatePublicId: string;\n  completed: boolean;\n  stageInterviews: {\n    date: string;\n    isGoodCandidate: boolean;\n    english: EnglishLevel;\n    comment: string;\n    rating: number;\n    programmingTask: {\n      task: string;\n      resolved: number;\n      codeWritingLevel: number;\n      comment: string;\n    };\n    interviewer: {\n      githubId: string;\n      name: string;\n    };\n    skills: {\n      htmlCss: number;\n      common: number;\n      dataStructures: number;\n    };\n  }[];\n  interviews: {\n    score: number;\n    comment: string;\n    formAnswers: {\n      questionText: string;\n      answer: string;\n    }[];\n    courseTask: {\n      id: number;\n      name: string;\n      descriptionUrl: string;\n    };\n  }[];\n  taskResults: {\n    score: number;\n    githubPrUrl: string;\n    comment: string;\n    courseTask: {\n      id: number;\n      name: string;\n      descriptionUrl: string;\n    };\n  }[];\n  mentor: {\n    id: number;\n    githubId: string;\n    name: string;\n  } | null;\n};\n\nexport type ResponseMentor = {\n  id: number;\n  students: {\n    id: number;\n    userId: number;\n    githubId: string;\n    lastName: string;\n    firstName: string;\n  }[];\n};\n\nexport type ResponseCourse = {\n  id: number;\n  name: string;\n};\n\nexport interface UserFull extends UserBasic {\n  firstName: string;\n  lastName: string;\n  externalAccounts: unknown[];\n  englishLevel: string;\n  readyFullTime: string;\n  educationHistory: unknown[];\n  employmentHistory: unknown[];\n  contactsTelegram: string;\n  contactsSkype: string;\n  contactsWhatsApp: string;\n  contactsEmail: string;\n  primaryEmail: string;\n  contactsEpamEmail: string;\n  contactsPhone: string;\n  contactsNotes: string;\n  locationId: number;\n  locationName: string;\n  aboutMyself: string;\n  tshirtSize: string;\n  countryName: string;\n  cityName: string;\n  languages: UpdateUserDtoLanguagesEnum[];\n  discord?: string;\n}\n\nexport interface ProfileResponse {\n  user: UserFull;\n  students: (ResponseStudent & { course: ResponseCourse })[];\n  mentors: (ResponseMentor & { course: ResponseCourse })[];\n}\n\nexport type ProfileInfo = {\n  permissionsSettings?: ConfigurableProfilePermissions;\n  generalInfo?: GeneralInfo;\n  contacts?: Contacts;\n  mentorStats?: MentorStats[];\n  studentStats?: StudentStats[];\n  publicFeedback?: PublicFeedback[];\n  stageInterviewFeedback?: StageInterviewDetailedFeedback[];\n  discord: Discord | null;\n} & ProfileDto;\n\nexport type ProfileMainCardData = {\n  location: Location | null;\n  name: string;\n  githubId: string | null;\n  publicCvUrl: string | null;\n};\n\nexport const enum ContactsKeys {\n  EpamEmail = 'epamEmail',\n  Email = 'email',\n  Telegram = 'telegram',\n  Phone = 'phone',\n  Skype = 'skype',\n  WhatsApp = 'whatsApp',\n  Notes = 'notes',\n  LinkedIn = 'linkedIn',\n}\n\nexport type Contact = {\n  name: string;\n  value: string | null;\n  key: ContactsKeys;\n  rules?: Rule[];\n};\n"
  },
  {
    "path": "client/src/services/validators.test.ts",
    "content": "import {\n  emailPattern,\n  epamEmailPattern,\n  englishNamePattern,\n  phonePattern,\n  urlWithIpPattern,\n  notGithubPattern,\n  githubPrUrl,\n  githubRepoUrl,\n  githubUsernamePattern,\n  notUrlPattern,\n  passwordPattern,\n} from './validators';\n\ndescribe('email pattern', () => {\n  it.each`\n    input             | match\n    ${'a@b.c'}        | ${true}\n    ${'a_b@epam.com'} | ${false}\n    ${'a@b.cd'}       | ${true}\n    ${'a@b'}          | ${false}\n  `('returns $match for $input', ({ input, match }) => {\n    expect(emailPattern.test(input)).toBe(match);\n  });\n});\n\ndescribe('epam email pattern', () => {\n  it.each`\n    input             | match\n    ${'a@b.c'}        | ${false}\n    ${'a_b@epam.com'} | ${true}\n    ${'a@b.cd'}       | ${false}\n  `('returns $match for $input', ({ input, match }) => {\n    expect(epamEmailPattern.test(input)).toBe(match);\n  });\n});\n\ndescribe('english name pattern', () => {\n  it.each`\n    input     | match\n    ${'abc'}  | ${true}\n    ${'abc#'} | ${false}\n    ${'abć'}  | ${false}\n    ${'абв'}  | ${false}\n  `('returns $match for $input', ({ input, match }) => {\n    expect(englishNamePattern.test(input)).toBe(match);\n  });\n});\n\ndescribe('phone pattern', () => {\n  it.each`\n    input             | match\n    ${'+1234567890'}  | ${true}\n    ${'+12345678901'} | ${false}\n    ${'1234567890'}   | ${false}\n    ${'+12'}          | ${false}\n    ${'+0123'}        | ${false}\n  `('returns $match for $input', ({ input, match }) => {\n    expect(phonePattern.test(input)).toBe(match);\n  });\n});\n\ndescribe('url with ip pattern', () => {\n  it.each`\n    url                                      | match\n    ${'http://google.com'}                   | ${true}\n    ${'https://google.com'}                  | ${true}\n    ${'ftp://google.com'}                    | ${true}\n    ${'http://192.168.1.1'}                  | ${true}\n    ${'http://username:password@google.com'} | ${true}\n    ${'not a url'}                           | ${false}\n  `('returns $match for $url', ({ url, match }) => {\n    expect(urlWithIpPattern.test(url)).toBe(match);\n  });\n});\n\ndescribe('not github pattern', () => {\n  it.each`\n    url                     | match\n    ${'https://google.com'} | ${true}\n    ${'https://github.com'} | ${false}\n    ${'not a url'}          | ${true}\n  `('returns $match for $url', ({ url, match }) => {\n    expect(notGithubPattern.test(url)).toBe(match);\n  });\n});\n\ndescribe('github pr url', () => {\n  it.each`\n    url                                      | match\n    ${'https://github.com/user/repo/pull/1'} | ${true}\n    ${'https://github.com/user/repo'}        | ${false}\n    ${'not a url'}                           | ${false}\n  `('returns $match for $url', ({ url, match }) => {\n    expect(githubPrUrl.test(url)).toBe(match);\n  });\n});\n\ndescribe('github repo url', () => {\n  it.each`\n    url                                      | match\n    ${'https://github.com/user/repo'}        | ${true}\n    ${'https://github.com/user/repo/pull/1'} | ${false}\n    ${'not a url'}                           | ${false}\n  `('returns $match for $url', ({ url, match }) => {\n    expect(githubRepoUrl.test(url)).toBe(match);\n  });\n});\n\ndescribe('githubUsernamePattern', () => {\n  it.each`\n    username          | match    | reason\n    ${'validuser'}    | ${true}  | ${'basic valid username'}\n    ${'user-name'}    | ${true}  | ${'single dash in the middle'}\n    ${'user123'}      | ${true}  | ${'alphanumeric'}\n    ${'1user'}        | ${true}  | ${'starts with number'}\n    ${'a'.repeat(39)} | ${true}  | ${'max allowed length'}\n    ${'user--name'}   | ${false} | ${'double dash not allowed'}\n    ${'-username'}    | ${false} | ${'cannot start with dash'}\n    ${'username-'}    | ${false} | ${'cannot end with dash'}\n    ${'_username'}    | ${false} | ${'underscore not allowed'}\n    ${'user_name'}    | ${false} | ${'underscore not allowed anywhere'}\n    ${'user name'}    | ${false} | ${'spaces not allowed'}\n    ${'user.name'}    | ${false} | ${'dot not allowed'}\n    ${'a'.repeat(40)} | ${false} | ${'exceeds max length'}\n  `('returns $match for \"$username\" ($reason)', ({ username, match }) => {\n    expect(githubUsernamePattern.test(username)).toBe(match);\n  });\n});\n\ndescribe('not url pattern', () => {\n  it.each`\n    string         | match\n    ${'/a/url'}    | ${false}\n    ${'not a url'} | ${true}\n  `('returns $match for $string', ({ string, match }) => {\n    expect(notUrlPattern.test(string)).toBe(match);\n  });\n});\n\ndescribe('password pattern', () => {\n  it.each`\n    password            | isValid\n    ${'1234_abcd'}      | ${true}\n    ${'5678_EFGH'}      | ${true}\n    ${'_abcd'}          | ${false}\n    ${'1234abcd'}       | ${false}\n    ${'1234_abcd_efgh'} | ${false}\n    ${'abc_1a2w'}       | ${false}\n  `('returns $isValid for $password', ({ password, isValid }) => {\n    expect(password.match(passwordPattern) !== null).toBe(isValid);\n  });\n});\n"
  },
  {
    "path": "client/src/services/validators.ts",
    "content": "export const emailPattern = /[^@]+@[^.]+\\..+/g;\nexport const epamEmailPattern = /[^@]+_[^@]+@epam.com/gi;\n\n// eslint-disable-next-line no-control-regex\nexport const englishNamePattern = /^[\\x00-\\x7F]+$/g;\nexport const phonePattern = /^\\+[1-9]{1}[0-9]{3,14}$/gi;\nexport const urlPattern = /^(http|https):\\/\\/.+(\\.[a-z]{2,10})/g;\nexport const urlWithIpPattern = /(ftp|http|https):\\/\\/(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(\\/|\\/([\\w#!:.?+=&%@!\\-/]))?/;\n\nexport const notGithubPattern = /^((?!https:\\/\\/github.com\\/).)*$/gi;\nexport const privateRsRepoPattern = /https:\\/\\/github\\.com\\/rolling-scopes-school\\//;\n\nexport const githubPrUrl = /https:\\/\\/github.com\\/(\\w|\\d|-)+\\/(\\w|\\d|-)+\\/pull\\/(\\d)+/gi;\nexport const githubRepoUrl = /https:\\/\\/github.com\\/(\\w|\\d|-)+\\/(\\w|\\d|-)+/gi;\nexport const githubUsernamePattern = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,37}[a-zA-Z0-9]$/;\n\nexport const notUrlPattern = /^((?!\\/).)*$/g;\n\nexport const passwordPattern = /^\\d+_[a-zA-Z0-9]+$/;\n\nexport const weAreCommunityUrlPattern = /^(https?:\\/\\/)?(www\\.)?wearecommunity\\.io.*$/;\nexport const rsAppRegistryUrlPattern = /^(https?:\\/\\/)?(www\\.)?app\\.rs\\.school\\/registry\\/student\\?course=.+$/;\n"
  },
  {
    "path": "client/src/setupTests.ts",
    "content": "import '@testing-library/jest-dom/vitest';\nimport matchMediaPolyfill from 'mq-polyfill';\n\nmatchMediaPolyfill(window);\n\n// antd v6 @rc-component/util calls getComputedStyle with pseudoElt argument.\n// jsdom does not support the second argument and throws \"Not implemented\".\n// Provide a stub that returns an empty CSSStyleDeclaration-like object.\nconst origGetComputedStyle = window.getComputedStyle;\nwindow.getComputedStyle = (elt: Element, pseudoElt?: string | null) => {\n  if (pseudoElt) {\n    return {} as CSSStyleDeclaration;\n  }\n  return origGetComputedStyle(elt);\n};\n\n// antd v6 uses CSS.supports for feature detection and animations.\n// jsdom does not include CSS.supports, so we provide a no-op stub.\nif (typeof globalThis.CSS === 'undefined') {\n  // @ts-expect-error partial polyfill for jsdom\n  globalThis.CSS = { supports: () => false };\n} else if (typeof globalThis.CSS.supports !== 'function') {\n  globalThis.CSS.supports = () => false;\n}\n\n// antd v6 uses ResizeObserver internally via @rc-component/resize-observer.\n// jsdom does not include ResizeObserver, so we provide a mock on global.\nif (typeof global.ResizeObserver === 'undefined') {\n  global.ResizeObserver = class ResizeObserver {\n    observe() {}\n    unobserve() {}\n    disconnect() {}\n  };\n}\n\n// antd v6 uses MessageChannel via @rc-component/form for batching form updates.\n// jsdom does not include MessageChannel, so we provide a working mock implementation on global.\nif (typeof global.MessageChannel === 'undefined') {\n  class MockMessagePort {\n    onmessage: ((event: { data: unknown }) => void) | null = null;\n    private _partner: MockMessagePort | null = null;\n    _link(partner: MockMessagePort) {\n      this._partner = partner;\n    }\n    postMessage(data: unknown) {\n      const partner = this._partner;\n      if (partner?.onmessage) {\n        setTimeout(() => partner.onmessage?.({ data }), 0);\n      }\n    }\n    start() {}\n    close() {}\n  }\n  class MockMessageChannel {\n    port1: MockMessagePort;\n    port2: MockMessagePort;\n    constructor() {\n      this.port1 = new MockMessagePort();\n      this.port2 = new MockMessagePort();\n      this.port1._link(this.port2);\n      this.port2._link(this.port1);\n    }\n  }\n  // @ts-expect-error polyfill for jsdom\n  global.MessageChannel = MockMessageChannel;\n}\n"
  },
  {
    "path": "client/src/shared/components/CommentModal.tsx",
    "content": "import { Col, Form, Input, Modal, Row } from 'antd';\n\ntype Props = {\n  title: string;\n  visible: boolean;\n  onCancel: () => void;\n  onOk: (text: string) => void;\n  initialValue?: string;\n  availableEmptyComment?: boolean;\n};\n\nexport function CommentModal(props: Props) {\n  const [form] = Form.useForm();\n\n  const onOk = async () => {\n    try {\n      await form.validateFields();\n      const comment = form.getFieldValue('comment');\n      props.onOk(comment);\n    } catch {\n      return;\n    }\n  };\n\n  return (\n    <Modal title={props.title} open={props.visible} onOk={onOk} onCancel={props.onCancel}>\n      <Form form={form} layout=\"vertical\" initialValues={{ comment: props.initialValue ?? '' }}>\n        <Row gutter={24}>\n          <Col span={24}>\n            <Form.Item\n              name=\"comment\"\n              rules={[\n                { required: props.availableEmptyComment === true ? false : true, message: 'Please enter comment' },\n              ]}\n              label=\"Comment\"\n            >\n              <Input.TextArea style={{ height: 200 }} />\n            </Form.Item>\n          </Col>\n        </Row>\n      </Form>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/CopyToClipboardButton.tsx",
    "content": "import { Button, ButtonProps } from 'antd';\nimport { useCopyToClipboard } from 'react-use';\nimport { useMessage } from '@client/hooks';\nimport { CopyOutlined } from '@ant-design/icons';\n\ntype Props = {\n  value: string;\n  type?: ButtonProps['type'];\n};\n\nexport default function CopyToClipboardButton({ value, type = 'dashed' }: Props) {\n  const { message } = useMessage();\n  const [, copyToClipboard] = useCopyToClipboard();\n\n  const handleClick = async () => {\n    copyToClipboard(value);\n    await message.success(`Copied ${value} to clipboard`);\n  };\n\n  return <Button data-testid=\"copy-to-clipboard\" type={type} icon={<CopyOutlined />} onClick={handleClick} />;\n}\n"
  },
  {
    "path": "client/src/shared/components/FilteredTags.tsx",
    "content": "import { Button, Col, Row, Tag, theme } from 'antd';\nimport { FilterFilled } from '@ant-design/icons';\n\ntype FilteredTagsProps = {\n  tagFilters: string[];\n  onTagClose: (tag: string) => void;\n  onClearAllButtonClick: () => void;\n  filterName?: string;\n  tagNameMap?: Record<string, string>;\n};\n\nexport function FilteredTags({\n  tagFilters,\n  onTagClose,\n  onClearAllButtonClick,\n  filterName = '',\n  tagNameMap,\n}: FilteredTagsProps) {\n  const { token } = theme.useToken();\n  return (\n    <>\n      {tagFilters?.length > 0 ? (\n        <Row style={{ padding: 12, background: token.colorBgContainer, marginBottom: 4, borderRadius: 4 }}>\n          <Col flex=\"auto\">\n            <FilterFilled\n              style={{\n                color: token.colorTextLabel,\n                marginRight: 8,\n              }}\n            />\n            {tagFilters.map(tag => (\n              <Tag key={tag} closable onClose={() => onTagClose(tag)}>\n                {`${filterName}${tagNameMap?.[tag] || tag}`}\n              </Tag>\n            ))}\n          </Col>\n          <Col flex=\"none\">\n            <Button size=\"small\" onClick={onClearAllButtonClick}>\n              Clear all\n            </Button>\n          </Col>\n        </Row>\n      ) : null}\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/CommentInput.tsx",
    "content": "import { Input, Form } from 'antd';\n\nexport function CommentInput(props: { [key: string]: unknown; notRequired?: boolean }) {\n  const { notRequired, ...otherProps } = props;\n  return (\n    <Form.Item\n      {...otherProps}\n      name=\"comment\"\n      label=\"Comment\"\n      rules={notRequired ? [] : [{ required: true, message: 'Please leave a detailed comment', min: 30 }]}\n    >\n      <Input.TextArea rows={5} />\n    </Form.Item>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/CourseTaskSelect.tsx",
    "content": "import { Select, Form } from 'antd';\nimport { CourseTaskDto } from '@client/api';\nimport { DeadlineIcon } from '@client/shared/components/Icons/DeadlineIcon';\n\nexport enum Group {\n  Default = 'default',\n  Deadline = 'deadline',\n  CrossCheckDeadline = 'crossCheckDeadline',\n}\n\nexport type GroupType = `${Group}`;\n\nenum Section {\n  Unknown = 'Unknown',\n  AllTasks = 'All tasks',\n  Future = 'Future',\n  Active = 'Active',\n  Review = 'Review',\n  Expired = 'Expired',\n}\n\nenum SortingDirection {\n  Ascending = 'ascending',\n  Descending = 'descending',\n}\n\ntype Props = {\n  data: CourseTaskDto[];\n  groupBy?: GroupType;\n  onChange?: (id: number) => void;\n  defaultValue?: number | null;\n};\n\nexport function CourseTaskSelect(props: Props) {\n  const { data, groupBy = Group.Default, onChange, defaultValue, ...options } = props;\n  const selectProps = onChange ? { onChange } : {};\n  const selectingGroup = getSelectingGroup(data, groupBy);\n\n  return (\n    <Form.Item\n      {...options}\n      name=\"courseTaskId\"\n      label=\"Task\"\n      initialValue={defaultValue}\n      rules={[{ required: true, message: 'Please select a task' }]}\n    >\n      <Select placeholder=\"Select task\" {...selectProps} showSearch optionFilterProp=\"label\" listHeight={274}>\n        {selectingGroup.map(\n          (section, index) =>\n            section.tasks.length > 0 && (\n              <Select.OptGroup key={index} label={`${section.title} (${section.tasks.length})`}>\n                {section.tasks.map(task => (\n                  <Select.Option key={task.id} value={task.id} label={task.name}>\n                    <span>\n                      {section.title === Section.Active && groupBy === Group.Deadline && (\n                        <DeadlineIcon group={groupBy} endDate={task.studentEndDate} />\n                      )}\n                      {section.title === Section.Review && groupBy === Group.CrossCheckDeadline && (\n                        <DeadlineIcon group={groupBy} endDate={task.crossCheckEndDate} />\n                      )}{' '}\n                      {task.name}\n                    </span>\n                  </Select.Option>\n                ))}\n              </Select.OptGroup>\n            ),\n        )}\n      </Select>\n    </Form.Item>\n  );\n}\n\nfunction getSelectingGroup(data: CourseTaskDto[], groupBy: GroupType) {\n  switch (groupBy) {\n    case Group.Deadline:\n      return [\n        {\n          title: Section.Active,\n          tasks: sortTasks({\n            tasks: getTasksBetweenStudentStartDateStudentEndDate(data),\n            sortBy: 'studentEndDate',\n            sortingDirection: SortingDirection.Ascending,\n          }),\n        },\n        {\n          title: Section.Future,\n          tasks: sortTasks({\n            tasks: getTasksBeforeStudentStartDate(data),\n            sortBy: 'studentStartDate',\n            sortingDirection: SortingDirection.Ascending,\n          }),\n        },\n        {\n          title: Section.Expired,\n          tasks: sortTasks({\n            tasks: getTasksAfterStudentEndDate(data),\n            sortBy: 'studentEndDate',\n            sortingDirection: SortingDirection.Descending,\n          }),\n        },\n      ];\n\n    case Group.CrossCheckDeadline: {\n      const unknownTasks = data.filter(({ crossCheckEndDate }) => !crossCheckEndDate);\n      const tasksWithoutUnknown = data.filter(({ crossCheckEndDate }) => crossCheckEndDate);\n\n      return [\n        {\n          title: Section.Review,\n          tasks: sortTasks({\n            tasks: getTasksBetweenStudentEndDateCrossCheckEndDate(tasksWithoutUnknown),\n            sortBy: 'crossCheckEndDate',\n            sortingDirection: SortingDirection.Ascending,\n          }),\n        },\n        {\n          title: Section.Unknown,\n          tasks: sortTasks({\n            tasks: unknownTasks,\n            sortBy: 'studentEndDate',\n            sortingDirection: SortingDirection.Ascending,\n          }),\n        },\n        {\n          title: Section.Future,\n          tasks: sortTasks({\n            tasks: getTasksBeforeStudentEndDate(tasksWithoutUnknown),\n            sortBy: 'studentEndDate',\n            sortingDirection: SortingDirection.Ascending,\n          }),\n        },\n        {\n          title: Section.Expired,\n          tasks: sortTasks({\n            tasks: getTasksAfterCrossCheckEndDate(tasksWithoutUnknown),\n            sortBy: 'crossCheckEndDate',\n            sortingDirection: SortingDirection.Descending,\n          }),\n        },\n      ];\n    }\n\n    case Group.Default:\n    default:\n      return [\n        {\n          title: Section.AllTasks,\n          tasks: sortTasks({\n            tasks: data,\n            sortBy: 'studentEndDate',\n            sortingDirection: SortingDirection.Descending,\n          }),\n        },\n      ];\n  }\n}\n\ntype SortTasksProps = {\n  tasks: CourseTaskDto[];\n  sortBy: 'studentStartDate' | 'studentEndDate' | 'crossCheckEndDate';\n  sortingDirection: SortingDirection;\n};\n\nfunction sortTasks({ tasks, sortBy, sortingDirection }: SortTasksProps) {\n  return [...tasks].sort((firstTask, secondTask) => {\n    const firstDate = firstTask[sortBy];\n    const secondDate = secondTask[sortBy];\n\n    if (firstDate && secondDate) {\n      return sortingDirection === SortingDirection.Ascending\n        ? firstDate.localeCompare(secondDate)\n        : secondDate.localeCompare(firstDate);\n    }\n\n    return 1;\n  });\n}\n\nfunction getTasksBeforeStudentStartDate(tasks: CourseTaskDto[]) {\n  return tasks.filter(task => Date.now() < Date.parse(task.studentStartDate));\n}\n\nfunction getTasksBeforeStudentEndDate(tasks: CourseTaskDto[]) {\n  return tasks.filter(task => Date.now() < Date.parse(task.studentEndDate));\n}\n\nfunction getTasksAfterStudentEndDate(tasks: CourseTaskDto[]) {\n  return tasks.filter(task => Date.now() >= Date.parse(task.studentEndDate));\n}\n\nfunction getTasksAfterCrossCheckEndDate(tasks: CourseTaskDto[]) {\n  return tasks.filter(task => {\n    if (task.crossCheckEndDate) {\n      return Date.now() >= Date.parse(task.crossCheckEndDate);\n    }\n  });\n}\n\nfunction getTasksBetweenStudentStartDateStudentEndDate(tasks: CourseTaskDto[]) {\n  return tasks.filter(\n    task => Date.now() >= Date.parse(task.studentStartDate) && Date.now() < Date.parse(task.studentEndDate),\n  );\n}\n\nfunction getTasksBetweenStudentEndDateCrossCheckEndDate(tasks: CourseTaskDto[]) {\n  return tasks.filter(task => {\n    if (task.crossCheckEndDate) {\n      return Date.now() >= Date.parse(task.studentEndDate) && Date.now() < Date.parse(task.crossCheckEndDate);\n    }\n  });\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/GdprCheckbox.tsx",
    "content": "import { Checkbox, Row, Typography, Form } from 'antd';\n\nexport function GdprCheckbox() {\n  return (\n    <>\n      <Row>\n        <Typography.Paragraph>\n          I hereby agree to the processing of my personal data contained in the application and sharing it with\n          companies only for students employment purposes.\n        </Typography.Paragraph>\n        <Typography.Paragraph>\n          Я согласен на обработку моих персональных данных, содержащихся в приложении, и передачу их компаниям только в\n          целях трудоустройства студентов.\n        </Typography.Paragraph>\n      </Row>\n      <Form.Item name=\"gdpr\" valuePropName=\"checked\">\n        <Checkbox>I agree / Я согласен</Checkbox>\n      </Form.Item>\n    </>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/Heroes/index.module.css",
    "content": ".masonry {\n  display: flex;\n  margin-left: -16px;\n  width: auto;\n}\n\n.masonryColumn {\n  padding-left: 16px;\n  background-clip: padding-box;\n}\n\n.flexCenter {\n  display: flex;\n  justify-content: center;\n}\n\n.badgeBg {\n  position: absolute;\n  background-position: center;\n  top: 0;\n  left: 0;\n  height: 100%;\n  width: 100%;\n  z-index: -1;\n  opacity: 0.1;\n  transform: scale(1.2);\n}\n\n.badgeNote {\n  padding: 16px;\n  border: 2px rgba(24, 144, 255, 0.5) dashed;\n}\n\n.card .badge {\n  transform: scale(1);\n  transition: transform 1s ease;\n}\n\n.card:hover .badge {\n  transform: scale(1.5);\n  transition: transform 1s ease;\n}\n\n.card:hover .badgeBg {\n  transition: all 2s ease;\n  opacity: 0.8;\n}\n\n.card:hover .badgeNote {\n  transition: all 1s ease;\n  border: 2px rgba(24, 144, 255, 1) dashed;\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/Heroes/index.tsx",
    "content": "import { Avatar, Button, Card, Form, Grid, Input, Pagination, Row, Select, Space, Typography } from 'antd';\nimport { FormLayout } from 'antd/es/form/Form';\nimport { useCallback, useEffect, useState } from 'react';\nimport Masonry from 'react-masonry-css';\nimport heroesBadges from '@client/configs/heroes-badges';\nimport styles from './index.module.css';\nimport {\n  GratitudeService,\n  HeroesFormData,\n  IGratitudeGetRequest,\n  IGratitudeGetResponse,\n} from '@client/services/gratitude';\nimport { onlyDefined } from '@client/shared/utils/onlyDefined';\nimport { getFullName } from '@client/domain/user';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts';\n\nconst { Text, Link, Paragraph } = Typography;\nconst { useBreakpoint } = Grid;\n\nconst initialPage = 1;\nconst initialPageSize = 20;\n\nexport const fields = {\n  name: 'name',\n  githubId: 'githubId',\n  courseId: 'courseId',\n} as const;\n\nexport const HeroesForm = ({ setLoading }: { setLoading: (arg: boolean) => void }) => {\n  const { courses } = useActiveCourseContext();\n\n  const [heroesData, setHeroesData] = useState<IGratitudeGetResponse[]>([]);\n  const [heroesCount, setHeroesCount] = useState(initialPage);\n  const [currentPage, setCurrentPage] = useState(initialPage);\n  const gratitudeService = new GratitudeService();\n  const [form] = Form.useForm();\n  const { xs } = useBreakpoint();\n  const formLayout: FormLayout = xs ? 'vertical' : 'inline';\n  const minWidth = xs ? undefined : 300;\n\n  useEffect(() => {\n    const getHeroes = async () => {\n      setLoading(true);\n      const heroes = await gratitudeService.getGratitude({ current: initialPage, pageSize: initialPageSize });\n      setHeroesData(heroes.content);\n      setHeroesCount(heroes.count);\n      setLoading(false);\n    };\n    getHeroes();\n  }, []);\n\n  const makeRequest = useCallback(\n    async (data: IGratitudeGetRequest) => {\n      setLoading(true);\n      const heroes = await gratitudeService.getGratitude(data);\n      setHeroesData(heroes.content);\n      setHeroesCount(heroes.count);\n      setCurrentPage(initialPage);\n      setLoading(false);\n    },\n    [heroesData],\n  );\n\n  const handleSubmit = useCallback(\n    async (formData: HeroesFormData) => {\n      const data = onlyDefined(formData) as Partial<HeroesFormData>;\n      setCurrentPage(initialPage);\n      await makeRequest(data);\n    },\n    [heroesData],\n  );\n\n  const onClear = useCallback(async () => {\n    setLoading(true);\n    const heroes = await gratitudeService.getGratitude({ current: initialPage, pageSize: initialPageSize });\n    setHeroesData(heroes.content);\n    setHeroesCount(heroes.count);\n    setCurrentPage(initialPage);\n    setLoading(false);\n    form.resetFields();\n  }, [heroesData]);\n\n  const onClickPagination = useCallback(\n    async (current: number, pageSize?: number) => {\n      const formData = form.getFieldsValue() as HeroesFormData;\n      await makeRequest({ current, pageSize: pageSize!, ...formData });\n      setCurrentPage(current);\n    },\n    [currentPage],\n  );\n\n  return (\n    <>\n      <Form layout={formLayout} form={form} onFinish={handleSubmit} style={{ marginBottom: 24 }}>\n        <Form.Item name={fields.name} label=\"Name\" style={{ marginBottom: 16 }}>\n          <Input />\n        </Form.Item>\n        <Form.Item name={fields.githubId} label=\"GitHub Username\" style={{ marginBottom: 16 }}>\n          <Input />\n        </Form.Item>\n        <Form.Item name={fields.courseId} label=\"Courses\" style={{ minWidth, marginBottom: 16 }}>\n          <Select options={courses.map(({ id, name }) => ({ value: id, label: name }))} />\n        </Form.Item>\n        <Space align=\"start\" size={20}>\n          <Button type=\"primary\" htmlType=\"submit\">\n            Submit\n          </Button>\n          <Button type=\"primary\" onClick={onClear}>\n            Clear\n          </Button>\n        </Space>\n      </Form>\n      <Masonry\n        breakpointCols={{\n          default: 4,\n          1100: 3,\n          700: 2,\n          500: 1,\n        }}\n        className={styles.masonry as string}\n        columnClassName={styles.masonryColumn as string}\n      >\n        {heroesData.map(feedback => (\n          <div\n            style={{ marginBottom: gapSize, overflow: 'hidden' }}\n            key={`card-${feedback.id}`}\n            className={styles.card}\n          >\n            <Card style={{ position: 'relative', background: 'none' }}>\n              <div\n                className={styles.badgeBg}\n                style={{ backgroundImage: `url(/static/svg/badges/${heroesBadges[feedback.badgeId]?.url})` }}\n              />\n              <div className={styles.badgeNote} style={{ marginBottom: 48 }}>\n                <Paragraph style={{ margin: 0 }}>\n                  <Text strong>From:</Text> {getFullName(feedback.from)} (\n                  <Link href={`/profile?githubId=${feedback.from.githubId}`}>@{feedback.from.githubId}</Link>)\n                </Paragraph>\n                <Paragraph style={{ margin: 0 }}>\n                  <Text strong>To:</Text> {getFullName(feedback)} (\n                  <Link href={`/profile?githubId=${feedback.githubId}`}>@{feedback.githubId}</Link>)\n                </Paragraph>\n              </div>\n              <div className={styles.flexCenter} style={{ marginBottom: 48 }}>\n                <div className={styles.badge}>\n                  <Avatar\n                    src={`/static/svg/badges/${heroesBadges[feedback.badgeId]?.url}`}\n                    alt={`${feedback.badgeId} badge`}\n                    size={128}\n                  />\n                </div>\n              </div>\n              <div className={styles.badgeNote}>\n                <Paragraph style={{ margin: 0 }}>{feedback.comment}</Paragraph>\n              </div>\n            </Card>\n          </div>\n        ))}\n      </Masonry>\n      <Row style={{ marginTop: 16, marginBottom: 16, justifyContent: 'flex-end' }}>\n        <Pagination\n          current={currentPage}\n          total={heroesCount}\n          onChange={onClickPagination}\n          defaultPageSize={initialPageSize}\n        />\n      </Row>\n    </>\n  );\n};\n\nconst gapSize = 16;\n"
  },
  {
    "path": "client/src/shared/components/Forms/LocationSelect.tsx",
    "content": "import React from 'react';\nimport { Alert, Select, Spin } from 'antd';\nimport { Location } from '@common/models/profile';\nimport { useGoogleMapsPlaces } from './useGoogleMapsPlaces';\n\ntype Props = {\n  onChange: (arg: Location | null) => void;\n  location: Location | null;\n  style?: React.CSSProperties;\n};\n\nexport function LocationSelect(props: Props) {\n  const { value, data, loading, setValue, initialized, error } = useGoogleMapsPlaces(props.location);\n\n  const handleInput = (value: string) => setValue(value);\n\n  const handleSelect = (value: string) => {\n    setValue(value, false);\n    props.onChange(toLocation(value));\n  };\n\n  const handleBlur = () => {\n    if (!value) {\n      setValue(fromLocation(props.location), false);\n    }\n  };\n\n  if (error) {\n    return <Alert message={error.message} type=\"error\" showIcon />;\n  }\n\n  if (!initialized) {\n    return <Spin size=\"small\" />;\n  }\n\n  return (\n    <Select\n      filterOption={false}\n      onSearch={handleInput}\n      onSelect={handleSelect}\n      showSearch\n      onBlur={handleBlur}\n      notFoundContent={loading ? <Spin size=\"small\" /> : null}\n      value={value}\n      placeholder=\"Select city\"\n      options={data.map(({ description }) => ({ value: description }))}\n      style={props.style}\n    />\n  );\n}\n\nconst toLocation = (value: string): Location => {\n  const parts = value.split(', ');\n  return {\n    cityName: parts[0] ?? '',\n    countryName: parts[parts.length - 1] ?? '',\n  };\n};\n\nconst fromLocation = (value: Location | null): string => {\n  if (value) {\n    return `${value.cityName}, ${value.countryName}`;\n  }\n  return '';\n};\n"
  },
  {
    "path": "client/src/shared/components/Forms/MarkdownInput.tsx",
    "content": "import { useEffect } from 'react';\nimport { Input, Form, Button, Typography } from 'antd';\nimport { useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\n\ntype Props = {\n  historicalCommentSelected: string;\n};\n\nexport default function MarkdownInput({ historicalCommentSelected }: Props) {\n  const [previewVisible, setPreviewVisible] = useState(false);\n  const [text, setText] = useState('');\n\n  useEffect(() => {\n    if (historicalCommentSelected !== '') {\n      setText(historicalCommentSelected);\n    }\n  }, [historicalCommentSelected]);\n\n  const toggleView = () => {\n    setPreviewVisible(!previewVisible);\n  };\n\n  const resetText = () => {\n    setPreviewVisible(false);\n    setText('');\n  };\n\n  const renderComment = () => {\n    return (\n      <Typography.Paragraph type=\"danger\">\n        {!text ? 'Please leave a comment' : 'Please leave a detailed comment'}\n      </Typography.Paragraph>\n    );\n  };\n\n  const link = (\n    <span style={{ marginLeft: '20px' }}>\n      <a\n        target=\"_blank\"\n        href=\"https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax\"\n      >\n        About markdown\n      </a>\n    </span>\n  );\n\n  return (\n    <div style={{ marginBottom: '20px' }}>\n      <div style={{ display: previewVisible ? 'none' : 'block' }}>\n        <Form.Item\n          name=\"comment\"\n          label=\"Comment (markdown syntax is supported)\"\n          rules={[{ required: true, message: 'Please leave a detailed comment', min: 30 }]}\n          onReset={resetText}\n        >\n          <Input.TextArea onChange={({ currentTarget: { value } }) => setText(value)} rows={5} />\n        </Form.Item>\n        <Button onClick={toggleView}>Preview</Button> {link}\n      </div>\n      {previewVisible ? (\n        <div>\n          <div>\n            <Typography.Text>\n              <ReactMarkdown rehypePlugins={[remarkGfm]}>{text}</ReactMarkdown>\n            </Typography.Text>\n          </div>\n          {(!text || text?.length < 30) && renderComment()}\n          <Button onClick={toggleView}>Write</Button> {link}\n        </div>\n      ) : (\n        ''\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/ModalForm.tsx",
    "content": "import { ButtonProps, Form, Modal, Spin } from 'antd';\nimport { FormInstance } from 'antd/es/form/Form';\nimport React from 'react';\n\ntype Props<T> = React.PropsWithChildren<{\n  data: T;\n  title?: string;\n  submit: (arg: any) => void;\n  cancel: (arg: any) => void;\n  onChange?: (values: any) => void;\n  getInitialValues?: (arg: any) => any;\n  loading?: boolean;\n  okText?: string;\n  form?: FormInstance;\n  okButtonProps?: ButtonProps;\n}>;\n\nexport function ModalForm<T extends object>(props: Props<T>) {\n  const antForm = Form.useForm()[0];\n  const form = props.form || antForm;\n\n  if (props.data == null) {\n    return null;\n  }\n  const initialValues = props.getInitialValues ? props.getInitialValues?.(props.data) : props.data;\n  return (\n    <Modal\n      style={{ top: 20 }}\n      width={700}\n      open={true}\n      title={props.title}\n      okText={props.okText ?? 'Save'}\n      maskClosable={false}\n      onOk={async e => {\n        e.preventDefault();\n        const values = await form.validateFields().catch(() => null);\n        if (values == null) {\n          return;\n        }\n        props.submit(values);\n      }}\n      okButtonProps={{ disabled: props.loading, ...props.okButtonProps }}\n      onCancel={e => {\n        if (form.isFieldsTouched()) {\n          Modal.confirm({\n            title: 'Are you sure you want to discard changes?',\n            content: 'You will lose all unsaved changes.',\n            okText: 'Yes, discard',\n            cancelText: 'Keep editing',\n            okButtonProps: { danger: true },\n            onOk: () => {\n              props.cancel(e);\n              form.resetFields();\n            },\n          });\n        } else {\n          props.cancel(e);\n          form.resetFields();\n        }\n      }}\n    >\n      <Spin spinning={props.loading ?? false}>\n        <Form\n          layout=\"vertical\"\n          onValuesChange={() => props.onChange?.(form.getFieldsValue())}\n          form={form}\n          initialValues={initialValues}\n        >\n          {props.children}\n        </Form>\n      </Spin>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/ModalSubmitForm.test.tsx",
    "content": "import { fireEvent, render, screen } from '@testing-library/react';\nimport { Form } from 'antd';\nimport { ModalSubmitForm } from './ModalSubmitForm';\n\nconst PROPS_MOCK = {\n  submit: vi.fn(),\n  close: vi.fn(),\n  children: [],\n  title: 'Form',\n  data: { field: 'value' },\n};\n\ndescribe('ModalSubmitForm', () => {\n  it('should not render when data was not provided', () => {\n    render(<ModalSubmitForm {...PROPS_MOCK} data={null} />);\n\n    const form = screen.queryByText('Form');\n\n    expect(form).not.toBeInTheDocument();\n  });\n\n  describe('when form was submitted', () => {\n    it('should not render footer', () => {\n      render(<ModalSubmitForm {...PROPS_MOCK} submitted={true} />);\n\n      const footerBtn = screen.queryByText('Submit');\n\n      expect(footerBtn).not.toBeInTheDocument();\n    });\n\n    it('should render success message', () => {\n      render(<ModalSubmitForm {...PROPS_MOCK} submitted={true} />);\n\n      const success = screen.getByText('Successfully submitted');\n\n      expect(success).toBeInTheDocument();\n    });\n\n    it('should close on OK button click', () => {\n      render(<ModalSubmitForm {...PROPS_MOCK} submitted={true} />);\n      const okButton = screen.getByText('Ok');\n\n      fireEvent.click(okButton);\n\n      expect(PROPS_MOCK.close).toHaveBeenCalled();\n    });\n  });\n\n  describe('when form was not submitted', () => {\n    it('should render footer', () => {\n      render(<ModalSubmitForm {...PROPS_MOCK} submitted={false} />);\n\n      const footerBtn = screen.getByText('Submit');\n\n      expect(footerBtn).toBeInTheDocument();\n    });\n\n    it('should render form fields', () => {\n      render(\n        <ModalSubmitForm {...PROPS_MOCK} submitted={false}>\n          <Form.Item label=\"Input\"></Form.Item>\n        </ModalSubmitForm>,\n      );\n\n      const input = screen.getByText(/input/i);\n\n      expect(input).toBeInTheDocument();\n    });\n  });\n\n  describe('when error text was provided', () => {\n    it('should render error', () => {\n      const ERROR_MESSAGE = 'Error!';\n      render(<ModalSubmitForm {...PROPS_MOCK} errorText={ERROR_MESSAGE} />);\n\n      const error = screen.getByText(ERROR_MESSAGE);\n\n      expect(error).toBeInTheDocument();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/shared/components/Forms/ModalSubmitForm.tsx",
    "content": "import { Alert, Button, Form, Modal, Result, Spin } from 'antd';\nimport { useEffect } from 'react';\ntype Props = {\n  data: any;\n  title?: string;\n  submit: (arg: any) => void;\n  close: (arg: any) => void;\n  onChange?: (values: any) => void;\n  getInitialValues?: (arg: any) => any;\n  children: React.ReactNode;\n  loading?: boolean;\n  submitted?: boolean;\n  successText?: string;\n  errorText?: string;\n  open?: boolean;\n};\n\nexport function ModalSubmitForm({\n  data,\n  title,\n  submit,\n  close,\n  onChange,\n  getInitialValues,\n  children,\n  loading,\n  submitted,\n  successText,\n  errorText,\n  open,\n}: Props) {\n  const [form] = Form.useForm();\n\n  useEffect(() => {\n    if (data?.selectedSolutionUrl !== undefined) {\n      form.setFieldsValue({ url: data.selectedSolutionUrl });\n    }\n  }, [data?.selectedSolutionUrl]);\n\n  if (data == null) {\n    return null;\n  }\n\n  const initialValues = getInitialValues ? getInitialValues?.(data) : data;\n\n  function onSubmit(): ((e: React.MouseEvent<HTMLElement, MouseEvent>) => void) | undefined {\n    return async e => {\n      e.preventDefault();\n      const values = await form.validateFields().catch(() => null);\n      if (values == null) {\n        return;\n      }\n      submit(values);\n    };\n  }\n\n  return (\n    <Modal\n      open={open ?? true}\n      footer={submitted ? null : undefined}\n      title={title}\n      okText=\"Submit\"\n      onOk={onSubmit()}\n      onCancel={e => {\n        close(e);\n        form.resetFields();\n      }}\n    >\n      <Spin spinning={loading ?? false}>\n        {errorText ? <Alert style={{ marginBottom: 16 }} message={errorText} type=\"error\" showIcon /> : null}\n        {submitted ? (\n          <Result\n            status=\"success\"\n            title=\"Success\"\n            subTitle={successText ?? 'Successfully submitted'}\n            extra={[\n              <Button style={{ minWidth: 80 }} onClick={close} type=\"primary\" key=\"ok\">\n                Ok\n              </Button>,\n            ]}\n          />\n        ) : (\n          <Form\n            onValuesChange={() => onChange?.(form.getFieldsValue())}\n            form={form}\n            initialValues={initialValues}\n            layout=\"vertical\"\n          >\n            {children}\n          </Form>\n        )}\n      </Spin>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/PreparedComment.tsx",
    "content": "import { Typography } from 'antd';\nimport { FC, useEffect, useState } from 'react';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\n\nexport const markdownLabel = '{markdown}\\n';\n\ntype Props = {\n  text: string;\n};\n\nconst PreparedComment: FC<Props> = ({ text }) => {\n  const [state, setState] = useState(text ?? '');\n  const [isMD, setIsMD] = useState(false);\n\n  useEffect(() => {\n    if (text && text.indexOf(markdownLabel) === 0) {\n      setIsMD(true);\n      setState(text.slice(markdownLabel.length));\n    }\n  }, []);\n\n  /**\n   * In order to not affect the comments created in previosly versions of RS App, this component use a special mark for the markdown syntax.\n   */\n  return (\n    <>\n      {isMD ? (\n        <Typography.Text>\n          <ReactMarkdown rehypePlugins={[remarkGfm]}>{state}</ReactMarkdown>\n        </Typography.Text>\n      ) : (\n        <Typography.Text>\n          {state.split('\\n').map((item, i) => (\n            <div key={i}>{item}</div>\n          ))}\n        </Typography.Text>\n      )}\n    </>\n  );\n};\n\nexport default PreparedComment;\n"
  },
  {
    "path": "client/src/shared/components/Forms/ScoreInput.tsx",
    "content": "import * as React from 'react';\nimport { InputNumber, Form } from 'antd';\nimport { CourseTaskDto } from '@client/api';\n\ntype Props = {\n  maxScore?: number;\n  courseTask?: Pick<CourseTaskDto, 'id' | 'maxScore'>;\n  style?: React.CSSProperties;\n};\n\nexport function ScoreInput({ maxScore, courseTask, style }: Props) {\n  const maxScoreValue = maxScore || (courseTask ? courseTask.maxScore || 100 : undefined);\n  const maxScoreLabel = maxScoreValue ? ` (Max ${maxScoreValue} points)` : '';\n  return (\n    <Form.Item name=\"score\" label={`Score${maxScoreLabel}`} rules={[{ required: true, message: 'Please enter score' }]}>\n      <InputNumber style={style} step={1} min={0} max={maxScoreValue} decimalSeparator={','} />\n    </Form.Item>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Forms/__tests__/CourseTaskSelect.test.tsx",
    "content": "/* eslint-disable testing-library/no-container */\n/* eslint-disable testing-library/no-node-access */\nimport { Form } from 'antd';\nimport { fireEvent, render } from '@testing-library/react';\n\nimport { CourseTaskSelect } from '..';\nimport { CourseTaskDto } from '@client/api';\n\nconst tomorrowDate = new Date();\nconst dayAfterTomorrowDate = new Date();\nconst afterDayAfterTomorrowDate = new Date();\n\ntomorrowDate.setDate(tomorrowDate.getDate() + 1);\ndayAfterTomorrowDate.setDate(dayAfterTomorrowDate.getDate() + 2);\nafterDayAfterTomorrowDate.setDate(afterDayAfterTomorrowDate.getDate() + 3);\n\nconst ActiveCodewarsData: CourseTaskDto[] = [\n  {\n    id: 451,\n    taskId: 704,\n    type: 'codewars',\n    name: 'Codewars Algorithms-2',\n    studentStartDate: '2022-09-12T23:59:00.000Z',\n    studentEndDate: tomorrowDate.toISOString(),\n    maxScore: 100,\n    scoreWeight: 1,\n    descriptionUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars/algorithms-2.md',\n    checker: 'auto-test',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: null,\n    pairsCount: null,\n    submitText: null,\n    taskOwner: null,\n    validations: null,\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n  {\n    id: 764,\n    taskId: 592,\n    type: 'codewars',\n    name: 'Codewars #0',\n    studentStartDate: '2022-09-14T23:59:00.000Z',\n    studentEndDate: tomorrowDate.toISOString(),\n    maxScore: 15,\n    scoreWeight: 1,\n    descriptionUrl: 'https://rolling-scopes-school.github.io/stage0/#/stage0/tasks/codewars',\n    checker: 'auto-test',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: null,\n    pairsCount: null,\n    submitText: null,\n    taskOwner: null,\n    validations: null,\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n];\n\nconst ActiveTestData: CourseTaskDto[] = [\n  {\n    id: 440,\n    taskId: 720,\n    type: 'selfeducation',\n    name: 'React. Testing',\n    studentStartDate: '2022-09-12T23:59:00.000Z',\n    studentEndDate: tomorrowDate.toISOString(),\n    maxScore: 100,\n    scoreWeight: 0.1,\n    descriptionUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-testing.md',\n    checker: 'auto-test',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: null,\n    pairsCount: null,\n    submitText: null,\n    taskOwner: null,\n    validations: null,\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n  {\n    id: 442,\n    taskId: 727,\n    type: 'selfeducation',\n    name: 'Test Algorithms & Data structures',\n    studentStartDate: '2022-09-15T23:59:00.000Z',\n    studentEndDate: tomorrowDate.toISOString(),\n    maxScore: 100,\n    scoreWeight: 1,\n    descriptionUrl: 'https://www.youtube.com/playlist?list=PLP-a1IHLCS7PqDf08LFIYCiTYY1CtoAkt',\n    checker: 'auto-test',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: null,\n    pairsCount: null,\n    submitText: null,\n    taskOwner: null,\n    validations: null,\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n];\n\nconst UnknownTaskData: CourseTaskDto[] = [\n  {\n    id: 846,\n    taskId: 625,\n    type: 'jstask',\n    name: 'Virtual-piano',\n    studentStartDate: '2021-03-16T04:32:00.000Z',\n    studentEndDate: '2021-03-23T01:59:00.000Z',\n    maxScore: 50,\n    scoreWeight: 1,\n    descriptionUrl: 'https://rolling-scopes-school.github.io/stage0/#/stage1/tasks/js-projects/virtual-piano',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: null,\n    pairsCount: 4,\n    submitText: null,\n    taskOwner: null,\n    validations: null,\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n  {\n    id: 853,\n    taskId: 630,\n    type: 'htmltask',\n    name: 'Clean-code-s1e1',\n    studentStartDate: '2021-03-23T01:59:00.000Z',\n    studentEndDate: '2021-04-06T23:59:00.000Z',\n    maxScore: 45,\n    scoreWeight: 1,\n    descriptionUrl: 'https://rolling-scopes-school.github.io/stage0/#/stage1/tasks/clean-code/clean-code-s1e1',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: null,\n    pairsCount: 4,\n    submitText: null,\n    taskOwner: null,\n    validations: null,\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n];\n\nconst FutureTaskData: CourseTaskDto[] = [\n  {\n    id: 438,\n    taskId: 576,\n    type: 'jstask',\n    name: 'Shelter Cross-check',\n    studentStartDate: tomorrowDate.toISOString(),\n    studentEndDate: dayAfterTomorrowDate.toISOString(),\n    maxScore: 100,\n    scoreWeight: 1,\n    descriptionUrl: 'https://github.com/rolling-scopes-school/tasks/tree/master/tasks/markups/level-2/shelter',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: afterDayAfterTomorrowDate.toISOString(),\n    pairsCount: null,\n    submitText: null,\n    taskOwner: null,\n    validations: {},\n  },\n  {\n    id: 441,\n    taskId: 493,\n    type: 'jstask',\n    name: 'Virtual Keyboard Cross-Check',\n    studentStartDate: tomorrowDate.toISOString(),\n    studentEndDate: dayAfterTomorrowDate.toISOString(),\n    maxScore: 100,\n    scoreWeight: 1,\n    descriptionUrl: 'https://rolling-scopes-school.github.io/checklist/',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: afterDayAfterTomorrowDate.toISOString(),\n    pairsCount: 4,\n    submitText: null,\n    taskOwner: null,\n    validations: {},\n  },\n] as unknown as CourseTaskDto[];\n\nconst ReviewTaskData: CourseTaskDto[] = [\n  {\n    id: 434,\n    taskId: 680,\n    type: 'jstask',\n    name: 'Async Race',\n    studentStartDate: '2022-09-05T23:59:00.000Z',\n    studentEndDate: '2022-09-12T23:59:00.000Z',\n    maxScore: 100,\n    scoreWeight: 1,\n    descriptionUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/tasks/async-race.md',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: tomorrowDate.toISOString(),\n    pairsCount: 2,\n    submitText: null,\n    taskOwner: null,\n    validations: {\n      githubIdInUrl: false,\n      githubPrInUrl: false,\n    },\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n  {\n    id: 450,\n    taskId: 452,\n    type: 'htmltask',\n    name: 'Fancy-weather Cross-Check',\n    studentStartDate: '2022-09-14T16:48:00.000Z',\n    studentEndDate: '2022-09-16T16:48:00.000Z',\n    maxScore: 100,\n    scoreWeight: 1,\n    descriptionUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/tasks/fancy-weather.md',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: dayAfterTomorrowDate.toISOString(),\n    pairsCount: 2,\n    submitText: null,\n    taskOwner: null,\n    validations: {\n      githubIdInUrl: false,\n      githubPrInUrl: false,\n    },\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n];\n\nconst expiredTaskData: CourseTaskDto[] = [\n  {\n    id: 821,\n    taskId: 593,\n    type: 'htmltask',\n    name: 'CV. Cross-Check',\n    studentStartDate: '2021-02-28T23:59:00.000Z',\n    studentEndDate: '2021-03-08T23:59:00.000Z',\n    maxScore: 100,\n    scoreWeight: 0.2,\n    descriptionUrl: 'https://github.com/rolling-scopes-school/tasks/blob/master/tasks/cv/html-css.md',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: '2021-03-11T23:59:00.000Z',\n    pairsCount: 4,\n    submitText: null,\n    taskOwner: null,\n    validations: {\n      githubIdInUrl: false,\n      githubPrInUrl: false,\n    },\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n  {\n    id: 841,\n    taskId: 594,\n    type: 'htmltask',\n    name: 'Wildlife',\n    studentStartDate: '2021-02-28T23:59:00.000Z',\n    studentEndDate: '2021-03-15T23:59:00.000Z',\n    maxScore: 50,\n    scoreWeight: 0.5,\n    descriptionUrl: 'https://rolling-scopes-school.github.io/stage0/#/stage0/tasks/wildlife',\n    checker: 'crossCheck',\n    crossCheckStatus: 'initial',\n    crossCheckEndDate: '2021-05-06T23:59:00.000Z',\n    pairsCount: 4,\n    submitText: null,\n    taskOwner: null,\n    validations: {\n      githubIdInUrl: false,\n      githubPrInUrl: false,\n    },\n    studentRegistrationStartDate: '2022-09-12T23:59:00.000Z',\n    taskSolutions: [],\n  },\n];\n\ndescribe('CourseTaskSelect', () => {\n  describe('Should render correctly', () => {\n    it('if groupBy is default', () => {\n      const { container, baseElement } = render(\n        <Form>\n          <CourseTaskSelect\n            data={[...UnknownTaskData, ...FutureTaskData, ...ReviewTaskData, ...expiredTaskData]}\n            groupBy=\"default\"\n          ></CourseTaskSelect>\n        </Form>,\n      );\n\n      const select = container.querySelector('.ant-select-selector');\n\n      if (select) {\n        fireEvent.mouseDown(select);\n      }\n\n      expect(baseElement).toMatchSnapshot();\n    });\n\n    it('if groupBy is deadline', () => {\n      const { container, baseElement } = render(\n        <Form>\n          <CourseTaskSelect data={[...ActiveCodewarsData, ...ActiveTestData]} groupBy=\"deadline\"></CourseTaskSelect>,\n        </Form>,\n      );\n\n      const select = container.querySelector('.ant-select-selector');\n\n      if (select) {\n        fireEvent.mouseDown(select);\n      }\n\n      expect(baseElement).toMatchSnapshot();\n    });\n\n    it('if groupBy is cross-check deadline', () => {\n      const { container, baseElement } = render(\n        <Form>\n          <CourseTaskSelect\n            data={[...UnknownTaskData, ...FutureTaskData, ...ReviewTaskData, ...expiredTaskData]}\n            groupBy=\"crossCheckDeadline\"\n          ></CourseTaskSelect>\n        </Form>,\n      );\n\n      const select = container.querySelector('.ant-select-selector');\n\n      if (select) {\n        fireEvent.mouseDown(select);\n      }\n\n      expect(baseElement).toMatchSnapshot();\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/shared/components/Forms/__tests__/__snapshots__/CourseTaskSelect.test.tsx.snap",
    "content": "// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html\n\nexports[`CourseTaskSelect > Should render correctly > if groupBy is cross-check deadline 1`] = `\n<body>\n  <div>\n    <form\n      class=\"ant-form ant-form-horizontal css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14\"\n    >\n      <div\n        class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n      >\n        <div\n          class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-col ant-form-item-label css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <label\n              class=\"ant-form-item-required\"\n              for=\"courseTaskId\"\n              title=\"Task\"\n            >\n              Task\n            </label>\n          </div>\n          <div\n            class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <div\n              class=\"ant-form-item-control-input\"\n            >\n              <div\n                class=\"ant-form-item-control-input-content\"\n              >\n                <div\n                  class=\"ant-select ant-select-outlined ant-select-in-form-item css-var-root ant-select-css-var css-dev-only-do-not-override-1enej14 ant-select-single ant-select-show-arrow ant-select-show-search\"\n                >\n                  <div\n                    class=\"ant-select-content\"\n                  >\n                    <div\n                      class=\"ant-select-placeholder\"\n                      style=\"visibility: visible;\"\n                    >\n                      Select task\n                    </div>\n                    <input\n                      aria-autocomplete=\"list\"\n                      aria-expanded=\"false\"\n                      aria-haspopup=\"listbox\"\n                      aria-required=\"true\"\n                      autocomplete=\"off\"\n                      class=\"ant-select-input\"\n                      id=\"courseTaskId\"\n                      role=\"combobox\"\n                      type=\"search\"\n                      value=\"\"\n                    />\n                  </div>\n                  <div\n                    class=\"ant-select-suffix\"\n                  >\n                    <span\n                      aria-label=\"down\"\n                      class=\"anticon anticon-down\"\n                      role=\"img\"\n                    >\n                      <svg\n                        aria-hidden=\"true\"\n                        data-icon=\"down\"\n                        fill=\"currentColor\"\n                        focusable=\"false\"\n                        height=\"1em\"\n                        viewBox=\"64 64 896 896\"\n                        width=\"1em\"\n                      >\n                        <path\n                          d=\"M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z\"\n                        />\n                      </svg>\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </form>\n  </div>\n</body>\n`;\n\nexports[`CourseTaskSelect > Should render correctly > if groupBy is deadline 1`] = `\n<body>\n  <div>\n    <form\n      class=\"ant-form ant-form-horizontal css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14\"\n    >\n      <div\n        class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n      >\n        <div\n          class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-col ant-form-item-label css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <label\n              class=\"ant-form-item-required\"\n              for=\"courseTaskId\"\n              title=\"Task\"\n            >\n              Task\n            </label>\n          </div>\n          <div\n            class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <div\n              class=\"ant-form-item-control-input\"\n            >\n              <div\n                class=\"ant-form-item-control-input-content\"\n              >\n                <div\n                  class=\"ant-select ant-select-outlined ant-select-in-form-item css-var-root ant-select-css-var css-dev-only-do-not-override-1enej14 ant-select-single ant-select-show-arrow ant-select-show-search\"\n                >\n                  <div\n                    class=\"ant-select-content\"\n                  >\n                    <div\n                      class=\"ant-select-placeholder\"\n                      style=\"visibility: visible;\"\n                    >\n                      Select task\n                    </div>\n                    <input\n                      aria-autocomplete=\"list\"\n                      aria-expanded=\"false\"\n                      aria-haspopup=\"listbox\"\n                      aria-required=\"true\"\n                      autocomplete=\"off\"\n                      class=\"ant-select-input\"\n                      id=\"courseTaskId\"\n                      role=\"combobox\"\n                      type=\"search\"\n                      value=\"\"\n                    />\n                  </div>\n                  <div\n                    class=\"ant-select-suffix\"\n                  >\n                    <span\n                      aria-label=\"down\"\n                      class=\"anticon anticon-down\"\n                      role=\"img\"\n                    >\n                      <svg\n                        aria-hidden=\"true\"\n                        data-icon=\"down\"\n                        fill=\"currentColor\"\n                        focusable=\"false\"\n                        height=\"1em\"\n                        viewBox=\"64 64 896 896\"\n                        width=\"1em\"\n                      >\n                        <path\n                          d=\"M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z\"\n                        />\n                      </svg>\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n      ,\n    </form>\n  </div>\n</body>\n`;\n\nexports[`CourseTaskSelect > Should render correctly > if groupBy is default 1`] = `\n<body>\n  <div>\n    <form\n      class=\"ant-form ant-form-horizontal css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14\"\n    >\n      <div\n        class=\"ant-form-item css-var-root ant-form-css-var css-dev-only-do-not-override-1enej14 ant-form-item-horizontal\"\n      >\n        <div\n          class=\"ant-row ant-form-item-row css-dev-only-do-not-override-1enej14 css-var-root\"\n        >\n          <div\n            class=\"ant-col ant-form-item-label css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <label\n              class=\"ant-form-item-required\"\n              for=\"courseTaskId\"\n              title=\"Task\"\n            >\n              Task\n            </label>\n          </div>\n          <div\n            class=\"ant-col ant-form-item-control css-dev-only-do-not-override-1enej14 css-var-root\"\n          >\n            <div\n              class=\"ant-form-item-control-input\"\n            >\n              <div\n                class=\"ant-form-item-control-input-content\"\n              >\n                <div\n                  class=\"ant-select ant-select-outlined ant-select-in-form-item css-var-root ant-select-css-var css-dev-only-do-not-override-1enej14 ant-select-single ant-select-show-arrow ant-select-show-search\"\n                >\n                  <div\n                    class=\"ant-select-content\"\n                  >\n                    <div\n                      class=\"ant-select-placeholder\"\n                      style=\"visibility: visible;\"\n                    >\n                      Select task\n                    </div>\n                    <input\n                      aria-autocomplete=\"list\"\n                      aria-expanded=\"false\"\n                      aria-haspopup=\"listbox\"\n                      aria-required=\"true\"\n                      autocomplete=\"off\"\n                      class=\"ant-select-input\"\n                      id=\"courseTaskId\"\n                      role=\"combobox\"\n                      type=\"search\"\n                      value=\"\"\n                    />\n                  </div>\n                  <div\n                    class=\"ant-select-suffix\"\n                  >\n                    <span\n                      aria-label=\"down\"\n                      class=\"anticon anticon-down\"\n                      role=\"img\"\n                    >\n                      <svg\n                        aria-hidden=\"true\"\n                        data-icon=\"down\"\n                        fill=\"currentColor\"\n                        focusable=\"false\"\n                        height=\"1em\"\n                        viewBox=\"64 64 896 896\"\n                        width=\"1em\"\n                      >\n                        <path\n                          d=\"M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z\"\n                        />\n                      </svg>\n                    </span>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </form>\n  </div>\n</body>\n`;\n"
  },
  {
    "path": "client/src/shared/components/Forms/index.ts",
    "content": "export * from './GdprCheckbox';\nexport * from './ScoreInput';\nexport * from './CommentInput';\nexport * from './CourseTaskSelect';\nexport * from './ModalForm';\nexport * from './LocationSelect';\nexport * from './PreparedComment';\nexport * from './MarkdownInput';\nexport { ModalSubmitForm } from './ModalSubmitForm';\n"
  },
  {
    "path": "client/src/shared/components/Forms/useGoogleMapsPlaces.test.ts",
    "content": "import { renderHook, act } from '@testing-library/react';\nimport { useGoogleMapsPlaces } from './useGoogleMapsPlaces';\nimport * as usePlacesAutocompleteModule from 'use-places-autocomplete';\nimport { useInterval } from 'ahooks';\nimport { Location } from '@common/models/profile';\n\nvi.mock('use-places-autocomplete');\nvi.mock('ahooks');\n\ndescribe('useGoogleMapsPlaces', () => {\n  const mockInit = vi.fn();\n  const mockSetValue = vi.fn();\n  const mockStopPolling = vi.fn();\n  const usePlacesAutocompleteMock = vi.mocked(usePlacesAutocompleteModule.default);\n  const useIntervalMock = vi.mocked(useInterval);\n\n  const defaultAutocompleteState = {\n    value: '',\n    suggestions: { data: [], loading: false },\n    setValue: mockSetValue,\n    init: mockInit,\n  };\n\n  const renderUseGoogleMapsPlaces = (location: Location | null = null) =>\n    renderHook(() => useGoogleMapsPlaces(location));\n\n  const setupPollingCallback = () => {\n    let pollCallback: (() => void) | null = null;\n\n    useIntervalMock.mockImplementation((callback: () => void, interval: number | null) => {\n      if (interval !== null) {\n        pollCallback = callback;\n      }\n      return mockStopPolling;\n    });\n\n    return () => pollCallback?.();\n  };\n\n  beforeEach(() => {\n    vi.clearAllMocks();\n    usePlacesAutocompleteMock.mockReturnValue(defaultAutocompleteState);\n    useIntervalMock.mockReturnValue(mockStopPolling);\n    delete (window as unknown as { google?: unknown }).google;\n    delete (Window.prototype as { google?: unknown }).google;\n  });\n\n  it('returns default state when location is null', () => {\n    const { result } = renderUseGoogleMapsPlaces();\n\n    expect(result.current.value).toBe('');\n    expect(result.current.data).toEqual([]);\n    expect(result.current.loading).toBe(false);\n    expect(result.current.initialized).toBe(false);\n    expect(result.current.error).toBe(null);\n  });\n\n  describe('location formatting', () => {\n    it.each([\n      { location: null, expected: '' },\n      { location: { cityName: 'Minsk', countryName: 'Belarus' } as Location, expected: 'Minsk, Belarus' },\n      { location: { cityName: 'São Paulo', countryName: 'Brazil' } as Location, expected: 'São Paulo, Brazil' },\n    ])('uses \"$expected\" as the autocomplete default value', ({ location, expected }) => {\n      renderUseGoogleMapsPlaces(location);\n\n      expect(usePlacesAutocompleteModule.default).toHaveBeenCalledWith(\n        expect.objectContaining({\n          defaultValue: expected,\n          requestOptions: {\n            types: ['(cities)'],\n            debounce: 300,\n          },\n          initOnMount: false,\n        }),\n      );\n    });\n  });\n\n  describe('Google Maps API polling', () => {\n    it('initializes and stops polling when Google Maps API is loaded', async () => {\n      const triggerPoll = setupPollingCallback();\n      Object.defineProperty(window, 'google', {\n        configurable: true,\n        writable: true,\n        value: {},\n      });\n\n      const { result } = renderUseGoogleMapsPlaces();\n\n      await act(async () => triggerPoll());\n\n      expect(result.current.initialized).toBe(true);\n      expect(mockInit).toHaveBeenCalled();\n      expect(mockStopPolling).toHaveBeenCalled();\n      expect(result.current.error).toBe(null);\n    });\n\n    it('sets an error and stops polling when Google Maps API is not loaded', async () => {\n      const triggerPoll = setupPollingCallback();\n      expect('google' in window).toBe(false);\n      const { result } = renderUseGoogleMapsPlaces();\n\n      await act(async () => {\n        for (let attempts = 0; attempts < 500; attempts += 1) {\n          triggerPoll();\n        }\n      });\n\n      expect(mockStopPolling).toHaveBeenCalled();\n      expect(mockInit).not.toHaveBeenCalled();\n      expect(result.current.initialized).toBe(false);\n      expect(result.current.error?.message).toBe('Google Maps API is not loaded');\n    });\n  });\n\n  describe('autocomplete values', () => {\n    it('returns values provided by usePlacesAutocomplete', () => {\n      const data = [{ description: 'Minsk, Belarus' }, { description: 'Munich, Germany' }];\n\n      usePlacesAutocompleteMock.mockReturnValue({\n        ...defaultAutocompleteState,\n        value: 'M',\n        suggestions: { data, loading: true },\n      });\n\n      const { result } = renderUseGoogleMapsPlaces();\n\n      expect(result.current.value).toBe('M');\n      expect(result.current.data).toEqual(data);\n      expect(result.current.loading).toBe(true);\n      expect(result.current.setValue).toBe(mockSetValue);\n    });\n\n    it('forwards setValue calls', () => {\n      const { result } = renderUseGoogleMapsPlaces();\n\n      act(() => {\n        result.current.setValue('New York');\n      });\n\n      expect(mockSetValue).toHaveBeenCalledWith('New York');\n    });\n  });\n\n  it('uses 100ms polling interval', () => {\n    renderUseGoogleMapsPlaces();\n\n    expect(useInterval).toHaveBeenCalledWith(expect.any(Function), 100);\n  });\n});\n"
  },
  {
    "path": "client/src/shared/components/Forms/useGoogleMapsPlaces.ts",
    "content": "import { useRef, useState } from 'react';\nimport usePlacesAutocomplete from 'use-places-autocomplete';\nimport { useInterval } from 'ahooks';\nimport { Location } from '@common/models/profile';\n\nconst MAX_POLLING_TIME_MS = 30_000;\nconst POLLING_INTERVAL_MS = 100;\n\n/**\n * Hook to use Google Maps Places API for location autocomplete\n */\nexport function useGoogleMapsPlaces(location: Location | null) {\n  const [initialized, setInitialized] = useState(false);\n  const [error, setError] = useState<Error | null>(null);\n\n  const elapsedRef = useRef(0);\n\n  const {\n    value,\n    suggestions: { data, loading },\n    setValue,\n    init,\n  } = usePlacesAutocomplete({\n    defaultValue: fromLocation(location),\n    requestOptions: {\n      types: ['(cities)'],\n      debounce: 300,\n    },\n    initOnMount: false,\n  });\n\n  /**\n   * Poll until Google Maps API is loaded or 30 seconds have passed\n   */\n  const stopPolling = useInterval(() => {\n    elapsedRef.current += POLLING_INTERVAL_MS;\n\n    if ('google' in window) {\n      setInitialized(true);\n      init();\n      stopPolling();\n    } else if (elapsedRef.current >= MAX_POLLING_TIME_MS) {\n      stopPolling();\n      setError(new Error('Google Maps API is not loaded'));\n    }\n  }, POLLING_INTERVAL_MS);\n\n  return { value, data, loading, setValue, initialized, error };\n}\n\nconst fromLocation = (value: Location | null): string => {\n  if (value) {\n    return `${value.cityName}, ${value.countryName}`;\n  }\n  return '';\n};\n"
  },
  {
    "path": "client/src/shared/components/GithubAvatar.tsx",
    "content": "import * as React from 'react';\nimport { Avatar } from 'antd';\nimport { CDN_AVATARS_URL } from '@client/configs/cdn';\n\ntype Props = {\n  githubId?: string;\n  size: 24 | 32 | 48 | 96;\n  style?: React.CSSProperties;\n  alt?: string;\n};\n\nexport function GithubAvatar({ githubId, size, style }: Props) {\n  if (!githubId || githubId.startsWith('gdpr-')) {\n    return <Avatar size={size} style={style} />;\n  }\n  return <Avatar src={`${CDN_AVATARS_URL}/${githubId}.png?size=${size * 2}`} size={size} style={style} />;\n}\n"
  },
  {
    "path": "client/src/shared/components/GithubUserLink.module.css",
    "content": ".linkUser,\n.linkUserProfile {\n  white-space: nowrap;\n  display: flex;\n  align-items: center;\n  gap: 0.5ch;\n}\n\n.linkUserProfile {\n  color: inherit;\n}\n\n.linkUserAction {\n  opacity: 0;\n  margin: 0.1ch;\n}\n\n.linkUser:hover .linkUserAction {\n  cursor: pointer;\n  opacity: 1;\n  transition: opacity 0.5s ease-in-out;\n}\n"
  },
  {
    "path": "client/src/shared/components/GithubUserLink.tsx",
    "content": "import { GithubAvatar } from './GithubAvatar';\nimport { CopyOutlined, GithubOutlined } from '@ant-design/icons';\nimport { useMessage } from '@client/hooks';\nimport { useCopyToClipboard } from 'react-use';\nimport { theme } from 'antd';\nimport styles from './GithubUserLink.module.css';\n\ntype Props = {\n  value: string;\n  isUserIconHidden?: boolean;\n  fullName?: string;\n  copyable?: boolean;\n};\n\nexport function GithubUserLink({ value, isUserIconHidden = false, fullName, copyable = true }: Props) {\n  const { token } = theme.useToken();\n  const { message } = useMessage();\n  const [, copyToClipboard] = useCopyToClipboard();\n\n  const handleCopyToClipboard = async () => {\n    copyToClipboard(value);\n    await message.success(\"User's name copied to clipboard\");\n  };\n\n  return (\n    <div className={styles.linkUser} style={{ color: token.colorText }}>\n      <a\n        title=\"Open Rolling Scopes App profile page\"\n        target=\"_blank\"\n        className={styles.linkUserProfile}\n        href={`/profile?githubId=${value}`}\n      >\n        {!isUserIconHidden && <GithubAvatar githubId={value} size={24} />}\n        {fullName || value}\n      </a>\n      <a\n        title=\"Open GitHub profile page\"\n        target=\"_blank\"\n        className={styles.linkUserAction}\n        href={`https://github.com/${value}`}\n      >\n        <GithubOutlined style={{ color: token.colorTextBase }} />\n      </a>\n      {copyable && (\n        <span title=\"Copy GitHub name to clipboard\" className={styles.linkUserAction} onClick={handleCopyToClipboard}>\n          <CopyOutlined style={{ color: token.colorInfo }} />\n        </span>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Header.module.css",
    "content": ".nav {\n  padding: 8px;\n  display: flex;\n  flex-wrap: wrap;\n  justify-content: space-between;\n}\n\n.icons {\n  display: flex;\n  align-items: center;\n}\n\n.logoLink {\n  display: inline-flex;\n}\n\n.headerLogo {\n  height: 56px;\n}\n\n.center {\n  display: flex;\n  align-items: center;\n  gap: 12px;\n}\n\n.title {\n  font-weight: 700;\n}\n\n.menuItemActive {\n  font-weight: 600;\n}\n\n.carousel {\n  flex-shrink: 0;\n}\n\n.controls {\n  gap: 8px;\n}\n\n.avatarButton {\n  display: flex;\n  align-items: center;\n}\n\n@media all and (max-width: 768px) {\n  .center {\n    width: 100%;\n    order: 3;\n    justify-content: center;\n    margin-top: 16px;\n  }\n\n  .carousel {\n    display: none;\n  }\n}\n"
  },
  {
    "path": "client/src/shared/components/Header.tsx",
    "content": "import Link from 'next/link';\nimport { useRouter } from 'next/router';\nimport { useContext, useMemo } from 'react';\nimport { Button, Dropdown, Flex, Menu, MenuProps, Space, theme } from 'antd';\nimport {\n  EyeOutlined,\n  LogoutOutlined,\n  NotificationOutlined,\n  QuestionCircleFilled,\n  SolutionOutlined,\n} from '@ant-design/icons';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { HeaderMiniBannerCarousel, HeaderMiniBannerCarouselItem } from '@client/components/HeaderMiniBannerCarousel';\nimport { SolidarityUkraine } from './SolidarityUkraine';\nimport { SessionContext } from '@client/modules/Course/contexts';\nimport { getNavigationItems } from '@client/modules/Home/data/links';\nimport { useActiveCourseContext } from '@client/modules/Course/contexts/ActiveCourseContext';\nimport ThemeSwitch from '@client/shared/components/ThemeSwitch';\nimport styles from './Header.module.css';\nimport clsx from 'clsx';\n\ntype Props = {\n  showCourseName?: boolean;\n  showCarousel?: boolean;\n  title?: string;\n};\n\ntype MenuItem = Required<MenuProps>['items'][number];\n\nconst MENU_ITEMS = [\n  {\n    link: '/profile',\n    icon: <EyeOutlined />,\n    title: 'Profile',\n  },\n  {\n    link: '/profile/notifications',\n    icon: <NotificationOutlined />,\n    title: 'Notifications',\n  },\n  {\n    link: '/cv/edit',\n    icon: <SolutionOutlined />,\n    title: 'My CV',\n  },\n  {\n    link: 'https://rs.school/docs/en',\n    icon: <QuestionCircleFilled />,\n    title: 'Help',\n    target: '_blank',\n  },\n  {\n    link: '/api/v2/auth/github/logout',\n    icon: <LogoutOutlined />,\n    title: 'Logout',\n  },\n];\n\nconst CAROUSEL_ITEMS: ReadonlyArray<HeaderMiniBannerCarouselItem> = [\n  {\n    banner: '/static/images/banner-mentors-hall-of-fame.svg',\n    url: '/mentors-hall-of-fame',\n    title: 'Mentors Hall of Fame',\n  },\n  {\n    banner: '/static/images/banner-gratitude.svg',\n    url: '/gratitude',\n    title: '#gratitude',\n  },\n];\nconst CAROUSEL_INTERVAL_MS = 5000;\n\nexport function Header({ title, showCourseName, showCarousel = true }: Props) {\n  const { asPath: currentRoute } = useRouter();\n\n  const session = useContext(SessionContext);\n  const { course } = useActiveCourseContext();\n  const courseNotEmpty = course.id ? course : null;\n  const courseLinks = useMemo(() => getNavigationItems(session, courseNotEmpty ?? null), [session, course.id]);\n\n  const menuItems = useMemo((): MenuProps['items'] => {\n    const items = MENU_ITEMS.map(({ title, link, target, icon }) => {\n      const isActive = currentRoute === link;\n\n      return {\n        key: title,\n        label: (\n          <Button type=\"link\" target={target} href={link} className={isActive ? styles.menuItemActive : undefined}>\n            {icon} {title}\n          </Button>\n        ),\n      };\n    });\n\n    const lastItem = items.pop() as MenuItem;\n\n    return [...items, { type: 'divider' }, lastItem];\n  }, [currentRoute]);\n\n  const { token } = theme.useToken();\n\n  return (\n    <Space\n      direction=\"vertical\"\n      size={0}\n      style={{\n        width: '100%',\n        boxShadow: token.boxShadow,\n      }}\n    >\n      <nav\n        className={`${styles.nav} no-print page-header`}\n        style={{\n          background: token.colorBgContainer,\n          color: token.colorTextBase,\n        }}\n      >\n        <Space className={styles.icons}>\n          <Link href=\"/\" className={styles.logoLink}>\n            <img\n              className={clsx(styles.headerLogo, 'header-logo')}\n              src=\"/static/images/logo-rsschool3.png\"\n              alt=\"Rolling Scopes School Logo\"\n            />\n          </Link>\n          <SolidarityUkraine />\n          {showCarousel && (\n            <HeaderMiniBannerCarousel\n              className={styles.carousel}\n              items={CAROUSEL_ITEMS}\n              intervalMs={CAROUSEL_INTERVAL_MS}\n            />\n          )}\n        </Space>\n        <div className={styles.center}>\n          <div className={styles.title}>\n            {title} {showCourseName ? course?.name : null}\n          </div>\n        </div>\n        <Flex align=\"center\" className={styles.controls}>\n          <ThemeSwitch />\n          {session.githubId && (\n            <Dropdown menu={{ items: menuItems }} trigger={['click']}>\n              <Button type=\"link\" className={styles.avatarButton}>\n                <GithubAvatar githubId={session?.githubId} size={32} />\n              </Button>\n            </Dropdown>\n          )}\n        </Flex>\n      </nav>\n      <Menu selectedKeys={[currentRoute]} mode=\"horizontal\" items={courseLinks} />\n    </Space>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Icons/CourseIcon.tsx",
    "content": "import { CheckCircleTwoTone, PlayCircleTwoTone } from '@ant-design/icons';\nimport { DEFAULT_COURSE_ICONS } from '@client/configs/course-icons';\nimport { Course } from '@client/services/models';\nimport { PublicSvgIcon } from '@client/shared/components/Icons/PublicSvgIcon';\n\nexport const CourseIcon = ({ course }: { course: Course }) => {\n  if (course.completed) {\n    return course.logo && DEFAULT_COURSE_ICONS[course.logo] ? (\n      <Logo url={DEFAULT_COURSE_ICONS[course.logo]?.archived ?? ''} />\n    ) : (\n      <CheckCircleTwoTone style={{ display: 'inline' }} twoToneColor=\"#aaa\" />\n    );\n  }\n  return course.logo && DEFAULT_COURSE_ICONS[course.logo] ? (\n    <Logo url={DEFAULT_COURSE_ICONS[course.logo]?.active ?? ''} />\n  ) : (\n    <PlayCircleTwoTone style={{ display: 'inline' }} />\n  );\n};\n\nfunction Logo({ url }: { url: string }) {\n  return <PublicSvgIcon src={url} size=\"16px\" />;\n}\n"
  },
  {
    "path": "client/src/shared/components/Icons/DeadlineIcon.tsx",
    "content": "import ClockCircleOutlined from '@ant-design/icons/ClockCircleOutlined';\nimport { Tooltip } from 'antd';\nimport { formatDateTime } from '@client/services/formatter';\nimport { Group, GroupType } from '../Forms/CourseTaskSelect';\n\nenum Color {\n  Default = '#000',\n  Green = '#52C41A',\n  Orange = '#FAAD14',\n  Red = '#FF4D4F',\n}\n\ntype Props = {\n  group: GroupType;\n  endDate: string | null;\n};\n\nexport function DeadlineIcon({ group, endDate }: Props) {\n  const date = formatDateTime(endDate ?? '');\n  const title = `${group === Group.CrossCheckDeadline ? `Cross-Check Deadline` : 'Deadline'}: ${date}`;\n  const color = getColor(endDate);\n\n  return (\n    <Tooltip title={title} placement=\"bottomRight\">\n      <ClockCircleOutlined style={{ fontSize: '14px', color }} />\n    </Tooltip>\n  );\n}\n\nfunction getColor(endDate: string | null) {\n  if (!endDate) {\n    return Color.Default;\n  }\n\n  const endDateMilliseconds = Date.parse(endDate);\n  const currentDate = new Date();\n  const tomorrowDate = currentDate.setDate(currentDate.getDate() + 1);\n  const dayAfterTomorrowDate = currentDate.setDate(currentDate.getDate() + 1);\n\n  if (tomorrowDate >= endDateMilliseconds) {\n    return Color.Red;\n  }\n  if (dayAfterTomorrowDate >= endDateMilliseconds) {\n    return Color.Orange;\n  }\n  return Color.Green;\n}\n"
  },
  {
    "path": "client/src/shared/components/Icons/DiscordFilled.tsx",
    "content": "import Icon from '@ant-design/icons/lib/components/Icon';\nimport type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';\n\nconst svg = () => (\n  <svg width=\"1em\" height=\"1em\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g clipPath=\"url(#a)\">\n      <path\n        d=\"M12.445 24.002c6.628 0 12-5.373 12-12s-5.372-12-12-12c-6.627 0-12 5.373-12 12s5.373 12 12 12Z\"\n        fill=\"#5865F2\"\n      />\n      <path\n        d=\"M17.901 7.91s-1.56-1.224-3.407-1.364l-.164.333c1.668.407 2.432.993 3.234 1.71-1.378-.703-2.742-1.364-5.114-1.364s-3.736.661-5.114 1.364c.801-.717 1.71-1.368 3.234-1.71l-.164-.333C8.47 6.729 6.998 7.91 6.998 7.91s-1.743 2.531-2.043 7.5c1.757 2.03 4.43 2.043 4.43 2.043l.557-.745a6.807 6.807 0 0 1-2.944-1.983c1.102.835 2.77 1.707 5.457 1.707 2.685 0 4.35-.867 5.456-1.707a6.806 6.806 0 0 1-2.944 1.983l.558.745s2.672-.014 4.43-2.043c-.31-4.969-2.053-7.5-2.053-7.5Zm-7.673 6.136c-.66 0-1.195-.61-1.195-1.364 0-.755.534-1.364 1.195-1.364s1.195.609 1.195 1.364c0 .754-.534 1.364-1.195 1.364Zm4.434 0c-.66 0-1.195-.61-1.195-1.364 0-.755.534-1.364 1.195-1.364s1.196.609 1.196 1.364c0 .754-.54 1.364-1.196 1.364Z\"\n        fill=\"#fff\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"#fff\" transform=\"translate(.445 .002)\" d=\"M0 0h24v24H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nexport const DiscordFilled = (props: Partial<CustomIconComponentProps>) => {\n  return <Icon component={svg} {...props} />;\n};\n"
  },
  {
    "path": "client/src/shared/components/Icons/DiscordOutlined.tsx",
    "content": "import Icon from '@ant-design/icons/lib/components/Icon';\nimport type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';\n\nconst svg = () => (\n  <svg width=\"1em\" height=\"1em\" fill=\"currentColor\" viewBox=\"0 -7 71 71\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g>\n      <path d=\"M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z\" />\n    </g>\n  </svg>\n);\n\nexport const DiscordOutlined = (props: Partial<CustomIconComponentProps>) => {\n  return <Icon component={svg} {...props} />;\n};\n"
  },
  {
    "path": "client/src/shared/components/Icons/GitHubLogoIcon.tsx",
    "content": "import Icon from '@ant-design/icons';\nimport type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';\n\nconst svg = () => (\n  <svg width=\"33\" height=\"32\" viewBox=\"0 0 33 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g clipPath=\"url(#clip0_129393_199365)\">\n      <path\n        d=\"M16.3667 0.437547C7.53455 0.433976 0.380981 7.58398 0.380981 16.409C0.380981 23.3876 4.85598 29.3197 11.0881 31.4983C11.9274 31.709 11.7988 31.1126 11.7988 30.7054V27.9375C6.95241 28.5054 6.75598 25.2983 6.43098 24.7626C5.77384 23.6411 4.22027 23.3554 4.68455 22.8197C5.78812 22.2518 6.91312 22.9626 8.2167 24.8876C9.15955 26.284 10.9988 26.0483 11.931 25.8161C12.1346 24.9768 12.5703 24.2268 13.1703 23.6447C8.14884 22.7447 6.05598 19.6804 6.05598 16.0375C6.05598 14.2697 6.63812 12.6447 7.78098 11.334C7.05241 9.17326 7.84884 7.32326 7.95598 7.04826C10.031 6.86255 12.1881 8.53398 12.356 8.66612C13.5346 8.34826 14.881 8.18041 16.3881 8.18041C17.9024 8.18041 19.2524 8.3554 20.4417 8.67683C20.8453 8.36969 22.8453 6.93398 24.7738 7.10898C24.8774 7.38398 25.656 9.19112 24.9703 11.3233C26.1274 12.6375 26.7167 14.2768 26.7167 16.0483C26.7167 19.6983 24.6096 22.7661 19.5738 23.6518C20.0052 24.076 20.3476 24.5819 20.5813 25.1399C20.8149 25.6979 20.935 26.2969 20.9346 26.9018V30.9197C20.9631 31.2411 20.9346 31.559 21.4703 31.559C27.7953 29.4268 32.3488 23.4518 32.3488 16.4125C32.3488 7.58398 25.1917 0.437547 16.3667 0.437547Z\"\n        fill=\"black\"\n        fillOpacity=\"0.45\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"clip0_129393_199365\">\n        <rect width=\"32\" height=\"32\" fill=\"white\" transform=\"translate(0.380981)\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nexport const GitHubLogoIcon = (props: Partial<CustomIconComponentProps>) => {\n  return <Icon component={svg} {...props} />;\n};\n"
  },
  {
    "path": "client/src/shared/components/Icons/HealthMask.tsx",
    "content": "import Icon from '@ant-design/icons/lib/components/Icon';\n\nexport function HealthMask() {\n  return <Icon component={HealthMaskIcon} />;\n}\n\nfunction HealthMaskIcon() {\n  return (\n    <svg viewBox=\"0 0 1024 1024\" width=\"1em\" height=\"1em\">\n      <path\n        d=\"M928 607.317333a424.106667 424.106667 0 0 0 10.666667-105.472 423.936 423.936 0 0 0-10.837334-84.864l-82.986666 19.498667c2.474667 10.837333 4.309333 21.845333 5.845333 32.853333h-43.349333a299.264 299.264 0 0 0-56.32-136.021333l30.464-30.464c6.698667 8.661333 13.354667 17.493333 19.2 26.837333l72.149333-45.653333a429.610667 429.610667 0 0 0-134.656-133.717333L692.992 222.72c9.685333 5.973333 18.986667 12.8 28.16 19.84l-30.506667 30.464A299.264 299.264 0 0 0 554.666667 216.661333V173.482667c11.008 1.365333 21.845333 2.986667 32.682666 5.546666l18.816-83.2c-61.653333-13.994667-130.346667-13.312-185.514666-0.512l-4.992 1.024 20.693333 82.816c10.965333-2.474667 21.973333-4.138667 32.981333-5.504v43.008a299.264 299.264 0 0 0-136.021333 56.32l-30.464-30.464c8.661333-6.826667 17.664-13.525333 26.965333-19.370666L284.330667 151.04a429.056 429.056 0 0 0-133.845334 134.272L222.848 330.666667c5.973333-9.685333 12.672-18.816 19.626667-27.818667l30.506666 30.506667A299.264 299.264 0 0 0 216.661333 469.333333H173.653333c1.365333-11.008 2.858667-22.186667 5.333334-32.981333l-83.157334-18.858667a422.314667 422.314667 0 0 0 0 188.16v0.853334l83.328-18.858667A380.330667 380.330667 0 0 1 173.354667 554.666667h43.306666a299.264 299.264 0 0 0 56.32 136.021333l-30.464 30.464a352.853333 352.853333 0 0 1-19.498666-27.306667l-72.192 45.312a428.373333 428.373333 0 0 0 134.357333 134.186667l45.312-72.362667a358.698667 358.698667 0 0 1-27.648-19.498666l30.506667-30.464A298.538667 298.538667 0 0 0 469.333333 807.338667v43.178666a313.088 313.088 0 0 1-33.152-5.674666l-19.029333 83.157333a412.16 412.16 0 0 0 105.173333 10.496 421.802667 421.802667 0 0 0 82.858667-10.154667h0.298667l-17.834667-83.498666a309.674667 309.674667 0 0 1-32.981333 5.504v-43.008a297.770667 297.770667 0 0 0 136.021333-56.32l30.464 30.464a317.44 317.44 0 0 1-27.477333 19.669333l45.312 72.192a427.861333 427.861333 0 0 0 134.186666-134.186667l-72.192-45.312c-5.973333 9.301333-12.629333 18.474667-19.498666 27.306667l-30.464-30.506667A299.264 299.264 0 0 0 807.338667 554.666667h43.008c-1.365333 11.178667-2.986667 22.485333-5.546667 33.493333zM298.666667 512c0-23.466667 19.2-42.666667 42.666666-42.666667s42.666667 19.2 42.666667 42.666667-19.2 42.666667-42.666667 42.666667-42.666667-19.2-42.666666-42.666667z m85.333333 170.666667c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667 42.666667 19.2 42.666667 42.666667-19.2 42.666667-42.666667 42.666667z m0-256c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667 42.666667 19.2 42.666667 42.666667-19.2 42.666667-42.666667 42.666667z m128 298.666666c-23.466667 0-42.666667-19.2-42.666667-42.666666s19.2-42.666667 42.666667-42.666667 42.666667 19.2 42.666667 42.666667-19.2 42.666667-42.666667 42.666666z m0-170.666666c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667 42.666667 19.2 42.666667 42.666667-19.2 42.666667-42.666667 42.666667z m0-170.666667c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666666 42.666667 19.2 42.666667 42.666666-19.2 42.666667-42.666667 42.666667z m128-42.666667c23.466667 0 42.666667 19.2 42.666667 42.666667s-19.2 42.666667-42.666667 42.666667-42.666667-19.2-42.666667-42.666667 19.2-42.666667 42.666667-42.666667z m0 341.333334c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667 42.666667 19.2 42.666667 42.666667-19.2 42.666667-42.666667 42.666667z m42.666667-128c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667 42.666667 19.2 42.666666 42.666667-19.2 42.666667-42.666666 42.666667z\"\n        fill=\"#d81e06\"\n        p-id=\"15213\"\n      ></path>\n    </svg>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Icons/LinkedInIcon.tsx",
    "content": "import Icon from '@ant-design/icons/lib/components/Icon';\nimport type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';\n\nconst svg = () => (\n  <svg width=\"25\" height=\"25\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g clipPath=\"url(#a)\" fillRule=\"evenodd\" clipRule=\"evenodd\">\n      <path\n        d=\"M12.445 24.002c6.628 0 12-5.373 12-12s-5.372-12-12-12c-6.627 0-12 5.373-12 12s5.373 12 12 12Z\"\n        fill=\"#007EBB\"\n      />\n      <path\n        d=\"M20.112 19.002h-3.014v-5.133c0-1.407-.534-2.194-1.648-2.194-1.212 0-1.845.819-1.845 2.194v5.133h-2.904V9.224h2.904v1.317s.873-1.616 2.948-1.616c2.074 0 3.559 1.267 3.559 3.886v6.19ZM7.236 7.944c-.99 0-1.79-.808-1.79-1.804 0-.997.8-1.805 1.79-1.805s1.79.808 1.79 1.805c0 .996-.8 1.804-1.79 1.804Zm-1.5 11.058h3.029V9.224H5.737v9.778Z\"\n        fill=\"#fff\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"a\">\n        <path fill=\"#fff\" transform=\"translate(.445 .002)\" d=\"M0 0h24v24H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nexport const LinkedInIcon = (props: Partial<CustomIconComponentProps>) => {\n  return <Icon component={svg} {...props} />;\n};\n"
  },
  {
    "path": "client/src/shared/components/Icons/PublicSvgIcon.tsx",
    "content": "import { useEffect, useState } from 'react';\n\ntype Props = { src?: string; alt?: string; size?: string };\n\nexport function PublicSvgIcon({ src, alt = '', size = 'fit-content' }: Props) {\n  const [svgContent, setSvgContent] = useState<string>('');\n\n  useEffect(() => {\n    const loadSvg = async () => {\n      try {\n        if (!src) {\n          return;\n        }\n        const response = await fetch(src);\n        if (response.ok) {\n          const text = await response.text();\n          setSvgContent(text);\n        }\n      } catch (err) {\n        console.error('Error loading SVG:', err);\n      }\n    };\n\n    loadSvg();\n  }, [src]);\n\n  return (\n    <span\n      style={{ width: size, height: size, display: 'inline-block' }}\n      role=\"img\"\n      aria-label={alt}\n      dangerouslySetInnerHTML={{ __html: svgContent }}\n    />\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Icons/RSLogoIcon.tsx",
    "content": "import Icon from '@ant-design/icons';\nimport type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';\n\nconst svg = () => (\n  <svg width=\"38\" height=\"32\" viewBox=\"0 0 38 32\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g clipPath=\"url(#clip0_129393_199300)\">\n      <path\n        d=\"M15.7038 1.23307C17.9352 0.264417 20.3909 -0.131691 22.8058 0.038239C25.1699 0.220602 27.4941 0.954796 29.5363 2.19404C31.3665 3.30065 32.9741 4.7939 34.228 6.55418C35.692 8.604 36.6689 11.0191 37.0381 13.5332C37.3215 15.5261 37.2637 17.5795 36.7992 19.5392C37.1015 20.1461 37.2527 20.8449 37.1507 21.5234C37.034 22.5448 36.3959 23.479 35.5074 23.9522C35.3047 24.0949 35.0047 24.0972 34.8668 24.324C33.2482 26.9848 30.8558 29.1482 28.066 30.4349C27.4838 30.7138 26.8773 30.9347 26.2687 31.1431L26.1423 31.1715C23.21 32.0952 20.0092 32.0626 17.0808 31.1348C14.8887 30.4225 12.8466 29.2206 11.1605 27.6142C9.54605 26.0872 8.2501 24.2073 7.39085 22.1333C7.00771 21.2232 6.73016 20.2712 6.50397 19.3096C6.10005 19.0563 5.6131 18.9839 5.14797 19.0392C4.18728 19.141 3.27847 19.4972 2.34883 19.7412C3.01125 19.2403 3.73369 18.8234 4.48266 18.476C3.29284 18.492 2.1261 18.7791 0.972054 19.0539C0.662771 19.1327 0.353429 19.2137 0.0476074 19.3043C1.23569 18 2.79885 17.1173 4.41047 16.4859C3.85936 16.4137 3.30086 16.3467 2.77456 16.1532C2.2812 15.9839 1.82131 15.7216 1.40816 15.3983C1.80632 15.3859 2.20619 15.4119 2.60318 15.3645C3.82877 15.3735 5.11553 15.3053 6.20842 14.6736C6.40916 11.947 7.32316 9.27963 8.82342 7.02081C10.5083 4.46471 12.9272 2.422 15.7038 1.23307Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M19.9721 0.607228C21.938 0.399997 23.9477 0.53677 25.8473 1.11406C29.8651 2.28876 33.3341 5.27526 35.1667 9.12622C36.0727 10.9925 36.6002 13.0535 36.693 15.1366C36.6798 15.5721 36.7565 16.0069 36.7005 16.4415C36.6878 17.2993 36.5752 18.1514 36.4212 18.9939C36.3104 18.8866 36.1961 18.7832 36.0785 18.6838C36.2475 17.5718 36.3706 16.4425 36.3001 15.3165C36.2851 15.0221 36.2758 14.728 36.2475 14.4348C36.0179 12.0972 35.2488 9.8159 34.0035 7.84304C32.7508 5.84006 31.0283 4.14846 29.0238 2.95185C26.9465 1.71082 24.567 0.992619 22.1619 0.934003C21.8727 0.890188 21.5797 0.922161 21.2888 0.912095L21.2502 0.941108C18.7257 0.977815 16.2271 1.773 14.0748 3.11645C11.9652 4.44154 10.1874 6.31965 8.96529 8.52696C8.02243 10.2126 7.41315 12.0913 7.17023 14.018C7.0565 14.1601 6.88801 14.2394 6.73804 14.3336C7.01213 11.4626 8.1252 8.68979 9.86203 6.42439C11.675 4.05071 14.1776 2.23536 16.9693 1.28926C17.9444 0.958342 18.9524 0.730975 19.9721 0.607228Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M20.6346 1.8483C20.9328 1.69376 21.3483 1.67067 21.5964 1.93296C21.766 2.08987 21.7932 2.33678 21.8498 2.55111C21.6876 2.56532 21.5249 2.57479 21.3628 2.57302C21.3074 2.46763 21.3052 2.30006 21.177 2.25625C21.0737 2.22901 20.9676 2.2592 20.8648 2.27105C20.8567 2.35986 20.8521 2.44868 20.8499 2.53808C21.1429 2.6488 21.4718 2.71156 21.7015 2.94662C21.9583 3.1953 21.9381 3.62043 21.7845 3.92121C21.5555 4.3629 20.9132 4.42384 20.5555 4.11955C20.3644 3.93424 20.3282 3.65713 20.2889 3.4049C20.4632 3.4049 20.6374 3.4049 20.8118 3.40313C20.8499 3.51148 20.8648 3.63345 20.937 3.727C21.0079 3.83654 21.1452 3.79391 21.2532 3.79983C21.3292 3.66246 21.3721 3.50017 21.3218 3.3457C21.1539 3.18346 20.904 3.18583 20.706 3.07984C20.2491 2.87498 20.2006 2.10467 20.6346 1.8483Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M22.9229 1.87364C23.2086 1.81443 23.5651 1.82331 23.7758 2.0637C23.9495 2.24488 23.9714 2.51487 23.9691 2.75526C23.8185 2.75112 23.6685 2.73809 23.5196 2.71914C23.4705 2.60902 23.4412 2.49178 23.3967 2.37988C23.298 2.36507 23.1803 2.28218 23.0903 2.35797C22.9736 2.41303 22.9588 2.59954 23.0834 2.65402C23.328 2.78723 23.6062 2.89026 23.777 3.12768C23.9939 3.4036 23.9299 3.81688 23.7464 4.09279C23.4516 4.52087 22.7337 4.49843 22.4527 4.06917C22.3223 3.87076 22.3315 3.61977 22.3235 3.39181C22.4949 3.41313 22.6662 3.4374 22.8388 3.44688C22.8694 3.58247 22.8562 3.74351 22.9588 3.8489C23.0177 3.88449 23.0858 3.8993 23.151 3.91883C23.1844 3.89989 23.2502 3.86259 23.2831 3.84358C23.354 3.7145 23.369 3.56766 23.3431 3.42319C23.1117 3.2728 22.8278 3.19939 22.6351 2.99334C22.3638 2.64632 22.4822 2.011 22.9229 1.87364ZM18.2958 2.24133C18.5082 2.06903 18.8008 2.02698 19.0592 2.09567C19.3616 2.2058 19.5641 2.50895 19.6061 2.82927C19.4365 2.87368 19.2662 2.9169 19.0937 2.94295C19.0672 2.8571 19.0413 2.77125 19.0153 2.68599C18.9241 2.66467 18.8336 2.64394 18.7425 2.62322C18.6519 2.68776 18.5359 2.73987 18.5117 2.86302C18.5647 3.19162 18.6328 3.51787 18.7078 3.84115C18.7488 4.06023 19.0494 4.13074 19.1925 3.972C19.2894 3.88555 19.253 3.73878 19.2727 3.62385C19.4291 3.57353 19.5877 3.5232 19.7511 3.5007C19.8612 3.65583 19.8521 3.86898 19.8075 4.0472C19.6944 4.35923 19.3788 4.52916 19.0817 4.60731C18.6892 4.66357 18.2462 4.39002 18.1849 3.97141C18.1157 3.6055 18.0424 3.24018 17.9818 2.87308C17.9324 2.61494 18.1065 2.38698 18.2958 2.24133ZM26.3736 3.48767C26.5017 3.21235 26.7458 2.93999 27.0677 2.9542C27.5143 2.95716 28.0198 3.33433 27.9447 3.8376C27.8801 4.18397 27.6845 4.48357 27.5628 4.80981C27.4244 5.10586 27.3234 5.42203 27.1572 5.70387C26.9996 5.66183 26.8571 5.57899 26.7094 5.51209C26.7463 5.28591 26.868 5.09052 26.9511 4.88151C27.0872 4.55053 27.2425 4.22784 27.3712 3.8939C27.4242 3.78259 27.3555 3.67543 27.321 3.57181C27.1836 3.55109 26.9932 3.50313 26.9194 3.66536C26.6856 4.19173 26.4645 4.72402 26.2305 5.24979C26.0394 5.23025 25.8686 5.13434 25.7058 5.03783C25.9246 4.51963 26.1536 4.00575 26.3736 3.48767Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M14.1073 4.05728C14.2573 3.96486 14.4056 3.86781 14.563 3.78906C14.9157 4.36102 15.2197 4.96376 15.5671 5.53934C15.8279 5.3883 16.0812 5.22488 16.3472 5.0858C16.4361 5.23619 16.5203 5.38895 16.5983 5.54526C16.1793 5.77676 15.7743 6.03373 15.3582 6.27181C14.9399 5.53389 14.5325 4.79029 14.1073 4.05728Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M28.6245 4.24034C28.9989 3.98455 29.5725 4.13316 29.7825 4.54112C29.8455 4.64888 29.8754 4.77204 29.9158 4.89045C29.9043 5.05683 29.8824 5.23208 29.7809 5.36945C29.5621 5.67971 29.359 6.0018 29.13 6.30371C28.9621 6.51929 28.6654 6.56483 28.4156 6.50627C27.9494 6.41568 27.6049 5.86379 27.7941 5.40018C27.9141 5.17755 28.0745 4.98039 28.2089 4.76664C28.3405 4.58612 28.445 4.37711 28.6245 4.24034Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M28.7508 4.83821C28.8131 4.73341 28.9216 4.679 29.0279 4.63098C29.1156 4.67071 29.2033 4.71151 29.2898 4.75538C29.3365 4.88919 29.396 5.04781 29.3036 5.17754C29.1669 5.39306 29.022 5.60325 28.8744 5.81048C28.7376 5.9816 28.4889 5.99047 28.325 5.85607C28.3019 5.78976 28.2788 5.72463 28.2557 5.65891C28.3648 5.35339 28.5777 5.1059 28.7508 4.83821Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M12.7074 5.14134C12.9936 4.83523 13.5389 4.76891 13.8378 5.0916C14.2341 5.53271 14.6427 5.96375 15.0288 6.4161C14.903 6.53512 14.7813 6.65886 14.6554 6.77728C14.2429 6.37466 13.8886 5.91407 13.4783 5.50968C13.3924 5.41547 13.2591 5.4552 13.1489 5.45165C13.0778 5.56356 12.9994 5.7217 13.101 5.84184C13.4662 6.26636 13.8493 6.6755 14.2308 7.08522C14.229 7.12135 14.2279 7.15805 14.2268 7.19482C14.0969 7.3014 13.9798 7.42278 13.8569 7.53764C13.513 7.14681 13.1518 6.77201 12.8119 6.37768C12.7167 6.27821 12.6128 6.13912 12.4582 6.18407C12.2995 6.22966 12.202 6.4505 12.3359 6.57899C12.715 7.01477 13.1085 7.43752 13.5004 7.86205C13.3843 8.00651 13.2447 8.1273 13.1056 8.24815C12.7127 7.82954 12.333 7.39732 11.9423 6.97688C11.7151 6.75425 11.6752 6.38953 11.7994 6.10118C11.8997 5.79921 12.2026 5.62579 12.5026 5.61152C12.5458 5.44508 12.5868 5.26929 12.7074 5.14134Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M17.3032 5.99942C19.3448 5.03787 21.6886 4.78149 23.8842 5.26226C26.0048 5.71705 27.9719 6.86926 29.449 8.49217C30.2562 9.3844 30.921 10.4105 31.4074 11.5212C31.7363 12.3135 32.004 13.1384 32.1378 13.9909C31.034 13.7689 29.9439 13.4675 28.8839 13.0796C28.3808 12.8931 27.8823 12.6871 27.412 12.4242C27.3376 12.3845 27.2712 12.3324 27.2198 12.2655C27.0069 12.0062 26.794 11.7481 26.5805 11.4899C26.5268 11.3886 26.4113 11.427 26.322 11.4222C26.3029 11.3172 26.2869 11.2116 26.2741 11.1055C26.0647 10.8864 26.0693 10.575 26.0595 10.2889C25.9729 9.42156 26.0145 8.52989 25.7652 7.68616C25.438 6.93894 24.7675 6.33442 23.9695 6.19232C23.4056 6.10179 22.8224 6.14673 22.2741 6.30778C21.2551 6.60086 20.3238 7.14031 19.4405 7.72702C18.6425 8.26528 17.8842 8.86382 17.1502 9.4897C16.2427 9.54127 15.4036 9.93619 14.6062 10.3536C14.0834 10.6147 13.5618 10.9036 12.9877 11.0268C12.702 11.0641 12.4147 11.0866 12.1337 11.1564C13.2076 8.89296 15.0673 7.03611 17.3032 5.99942Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M30.4698 5.48844C30.5672 5.38779 30.6521 5.24272 30.8097 5.25279C31.0641 5.57074 31.4727 5.77975 31.5806 6.20191C31.6175 6.4323 31.5684 6.68979 31.4074 6.86209C31.172 7.13386 30.9308 7.4009 30.681 7.65904C30.4813 7.87456 30.1547 7.89227 29.8938 7.80648C29.5656 7.6247 29.314 7.33156 29.0427 7.07406C29.505 6.53283 30.0024 6.02481 30.4698 5.48844Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M30.7699 6.01126C30.9176 6.10244 31.0387 6.23863 31.0475 6.42513C30.7855 6.71585 30.5241 7.00775 30.2569 7.29373C30.1743 7.29196 30.0914 7.29077 30.0094 7.28899C29.9396 7.21913 29.8691 7.14926 29.7993 7.08052C30.128 6.72914 30.4516 6.37268 30.7699 6.01126Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M20.8433 7.9747C21.6472 7.50044 22.5023 7.05222 23.4359 6.94263C24.0279 6.86092 24.695 7.04032 25.0648 7.54952C25.3665 7.88826 25.4001 8.36726 25.4411 8.80179C25.4942 9.23816 25.4705 9.68046 25.5363 10.1162C25.3706 10.1488 25.2067 10.1974 25.0383 10.211C24.9517 10.1689 24.8658 10.1281 24.7895 10.0695C24.7601 9.95105 24.7422 9.83021 24.7243 9.70945C24.044 9.83915 23.3267 9.98362 22.77 10.4335C22.49 10.6526 22.3244 10.9913 22.2667 11.343C22.2662 11.4489 22.2638 11.5556 22.2713 11.6627C22.3655 11.7431 22.5004 11.7219 22.614 11.7508C22.4554 12.05 22.3134 12.3632 22.2701 12.7042V12.9411C22.5361 12.8984 22.7623 12.7421 23.0111 12.6479C23.0001 12.7089 22.9787 12.8309 22.9684 12.8919C22.9632 12.9748 22.9586 13.0577 22.9546 13.1406C23.0065 13.2717 23.0692 13.3981 23.1421 13.5183C23.5004 13.4383 23.8103 13.2258 24.1525 13.1002L24.1132 13.3039C24.1271 13.3441 24.1542 13.4241 24.1674 13.4637C24.536 13.3714 24.8845 13.2091 25.1951 12.9853C25.5997 12.7189 25.9926 12.3689 26.4848 12.3031C26.7595 12.4808 26.8875 12.8479 27.1778 13.0191C27.6499 13.3115 28.1623 13.5271 28.6758 13.7296C29.7306 14.1317 30.816 14.4489 31.9169 14.6863C32.4893 14.8148 33.0727 14.9025 33.6277 15.1002C33.9383 15.2156 34.251 15.4104 34.3883 15.7332C34.6467 16.3672 34.6434 17.1175 34.3387 17.7338C34.0659 18.2502 33.5788 18.6717 33.0087 18.7841C30.7259 19.3258 28.4063 19.6901 26.0857 20.0068C25.3961 20.0849 24.7082 20.1702 24.0169 20.23C22.4233 20.3763 20.8231 20.4585 19.2218 20.4385C18.6973 20.4462 18.1682 20.4338 17.6558 20.3089C17.455 20.2308 17.2489 20.1431 17.1041 19.9738C16.9057 19.776 16.8503 19.4883 16.7995 19.2213C16.7146 18.731 16.601 18.2431 16.5848 17.7433C16.5669 17.3087 16.7106 16.8936 16.7908 16.4721C17.0395 15.2004 17.2345 13.916 17.5513 12.6584C17.5933 12.5045 17.5697 12.3416 17.6113 12.1879C17.7781 11.4726 17.9748 10.7655 18.1548 10.0539C18.1762 9.99293 18.1843 9.91943 18.2345 9.87623C19.065 9.18848 19.9235 8.53185 20.8433 7.9747Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M30.4115 8.4749C31.0849 7.97517 31.756 7.47244 32.4305 6.97449C32.6711 7.33039 32.9255 7.67617 33.1702 8.02906C33.0312 8.12972 32.9001 8.2434 32.7496 8.3251C32.5846 8.13326 32.4466 7.91834 32.3023 7.70987C32.1951 7.78689 32.0866 7.86203 31.9781 7.93663C31.9844 8.10953 32.1236 8.22735 32.2031 8.36951C32.0755 8.46715 31.9463 8.56372 31.8136 8.65431C31.7254 8.52819 31.6405 8.39971 31.5412 8.28306C31.4264 8.35175 31.319 8.43168 31.2106 8.50919L31.1345 8.60635C31.2811 8.80943 31.4276 9.01246 31.5656 9.22271C31.4288 9.32929 31.2886 9.43058 31.1437 9.52517C30.9043 9.1718 30.6337 8.84022 30.4115 8.4749Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M10.7334 7.25645C10.7876 7.1812 10.832 7.09481 10.9076 7.03975C11.0582 7.10843 11.1782 7.2304 11.3116 7.32869C11.2066 7.47849 11.0987 7.62532 11.0017 7.78163C11.5343 8.17294 12.06 8.57385 12.5937 8.9628C12.5146 9.1168 12.4102 9.2541 12.3081 9.39206C11.7651 8.99773 11.2331 8.588 10.6895 8.19485C10.5972 8.33518 10.5095 8.47965 10.4033 8.60932C10.2342 8.60044 10.1165 8.42991 9.97522 8.34762C10.2054 7.96814 10.4875 7.62532 10.7334 7.25645Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M9.38088 9.4768C9.47143 9.2962 9.56262 9.1156 9.67285 8.94691C10.3855 9.34481 11.1166 9.71303 11.8176 10.1317C11.8263 10.3319 11.6613 10.5106 11.5696 10.683C11.2746 10.5385 11.0046 10.3395 10.6965 10.2271C10.6509 10.3301 10.6042 10.4337 10.5586 10.5379C10.8366 10.7084 11.1448 10.8268 11.4085 11.0234C11.3236 11.1998 11.2308 11.3722 11.1407 11.5456C10.4096 11.1685 9.69301 10.7611 8.96941 10.3709C8.94229 10.2584 9.03692 10.1548 9.07559 10.0536C9.12694 9.97948 9.14772 9.8623 9.23831 9.82974C9.53145 9.96423 9.80258 10.1454 10.0957 10.2815C10.15 10.178 10.2053 10.0749 10.2642 9.97364C9.97114 9.80546 9.67117 9.64969 9.38088 9.4768Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M31.4012 9.7585C31.4663 9.57619 31.6867 9.5649 31.827 9.66024C31.8882 9.78928 31.8933 9.92612 31.8235 10.0528C31.7531 10.0812 31.6833 10.1097 31.6134 10.1392C31.4847 10.0652 31.295 9.93205 31.4012 9.7585Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M22.8895 10.9492C23.4192 10.4814 24.0862 10.2446 24.7422 10.0338L24.7895 10.0694C24.8656 10.128 24.9518 10.1688 25.0383 10.2109C24.3094 10.5063 23.6072 10.8663 22.8883 11.1848C22.8871 11.1055 22.8878 11.0279 22.8895 10.9492Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M32.0128 10.5672C32.1092 10.334 32.3278 10.1919 32.5453 10.0942C32.6198 10.27 32.6982 10.4441 32.7715 10.6212C32.6221 10.7034 32.4824 10.8247 32.5148 11.0167C32.6013 11.0391 32.6919 11.1174 32.7837 11.0694C33.33 10.8438 33.8523 10.5614 34.3924 10.3209C34.4703 10.5069 34.5586 10.6874 34.6313 10.8758C34.2383 11.0706 33.8385 11.2511 33.4455 11.4459C33.1744 11.5578 32.8927 11.7242 32.5904 11.6656C32.1264 11.5863 31.8097 11.0089 32.0128 10.5672Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M13.4551 11.8007C14.8354 11.5154 15.9496 10.4467 17.3674 10.2968C17.2034 10.9843 17.005 11.6629 16.844 12.3514C16.7955 12.567 16.8036 12.7926 16.7384 13.0045C16.3697 14.4735 16.1845 15.9827 15.8412 17.4576C15.7482 17.9004 15.8002 18.3582 15.8821 18.7986C15.923 19.0929 15.9837 19.3865 16.0956 19.6624C15.6231 19.6833 15.1591 19.8485 14.7569 20.1001C14.2982 20.3772 13.9537 20.8638 13.4003 20.9549C12.7072 21.0065 12.1119 20.5494 11.4338 20.4967C11.188 20.4955 10.9589 20.6216 10.7114 20.6116C10.7564 20.5772 10.8015 20.5441 10.847 20.5109C11.1938 20.3245 11.5388 20.1089 11.9342 20.0508C12.4164 19.9946 12.9018 19.9596 13.3806 19.8779C13.6426 19.8294 13.8839 19.7027 14.0835 19.5233C14.3466 19.2959 14.7789 19.2468 14.8989 18.8726C14.9242 18.6061 14.8701 18.1763 14.5452 18.1409C14.2682 18.1816 14.0402 18.3717 13.8268 18.5441C13.4713 18.8312 13.1505 19.1657 12.7668 19.4151C12.5942 19.5371 12.3761 19.5015 12.1789 19.5045C11.8868 19.4998 11.6094 19.3867 11.3196 19.3641C10.8816 19.4116 10.4633 19.565 10.0485 19.7094C9.73047 19.81 9.40558 19.9202 9.06869 19.903C8.61112 19.8894 8.17488 19.7183 7.7704 19.5093C8.04852 19.4998 8.32953 19.5253 8.60593 19.4785C9.54129 19.3405 10.3877 18.8751 11.2988 18.6448C11.5897 18.5572 11.903 18.6471 12.1892 18.5328C12.6191 18.38 13.0242 18.0112 13.083 17.528C13.1125 17.3409 13.0404 17.1669 12.9555 17.0075C12.697 16.7667 12.3376 16.9124 12.072 17.0603C11.3058 17.5056 10.6763 18.1569 9.91574 18.6109C9.70975 18.7311 9.47436 18.8022 9.23603 18.7923C8.72652 18.7781 8.23836 18.6116 7.73745 18.5318C7.27525 18.4435 6.80383 18.4607 6.3365 18.4796C7.21645 18.0426 8.2199 18.0883 9.16217 17.9111C9.50722 17.8431 9.88345 17.7963 10.1656 17.5637C10.2776 17.4808 10.2816 17.328 10.3006 17.2013C10.1824 16.8608 9.90827 16.5364 9.53214 16.5287C8.94873 16.5471 8.46403 16.9259 7.99492 17.2392C7.67063 17.4541 7.34282 17.7643 6.92679 17.7141C6.50037 17.6549 6.08382 17.5105 5.64927 17.5163L5.61175 17.4908C5.19342 17.4831 4.77571 17.5287 4.36487 17.6074C4.71108 17.3854 5.06076 17.1541 5.45769 17.0403C5.92681 16.9 6.42362 16.9561 6.90025 16.8674C7.69486 16.6365 8.2072 15.9089 8.91752 15.521C9.12992 15.4151 9.37105 15.3269 9.60933 15.3807C10.2089 15.4802 10.5649 16.0805 11.1391 16.2423C11.6515 16.3713 12.1875 16.1718 12.6018 15.8632C12.8268 15.7029 13.011 15.4725 13.0721 15.1936C12.9688 14.8987 12.6099 14.8413 12.3439 14.8667C11.8979 14.9076 11.6359 15.3269 11.2596 15.5204C10.6988 15.4571 10.2892 15.0189 9.77443 14.8331C9.44329 14.7473 9.05895 14.7887 8.79057 15.0218C8.36305 15.3748 7.91465 15.7288 7.3786 15.8881C6.75599 16.0953 6.08382 15.9924 5.47564 15.7828C6.11152 15.7165 6.75663 15.5772 7.30768 15.2297C7.98337 14.8166 8.48596 14.0823 9.2961 13.9248C9.74671 13.8046 10.1922 14.0119 10.5793 14.231C10.824 14.3564 11.0901 14.5051 11.3728 14.4499C11.7992 14.3504 12.1084 14.0123 12.4679 13.7826C12.6382 13.6696 12.8683 13.6595 12.9976 13.4836C13.1062 13.3911 13.0698 13.2391 13.079 13.1129C12.9682 12.9867 12.8176 12.8872 12.645 12.9217C12.0842 12.9715 11.7524 13.5322 11.2302 13.6849C11.0316 13.7288 10.843 13.6352 10.6577 13.576C10.3502 13.4664 10.0282 13.3853 9.69986 13.4008C9.39923 13.3853 9.11365 13.492 8.82162 13.5447C8.95901 13.4198 9.09454 13.286 9.26714 13.2131C9.67681 13.0224 10.1408 13.1041 10.5707 13.0064C10.9624 12.9075 11.2624 12.6162 11.5977 12.4049C11.948 12.1603 12.3432 11.9816 12.7604 11.8992C12.9883 11.8422 13.226 11.8552 13.4551 11.8007Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M23.2715 11.7795C23.7567 11.4254 24.2236 11.0222 24.7803 10.7896C24.6545 10.9358 24.5126 11.0655 24.3626 11.1832C23.7902 11.638 23.2098 12.0814 22.6154 12.5053C22.7343 12.1874 23.0321 11.9962 23.2715 11.7795Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M25.2772 11.3572C25.4422 11.298 25.6205 11.2814 25.7919 11.3228C25.8051 11.6431 25.5057 11.8302 25.3066 12.0274C24.6846 12.5366 23.9605 12.8929 23.2271 13.1984C23.2704 12.8853 23.3742 12.5868 23.4832 12.2932C24.0602 11.9428 24.6372 11.5756 25.2772 11.3572Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M33.8027 12.1683C33.929 11.9278 34.2055 11.8338 34.4542 11.8054C34.9354 11.8123 35.2078 12.3951 35.097 12.8255C35.022 13.1197 34.7433 13.269 34.4895 13.3726C34.4455 13.2121 34.4058 13.0504 34.3655 12.8894C34.4525 12.8142 34.6164 12.7816 34.6159 12.6402C34.6188 12.5549 34.6015 12.4714 34.5928 12.3873C34.5115 12.3618 34.4296 12.3388 34.3476 12.3204L34.2697 12.459C34.2576 12.8083 34.2356 13.2092 33.9627 13.4591C33.763 13.6675 33.452 13.6622 33.1907 13.6225C32.7359 13.4982 32.5265 12.9174 32.691 12.4911C32.8081 12.2311 33.077 12.1085 33.3245 12.0157C33.3759 12.1879 33.4186 12.3625 33.4589 12.5378C33.3291 12.6207 33.1018 12.6841 33.138 12.8867C33.1167 12.9932 33.22 13.043 33.2793 13.1093C33.4104 13.0951 33.5806 13.1158 33.6597 12.9797C33.7277 12.7136 33.6861 12.4216 33.8027 12.1683Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M19.4355 14.7776C19.5901 14.2554 20.0904 13.9498 20.5953 13.8858C20.9981 13.861 21.4355 13.9255 21.754 14.2003C21.9726 14.3726 22.0563 14.6544 22.132 14.9144C22.2554 14.9144 22.3795 14.9175 22.5035 14.9263C22.5242 14.5918 22.8035 14.3389 23.0933 14.2298C23.2727 14.1547 23.4435 14.2595 23.6004 14.3383C23.7338 14.2719 23.852 14.1458 24.013 14.169C24.1509 14.1529 24.2894 14.2891 24.4204 14.2093C24.6148 14.0985 24.8663 14.0642 25.0609 14.1939C25.2646 14.2915 25.3339 14.529 25.387 14.7345C25.7955 14.5882 26.2853 14.5575 26.6459 14.8439C26.7515 14.796 26.856 14.7451 26.9604 14.6936C27.0361 14.3354 27.4197 14.1003 27.7624 14.1927C28.1414 14.2761 28.3867 14.7671 28.1404 15.1075C28.1993 15.3147 28.2154 15.5362 28.1381 15.7416C28.2905 15.8967 28.456 16.0844 28.4203 16.3237C28.4486 16.6475 28.2148 16.905 27.9737 17.0756C28.1283 17.3172 28.0331 17.6067 28.1226 17.8654C28.1809 18.1141 28.1358 18.4013 27.9408 18.5772C27.673 18.871 27.2484 18.8276 26.8947 18.894C26.8301 18.8514 26.7655 18.8087 26.7008 18.7679C26.3408 18.9418 25.8959 18.9739 25.5647 18.7151C25.4346 18.8412 25.2772 18.9337 25.1054 18.9852C24.9294 19.2331 24.6975 19.4814 24.3904 19.5365C24.0938 19.5472 23.8873 19.273 23.8342 18.9972C23.5364 19.0931 23.2295 19.0499 22.9398 18.9474C22.6864 19.1559 22.3345 19.1806 22.0339 19.077C21.9053 19.3345 21.724 19.5707 21.4654 19.7004C20.9138 19.9834 20.1556 19.8887 19.7597 19.3753C19.4291 18.9568 19.4833 18.3019 19.8728 17.9414C19.8151 17.8117 19.7706 17.6766 19.7465 17.5369C19.6686 17.5132 19.5913 17.4889 19.5145 17.4659C19.4406 17.5056 19.3668 17.5452 19.2935 17.5856C18.9028 17.5868 18.5076 17.5459 18.1251 17.6502C18.0172 17.5707 17.8884 17.5104 17.8174 17.3908C17.7505 17.2682 17.7644 17.1243 17.761 16.9899C17.2613 16.8395 17.0991 16.1231 17.446 15.743C17.6606 15.5055 17.9935 15.4664 18.2918 15.4806C18.4806 15.3864 18.7038 15.2603 18.9058 15.4041C19.1592 15.5179 19.1234 15.8406 19.2532 16.0467C19.446 16.1787 19.6618 15.9928 19.8696 15.9923C19.451 15.7676 19.3345 15.2187 19.4355 14.7776Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M20.3805 14.3331C20.7187 14.23 21.1071 14.2377 21.4156 14.4261C21.7417 14.612 21.8472 15.1051 21.6239 15.4131C21.5311 15.5398 21.3764 15.5889 21.2408 15.6487C21.4064 16.0968 21.662 16.6049 22.1432 16.7495C22.4214 16.827 22.6396 16.5452 22.9095 16.5719C23.0601 16.5758 23.0486 16.7689 23.0613 16.879C22.8115 17.2733 22.2691 17.342 21.879 17.1448C21.4878 16.9394 21.2357 16.5362 21.0775 16.1282C21.0146 15.9654 20.9638 15.7989 20.9102 15.6332C20.8853 15.6369 20.8345 15.6433 20.8098 15.6469C20.8156 15.7451 20.8219 15.844 20.8288 15.9429C20.8901 15.9713 20.9512 15.9997 21.0129 16.0286C20.9972 16.1015 20.9805 16.175 20.9627 16.2477C20.8509 16.4486 20.6811 16.6101 20.4837 16.7202C20.4272 16.665 20.3562 16.623 20.3192 16.5513C20.366 16.4165 20.4698 16.3028 20.4743 16.1534C20.4825 15.8965 20.4473 15.6401 20.4519 15.3832C20.4581 15.2304 20.3521 15.112 20.2736 14.9953C20.3959 14.826 20.6042 14.8556 20.7848 14.8598C20.8027 14.8917 20.8211 14.9237 20.8401 14.9562C20.8153 15.0919 20.8044 15.2291 20.7997 15.3671C20.9643 15.3665 21.165 15.3883 21.2723 15.2291C21.4407 15.036 21.2844 14.7366 21.0668 14.6696C20.7321 14.5529 20.2491 14.6383 20.1153 15.0184C20.1713 15.2333 20.3778 15.3879 20.3641 15.6217C20.2753 15.656 20.1812 15.736 20.0837 15.6816C19.8107 15.5371 19.7178 15.1722 19.8061 14.8864C19.8751 14.6077 20.1203 14.41 20.3805 14.3331Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M24.632 14.5219C24.7209 14.4887 24.8166 14.5107 24.909 14.513C24.987 14.6563 25.0382 14.876 24.8922 14.992C24.7769 15.0376 24.6066 15.0305 24.5542 14.8949C24.4699 14.7718 24.5461 14.619 24.632 14.5219Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M22.8842 14.8885C22.9586 14.7256 23.1248 14.6392 23.2777 14.57C23.4854 15.056 23.366 15.6061 23.46 16.1159C23.5115 16.1781 23.5708 16.2337 23.6279 16.2918C23.6296 16.3237 23.6325 16.3882 23.6342 16.4202C23.4329 16.5179 23.2049 16.4924 22.9896 16.4771C22.9879 16.441 22.9851 16.3681 22.9834 16.3319C23.0244 16.2675 23.0659 16.2029 23.0976 16.1332C23.0894 15.7827 23.0427 15.4338 23.0491 15.0828C22.9938 15.0193 22.8502 15.0021 22.8842 14.8885ZM23.6765 14.7843C23.7958 14.6789 23.9177 14.5664 24.0769 14.5326C24.2004 14.8151 24.1779 15.1324 24.2074 15.4343C24.2293 15.7008 24.1531 16.0186 24.3638 16.2248H24.5739C24.6015 16.1911 24.6564 16.1241 24.6834 16.091C24.6851 15.9122 24.6851 15.7334 24.6771 15.555C24.6257 15.542 24.523 15.5167 24.4717 15.5036C24.5201 15.2899 24.7059 15.1644 24.8906 15.0861C25.0164 15.3869 24.9568 15.7226 25.0175 16.0376C25.0792 16.1033 25.1415 16.169 25.2044 16.2347C25.2021 16.2638 25.1981 16.3207 25.1963 16.3491C25.0099 16.3847 24.8206 16.4006 24.6315 16.4029C24.5725 16.3574 24.5137 16.3135 24.4553 16.2685C24.314 16.5362 23.9702 16.4544 23.7341 16.4018C23.7843 16.259 23.9003 16.1329 23.8812 15.9718C23.8698 15.7155 23.8524 15.4591 23.8404 15.2027C23.8317 15.1412 23.8241 15.0802 23.8178 15.0191C23.6955 14.9974 23.6805 14.8897 23.6765 14.7843Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M27.3741 14.69C27.493 14.4579 27.9193 14.5669 27.857 14.8481C27.7786 14.9866 27.6193 14.9192 27.4951 14.9269C27.629 15.1128 27.8863 15.2833 27.7993 15.5503C27.6961 15.7989 27.3937 15.8191 27.1732 15.8937C27.1616 15.9192 27.1385 15.9701 27.127 15.9961C27.3653 16.1216 27.629 15.9251 27.8685 16.0446C27.9851 16.0861 28.0284 16.2146 28.0768 16.3193C27.9948 16.7291 27.5465 16.8884 27.1841 16.86C27.03 16.8321 26.8264 16.8096 26.7461 16.6527C26.674 16.5095 26.8061 16.3336 26.9556 16.3614C27.086 16.455 27.1985 16.5747 27.3428 16.648C27.4749 16.5966 27.7138 16.574 27.6705 16.3715C27.599 16.2696 27.4657 16.2826 27.3595 16.2916C27.1996 16.3046 27.0393 16.3046 26.8788 16.301C26.8373 16.2583 26.7962 16.2157 26.7554 16.1737C26.7547 16.1192 26.7547 16.0654 26.7547 16.0121C26.8182 15.952 26.8824 15.8928 26.9474 15.8345C26.8741 15.7487 26.7623 15.6824 26.7426 15.5621C26.7006 15.4282 26.775 15.304 26.824 15.1861C27 15.0878 27.1927 15.0328 27.3906 15.0049C27.3625 14.9037 27.2811 14.78 27.3741 14.69Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M25.8317 15.1798C25.9714 15.0898 26.156 14.969 26.317 15.0803C26.4912 15.2017 26.4671 15.4379 26.4842 15.6256C26.4768 15.7855 26.6447 15.8637 26.6557 16.0188C26.5108 16.1768 26.3665 16.444 26.115 16.3486C26.1606 16.0383 26.2425 15.7002 26.0994 15.4042C26.0111 15.4274 25.9246 15.4516 25.8368 15.4746C25.8138 15.5994 25.7705 15.7245 25.7877 15.8535C25.8027 16.0258 25.9591 16.124 26.0359 16.2667C25.8351 16.3674 25.6113 16.368 25.3936 16.3556L25.3031 16.2413C25.3584 16.1821 25.4144 16.124 25.4732 16.0683C25.4889 16.0199 25.5056 15.9725 25.5218 15.9244C25.4964 15.7965 25.4847 15.6668 25.4778 15.5371L25.3059 15.504C25.3071 15.4128 25.329 15.3257 25.3613 15.2424C25.46 15.1742 25.5581 15.1049 25.659 15.0392C25.7213 15.1114 25.764 15.196 25.8056 15.283C25.8121 15.2574 25.8254 15.2059 25.8317 15.1798Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M27.0555 15.4236C27.0994 15.2555 27.2898 15.2668 27.4224 15.2887C27.4654 15.3556 27.5 15.4279 27.5252 15.5037C27.4322 15.5611 27.333 15.605 27.2331 15.647C27.1634 15.5882 26.9995 15.5528 27.0555 15.4236Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M22.0141 15.3478C22.2675 15.2199 22.6136 15.307 22.7463 15.5721C22.8657 15.8393 22.8721 16.1957 22.6557 16.4129C22.4971 16.5929 22.2258 16.5728 22.017 16.5207C21.5774 16.323 21.5715 15.5467 22.0141 15.3478Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M20.8099 15.6469C20.8348 15.6432 20.8855 15.6368 20.9103 15.6333C20.964 15.7991 21.0148 15.9655 21.0776 16.1283C21.0389 16.1679 21.0002 16.2076 20.9628 16.2478C20.9807 16.1751 20.9974 16.1016 21.0129 16.0288C20.9512 15.9999 20.89 15.9714 20.829 15.9431C20.8221 15.844 20.8157 15.7451 20.8099 15.6469Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M22.085 15.6557C22.2085 15.5137 22.4659 15.6054 22.4877 15.7912C22.5062 15.9447 22.535 16.156 22.3723 16.2424C22.2586 16.2584 22.1058 16.2727 22.0527 16.1406C21.9828 15.9944 21.9691 15.7794 22.085 15.6557Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M18.4701 15.8777C18.5531 15.7826 18.6559 15.7215 18.7835 15.7475C18.7828 15.8553 18.7991 15.9832 18.6969 16.0506C18.6063 16.0732 18.5134 16.0749 18.4216 16.0834C18.4348 16.3095 18.432 16.5362 18.451 16.7619C18.4903 16.8122 18.5301 16.8632 18.5699 16.9146C18.4608 17.0663 18.3489 17.2658 18.1349 17.2486C18.173 17.0081 18.3017 16.7376 18.143 16.5145C18.0495 16.6642 17.8038 16.6423 17.7114 16.5033C17.5793 16.3487 17.6069 16.0764 17.7767 15.9609C17.9773 15.8114 18.241 15.9033 18.4701 15.8777Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M17.8405 16.3443C17.8647 16.2485 17.8491 16.1198 17.9594 16.0791C18.0338 16.0631 18.1094 16.0553 18.1863 16.0553C18.1914 16.1708 18.1989 16.2869 18.2092 16.4035C18.0933 16.3455 17.9645 16.3532 17.8405 16.3443Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M18.6968 16.0508C18.7389 16.0668 18.8232 16.0983 18.866 16.1137C18.8872 16.2368 18.8885 16.3629 18.8977 16.4885C19.0275 16.4208 19.1609 16.3966 19.2889 16.4831C19.3021 16.5974 19.3138 16.7115 19.3224 16.8265C19.3575 16.8774 19.3933 16.9289 19.4291 16.981C19.3647 17.0588 19.2977 17.1344 19.2284 17.2077L19.0628 17.1746C19.1234 17.0023 19.17 16.8015 19.0576 16.6387C19.0039 16.6583 18.9531 16.6848 18.9024 16.711C18.9037 16.7932 18.907 16.8767 18.9128 16.9596C18.9538 17.0283 18.9965 17.0964 19.0414 17.1632C18.9041 17.1828 18.7657 17.1952 18.6271 17.1982C18.6242 17.1756 18.6185 17.1302 18.6156 17.1069C18.6514 17.0508 18.6871 16.9957 18.7212 16.9395C18.6791 16.7577 18.6819 16.5701 18.6814 16.3848C18.4758 16.3356 18.6582 16.1651 18.6968 16.0508Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M19.6558 16.4107C19.8324 16.3748 20.0748 16.3906 20.1162 16.615C20.1855 16.8708 19.853 16.8388 19.6973 16.8459C19.7054 16.8718 19.7222 16.924 19.7308 16.9501C19.8641 16.9472 19.9869 16.8868 20.1185 16.8713C20.163 17.1946 19.6165 17.2438 19.5311 16.9566C19.4267 16.7636 19.5099 16.5498 19.6558 16.4107Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M19.6212 16.622C19.7297 16.5882 19.8392 16.5575 19.9507 16.532C19.9709 16.741 19.7181 16.7624 19.6212 16.622Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M20.8428 16.9567C20.9633 16.9751 21.0724 16.8164 21.186 16.9063C21.3407 17.0394 21.4348 17.252 21.4273 17.46C21.3764 17.5031 21.3181 17.5339 21.2611 17.5665C21.1156 17.4421 20.9696 17.3119 20.7885 17.248C20.718 17.2738 20.6476 17.3001 20.5774 17.3267C20.5682 17.4155 20.4718 17.5346 20.5889 17.5877C20.8889 17.766 21.2554 17.8057 21.5301 18.0373C21.6927 18.2604 21.7494 18.5387 21.7234 18.8134C21.5503 19.496 20.6185 19.6813 20.1383 19.2291C19.8383 18.9751 19.8354 18.3907 20.1966 18.1922C20.3859 18.0721 20.6623 18.0738 20.806 18.2662C20.7795 18.3308 20.7535 18.3959 20.7276 18.461C20.599 18.5061 20.4413 18.5131 20.3525 18.6315C20.3081 18.7985 20.3738 18.9976 20.5377 19.0663C20.7546 19.1644 21.0489 19.1419 21.2204 18.9626C21.4141 18.7358 21.356 18.3385 21.0934 18.1935C20.8355 18.0532 20.5349 18.0046 20.2914 17.8341C20.1004 17.6985 20.0675 17.4072 20.2055 17.2208C20.339 17.0028 20.6039 16.9235 20.8428 16.9567Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M26.8778 17.2537C27.0353 17.1095 27.2719 17.2537 27.4329 17.1164C27.4623 17.1164 27.5218 17.1158 27.5512 17.1158C27.6296 17.2426 27.6903 17.3805 27.7393 17.5232C27.572 17.6777 27.4018 17.5196 27.2541 17.4243C27.2276 17.428 27.1734 17.4338 27.1469 17.4373C27.1462 17.4835 27.1457 17.5303 27.1462 17.5776C27.3361 17.6665 27.5681 17.6889 27.7186 17.8502C27.8334 17.9756 27.7515 18.1462 27.7181 18.2894C27.5663 18.4233 27.3724 18.46 27.1779 18.4244C27.1081 18.4594 27.0343 18.528 26.9517 18.492C26.8144 18.3736 26.7256 18.1989 26.7313 18.013C26.9409 17.8999 27.1024 18.1095 27.2887 18.1745C27.4261 18.2313 27.3956 18.0568 27.4117 17.9809C27.216 17.9164 27.0176 17.8565 26.8341 17.7601L26.751 17.6228C26.7486 17.4888 26.7746 17.3455 26.8778 17.2537Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M24.5911 17.4535C24.751 17.3563 24.9477 17.251 25.1306 17.3563C25.4832 17.5599 25.5259 18.1089 25.2985 18.4262C25.186 18.6079 24.9199 18.6618 24.7521 18.531C24.6533 18.4789 24.6452 18.313 24.7336 18.2474C24.8161 18.235 24.9004 18.2297 24.9851 18.2309C25.1196 18.0982 25.1543 17.9135 25.0664 17.7423L24.9373 17.65C24.8137 17.6512 24.6569 17.6825 24.6285 17.83C24.6071 18.0278 24.6216 18.2279 24.6292 18.4267C24.6262 18.5411 24.74 18.5974 24.7913 18.6868C24.8179 18.7796 24.7307 18.843 24.6851 18.9099C24.5737 19.0196 24.4549 19.1805 24.28 19.1327C24.2096 18.9699 24.3088 18.8006 24.3042 18.6342C24.2938 18.367 24.2598 18.1018 24.2668 17.8348C24.2108 17.8069 24.1548 17.7797 24.1 17.7524C24.0947 17.7193 24.0843 17.653 24.0797 17.6198C24.1928 17.4944 24.3163 17.3533 24.494 17.3393C24.5184 17.3676 24.5668 17.425 24.5911 17.4535Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M25.7417 17.389C25.9783 17.1437 26.4485 17.1905 26.5968 17.5157C26.6552 17.6418 26.6915 17.8602 26.5368 17.9272C26.3453 18.0296 26.1266 17.9994 25.92 18.0011C25.9253 18.0313 25.9368 18.093 25.9426 18.1231C26.1705 18.296 26.3835 18.0509 26.6218 18.0592C26.6264 18.1332 26.6414 18.2085 26.6322 18.2836C26.5469 18.44 26.3611 18.4773 26.2099 18.5329C25.9231 18.537 25.634 18.3482 25.589 18.0444C25.5225 17.8175 25.5825 17.5606 25.7417 17.389Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M23.016 17.6097C23.1614 17.383 23.4637 17.3693 23.6939 17.4381C23.9801 17.5465 24.0361 17.8976 24.0274 18.1722C23.9772 18.3327 23.9125 18.5139 23.7533 18.5928C23.5992 18.6851 23.4169 18.6496 23.2478 18.656C22.8791 18.4695 22.7631 17.9401 23.016 17.6097Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M22.0129 17.5853C22.1197 17.4882 22.2644 17.4647 22.3965 17.4255C22.5183 17.4693 22.6579 17.4942 22.7514 17.5931C22.8213 17.6896 22.8028 17.8393 22.693 17.8979C22.5356 17.8328 22.3797 17.7459 22.2043 17.7736C22.1057 17.9529 22.0617 18.22 22.2453 18.3596C22.4414 18.4585 22.6261 18.2554 22.8286 18.2874C22.8326 18.3928 22.8811 18.5367 22.768 18.6024C22.6145 18.7528 22.3854 18.7615 22.1893 18.7202C21.755 18.5588 21.6644 17.8802 22.0129 17.5853Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M25.9627 17.5504C26.1099 17.5161 26.3257 17.4794 26.3366 17.6972C26.1814 17.7429 26.0209 17.7652 25.8599 17.7736C25.8929 17.6984 25.9274 17.6244 25.9627 17.5504Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M23.3078 17.7199C23.4059 17.6738 23.5143 17.7069 23.6165 17.7206C23.6922 17.8698 23.7522 18.0426 23.6858 18.209C23.6258 18.4145 23.298 18.4192 23.2299 18.2143C23.2062 18.0497 23.1717 17.8442 23.3078 17.7199Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M33.5955 19.4416C34.0034 19.3457 34.2994 19.0183 34.6145 18.7684C35.1857 18.8294 35.7288 19.1064 36.1285 19.528C36.4783 19.9283 36.7194 20.4529 36.6969 20.9993C36.7298 21.7254 36.356 22.4234 35.8118 22.8692C34.8921 23.6442 33.4379 23.6602 32.5303 22.8556C31.993 22.3955 31.6877 21.6554 31.7756 20.9397C31.82 20.5045 31.9988 20.093 32.2568 19.7472C32.7052 19.6565 33.1489 19.5415 33.5955 19.4416Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M7.17773 19.9426C7.34565 20.1148 7.5101 20.2907 7.6809 20.46C7.80496 20.5689 7.82458 20.7448 7.88164 20.8934C8.70454 23.2807 10.1251 25.4465 11.9722 27.1269C13.6179 28.6308 15.5953 29.7545 17.7188 30.3655C20.1383 31.0684 22.736 31.1158 25.1768 30.4928C28.6366 29.6337 31.744 27.4075 33.7491 24.3937C33.9186 24.3735 34.089 24.3545 34.2602 24.3469C32.2204 27.5602 28.9765 29.9522 25.3447 30.8707C24.4717 31.1044 23.5761 31.2437 22.6772 31.3046C21.4458 31.3887 20.2052 31.3134 18.9911 31.0879C16.6 30.6455 14.323 29.5834 12.4148 28.0423C9.88624 26.0086 8.0086 23.1303 7.17773 19.9426Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M30.1039 20.1579C30.1773 20.1492 30.2534 20.1212 30.3285 20.1401C30.7769 20.5143 31.097 21.0544 31.1686 21.6476C31.347 22.807 30.5362 23.9669 29.4625 24.2954C28.7376 24.537 27.9051 24.4375 27.272 23.9947C26.7758 23.6637 26.4082 23.1315 26.2899 22.534C26.1519 21.935 26.2882 21.2837 26.6142 20.7719C26.654 20.6838 26.7578 20.695 26.8358 20.6796C27.9292 20.5333 29.0175 20.3497 30.1039 20.1579ZM14.9981 22.0645C15.2347 21.3095 15.8134 20.6512 16.5578 20.4072C16.6599 20.7322 16.9098 20.9934 17.2289 21.0958C17.9305 21.3404 18.6846 21.2422 19.4123 21.2641C19.77 21.8057 19.9565 22.4879 19.8221 23.1375C19.6328 24.2897 18.5699 25.1967 17.4349 25.1956C16.6092 25.237 15.7812 24.8125 15.3172 24.1103C14.9184 23.5209 14.8101 22.753 14.9981 22.0645ZM21.2484 21.2136C22.4608 21.1574 23.6731 21.0679 24.8785 20.9175C25.3054 21.3623 25.5795 21.9733 25.5547 22.6044C25.5484 23.3469 25.1502 24.0545 24.5628 24.4773C24.0101 24.8823 23.2894 25.0421 22.624 24.8917C21.7614 24.717 21.0268 24.0131 20.8232 23.1308C20.7695 22.7359 20.7643 22.3262 20.8803 21.9419C20.9669 21.6814 21.1042 21.4439 21.2484 21.2136Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M13.7298 21.71C13.8193 21.664 13.9098 21.6196 14.0009 21.5745C13.7962 22.0838 13.7384 22.6386 13.7656 23.1844C13.4123 22.7645 13.0824 22.3245 12.7777 21.8664C13.1026 21.88 13.4263 21.8332 13.7298 21.71Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M11.135 23.6662C11.6029 23.3672 12.0425 23.022 12.514 22.7277C12.6254 22.8953 12.735 23.064 12.836 23.2387C12.4107 23.5229 11.9947 23.8201 11.5694 24.1032C11.3687 24.2287 11.1464 24.3595 10.9018 24.3289C10.4673 24.3141 10.12 23.8546 10.1869 23.4199C10.2348 23.1529 10.42 22.9184 10.6612 22.8076C10.7546 22.9598 10.8626 23.1019 10.9527 23.2565C10.8684 23.3837 10.6918 23.5004 10.7691 23.677C10.8621 23.8119 11.0254 23.7302 11.135 23.6662Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M12.4408 24.1701C12.6243 23.9941 12.8331 23.7828 13.1073 23.8149C13.5244 23.8728 13.8648 24.25 13.9306 24.671C13.952 24.9286 13.7979 25.1558 13.6104 25.3104C13.1955 25.6882 12.8118 26.102 12.3825 26.4633C12.2752 26.3508 12.1667 26.24 12.06 26.1275C12.2573 25.8575 12.5238 25.6486 12.7673 25.4241C12.6473 25.2725 12.5198 25.1281 12.39 24.9864C12.1303 25.189 11.91 25.4383 11.6618 25.6543C11.5493 25.5513 11.4431 25.4418 11.3376 25.3317C11.3352 25.3104 11.3307 25.2665 11.3278 25.2446C11.69 24.877 12.0789 24.5376 12.4408 24.1701Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M22.4469 25.8403C23.7658 26.0143 25.1497 25.2678 25.7394 24.0405C26.1174 24.565 26.6626 24.9545 27.2685 25.15C27.2668 25.1624 27.2622 25.1879 27.2599 25.2004C25.2224 26.4887 22.7585 27.0524 20.3829 26.7403C19.4833 26.6243 18.5942 26.3959 17.7557 26.0393C18.7298 25.8398 19.5855 25.1559 20.0373 24.2512C20.5364 25.1323 21.4596 25.738 22.4469 25.8403Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M12.982 24.3939C13.2018 24.3791 13.4551 24.59 13.3668 24.8309C13.2896 24.9251 13.1944 25.0021 13.1037 25.0825C12.9692 24.9493 12.8389 24.8121 12.7159 24.6674C12.8042 24.5758 12.8929 24.4846 12.982 24.3939Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M14.4874 24.9067C14.6235 24.9991 14.7529 25.1021 14.8844 25.2022C14.7113 25.6172 14.5475 26.037 14.3599 26.4455C14.3536 26.4839 14.3397 26.5603 14.3327 26.5983C14.7009 26.2685 15.0869 25.9611 15.4643 25.6425C15.5889 25.7485 15.7176 25.8492 15.8492 25.9463C15.1937 26.5143 14.5185 27.0583 13.8578 27.6207C13.7332 27.5237 13.6074 27.4289 13.4805 27.3353C13.8105 26.5231 14.1562 25.7185 14.4874 24.9067Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M26.5316 26.6421C26.992 26.3779 27.4393 26.0906 27.8889 25.8084C27.9817 25.9564 28.0706 26.1074 28.1502 26.2642C28.0042 26.3553 27.8513 26.4347 27.7122 26.5367C27.725 26.6142 27.7659 26.6817 27.8046 26.7492C28.0966 27.2472 28.3977 27.7403 28.68 28.2454C28.6931 28.2549 28.7204 28.2738 28.7336 28.2834C28.5869 28.371 28.443 28.4636 28.3021 28.561C27.9161 28.0045 27.6166 27.3899 27.2503 26.8198C27.0939 26.9132 26.941 27.0128 26.7794 27.0969C26.6966 26.9446 26.613 26.7935 26.5316 26.6421Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M16.196 26.3786C16.6623 26.2376 17.2341 26.5407 17.3501 27.0359C17.3969 27.2975 17.2423 27.5272 17.1354 27.7505C16.9167 28.2056 16.6842 28.6527 16.4592 29.1046C16.308 29.0365 16.162 28.9577 16.0177 28.8765C16.1627 28.5746 16.3131 28.2761 16.4598 27.9753C16.289 27.8866 16.1251 27.7793 15.9417 27.7196C15.806 28.012 15.6467 28.2928 15.5157 28.5881C15.3582 28.5432 15.2128 28.4657 15.0663 28.3945C15.0772 28.2584 15.1361 28.1352 15.1984 28.0174C15.3981 27.6106 15.6099 27.2104 15.8043 26.8007C15.8833 26.6196 16.0022 26.4408 16.196 26.3786Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M16.3252 26.9529C16.3582 26.9398 16.4228 26.9144 16.4551 26.9015C16.6091 26.9346 16.8353 27.0051 16.8324 27.2039C16.795 27.33 16.7257 27.443 16.6657 27.5591C16.4949 27.4732 16.3241 27.3875 16.1572 27.2934C16.2109 27.1786 16.2669 27.0651 16.3252 26.9529Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M24.7635 27.2863C25.1709 27.1708 25.5749 26.8392 26.0117 27.0056C26.4317 27.1294 26.6706 27.6724 26.4703 28.0744C26.3435 28.3367 26.0901 28.4953 25.8294 28.5889C25.9379 28.885 26.0567 29.1773 26.1537 29.4782C26.0129 29.5416 25.8697 29.5979 25.7307 29.6635C25.696 29.6648 25.6614 29.6659 25.6274 29.6677C25.3966 29.0644 25.1767 28.4568 24.9488 27.8523C24.879 27.6664 24.7913 27.4853 24.7635 27.2863Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M18.1094 27.1447C18.3835 27.0612 18.6881 27.1211 18.9254 27.2808C19.1804 27.4852 19.2299 27.8605 19.1377 28.1631C18.9872 28.1234 18.8378 28.0802 18.6911 28.0293C18.6755 27.9024 18.7021 27.7238 18.566 27.6568C18.4834 27.5953 18.379 27.6142 18.2849 27.6077C18.2561 27.6793 18.2278 27.7521 18.2 27.825C18.4591 28.0613 18.8105 28.2229 18.9427 28.5775C19.0892 29.0341 18.7707 29.5687 18.312 29.6587C18.2444 29.657 18.1781 29.6563 18.1117 29.6563C17.8747 29.5955 17.6091 29.5095 17.481 29.2762C17.3442 29.0502 17.3847 28.7712 17.421 28.522C17.5901 28.5593 17.7597 28.5983 17.9223 28.6612C17.9339 28.8287 17.9148 29.0104 18.0378 29.1431C18.0977 29.1431 18.2172 29.1438 18.2766 29.1438C18.3547 29.0324 18.451 28.907 18.4325 28.7619C18.2496 28.5073 17.9207 28.4067 17.7643 28.1271C17.6229 27.7793 17.7615 27.3033 18.1094 27.1447Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M23.8214 27.4479C24.0043 27.4083 24.1872 27.3675 24.3724 27.3408C24.5403 28.1733 24.6823 29.011 24.8543 29.8429C24.7038 29.8796 24.5526 29.9128 24.3996 29.9377C24.3811 29.9496 24.3436 29.9733 24.3251 29.9858C24.2501 29.7772 24.2225 29.5565 24.1786 29.3408C24.0597 28.7097 23.938 28.0797 23.8214 27.4479Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M25.4618 27.619C25.6113 27.5557 25.77 27.5099 25.9332 27.5456C25.9972 27.6634 26.0458 27.8025 25.9874 27.9345C25.8899 28.0322 25.7543 28.0708 25.6262 28.1057C25.5702 27.9434 25.5103 27.783 25.4618 27.619Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n      <path\n        d=\"M19.8134 27.8985C20.0812 27.4693 20.7597 27.4101 21.1008 27.7813C21.2047 27.8843 21.2526 28.0276 21.3113 28.1597C21.3108 28.2941 21.3102 28.4292 21.3079 28.5647C21.1364 28.5711 20.9652 28.5546 20.7948 28.5369C20.7931 28.4463 20.7909 28.3556 20.7914 28.2663C20.6788 28.1408 20.5057 28.0679 20.3464 28.1431C20.205 28.2462 20.2299 28.4409 20.1976 28.5949C20.1728 28.8466 20.1127 29.0947 20.1151 29.3498C20.1364 29.3824 20.1802 29.4475 20.2016 29.4801C20.3331 29.5594 20.4797 29.5416 20.613 29.4759C20.65 29.386 20.6868 29.2966 20.7231 29.2071C20.8924 29.2261 21.0608 29.2534 21.2304 29.2724C21.2298 29.5323 21.145 29.8264 20.9067 29.9574C20.5356 30.1634 20.0015 30.1314 19.7336 29.7649C19.5179 29.5217 19.5698 29.1704 19.624 28.875C19.6928 28.551 19.6894 28.207 19.8134 27.8985Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M21.7106 27.6563C22.0343 27.6095 22.3614 27.5858 22.6886 27.5858C22.8554 27.5799 23.0059 27.6615 23.1497 27.7398C23.3856 28.0365 23.4289 28.4734 23.2506 28.8109C23.1929 28.9287 23.0879 29.0127 23.0273 29.1282C23.1641 29.4575 23.3729 29.7506 23.5322 30.0702C23.347 30.0731 23.1629 30.091 22.9789 30.1034C22.7722 29.7855 22.6078 29.4397 22.3787 29.1383C22.3816 29.477 22.4388 29.812 22.4457 30.1513C22.2709 30.1667 22.0972 30.1915 21.9217 30.1975C21.8998 30.0162 21.8941 29.8333 21.8733 29.6521C21.8386 29.2175 21.7999 28.7829 21.7618 28.3483C21.7361 28.118 21.7256 27.8871 21.7106 27.6563Z\"\n        fill=\"#BFBFBF\"\n      />\n      <path\n        d=\"M22.298 28.1187C22.4769 28.0632 22.6603 28.0956 22.8144 28.2016C22.8351 28.2725 22.857 28.3449 22.8791 28.4165C22.8351 28.5012 22.801 28.6029 22.7134 28.6516C22.5957 28.6871 22.4745 28.7072 22.3533 28.7273C22.3136 28.5266 22.2997 28.3229 22.298 28.1187Z\"\n        fill=\"black\"\n        fillOpacity=\"0.85\"\n      />\n    </g>\n    <defs>\n      <clipPath id=\"clip0_129393_199300\">\n        <rect width=\"37.3333\" height=\"32\" fill=\"white\" transform=\"translate(0.0476074)\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nexport const RSLogoIcon = (props: Partial<CustomIconComponentProps>) => {\n  return <Icon component={svg} {...props} />;\n};\n"
  },
  {
    "path": "client/src/shared/components/Icons/ScoreIcon.tsx",
    "content": "import StarTwoTone from '@ant-design/icons/StarTwoTone';\n\nenum Score {\n  Outdated = 'gray',\n  Min = '#ff0000',\n  Average = '#ffa940',\n  Max = '#52c41a',\n  Undefined = '#13c2c2',\n}\n\ntype Props = {\n  score: number;\n  maxScore: number | undefined;\n  isOutdatedScore?: boolean;\n};\n\nexport function ScoreIcon(props: Props) {\n  const color = getColor(props);\n  return <StarTwoTone twoToneColor={color} />;\n}\n\nfunction getColor({ maxScore, score, isOutdatedScore = false }: Props) {\n  if (typeof maxScore === 'undefined') return Score.Undefined;\n  if (isOutdatedScore) return Score.Outdated;\n  if (score <= 0) return Score.Min;\n  if (score === maxScore) return Score.Max;\n  return Score.Average;\n}\n"
  },
  {
    "path": "client/src/shared/components/Icons/TelegramIcon.tsx",
    "content": "import Icon from '@ant-design/icons/lib/components/Icon';\nimport type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';\n\nconst svg = () => (\n  <svg width=\"25\" height=\"25\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n    <g clipPath=\"url(#a)\">\n      <path\n        d=\"M12.445 24.002c6.628 0 12-5.373 12-12s-5.372-12-12-12c-6.627 0-12 5.373-12 12s5.373 12 12 12Z\"\n        fill=\"url(#b)\"\n      />\n      <path\n        d=\"m8.568 12.88 1.424 3.94s.178.369.369.369c.19 0 3.025-2.95 3.025-2.95L16.54 8.15l-7.92 3.712-.05 1.017Z\"\n        fill=\"#C8DAEA\"\n      />\n      <path d=\"m10.456 13.89-.274 2.904s-.114.89.776 0 1.741-1.576 1.741-1.576\" fill=\"#A9C6D8\" />\n      <path\n        d=\"m8.594 13.02-2.929-.954s-.35-.143-.237-.465c.023-.066.07-.122.21-.22.649-.452 12.01-4.535 12.01-4.535s.322-.109.51-.037a.277.277 0 0 1 .19.206c.02.084.028.171.025.258-.001.076-.01.145-.017.255-.07 1.116-2.14 9.449-2.14 9.449s-.124.487-.568.504a.81.81 0 0 1-.593-.23c-.87-.748-3.881-2.772-4.547-3.217a.126.126 0 0 1-.054-.09c-.01-.047.041-.105.041-.105s5.243-4.66 5.382-5.149c.011-.038-.03-.057-.084-.04-.349.128-6.385 3.94-7.051 4.36a.32.32 0 0 1-.148.01Z\"\n        fill=\"#fff\"\n      />\n    </g>\n    <defs>\n      <linearGradient id=\"b\" x1=\"12.445\" y1=\"24.002\" x2=\"12.445\" y2=\".002\" gradientUnits=\"userSpaceOnUse\">\n        <stop stopColor=\"#1D93D2\" />\n        <stop offset=\"1\" stopColor=\"#38B0E3\" />\n      </linearGradient>\n      <clipPath id=\"a\">\n        <path fill=\"#fff\" transform=\"translate(.445 .002)\" d=\"M0 0h24v24H0z\" />\n      </clipPath>\n    </defs>\n  </svg>\n);\n\nexport const TelegramIcon = (props: Partial<CustomIconComponentProps>) => {\n  return <Icon component={svg} {...props} />;\n};\n"
  },
  {
    "path": "client/src/shared/components/Icons/index.tsx",
    "content": "export * from './CourseIcon';\nexport * from './DeadlineIcon';\nexport * from './DiscordFilled';\nexport * from './DiscordOutlined';\nexport * from './HealthMask';\nexport * from './LinkedInIcon';\nexport * from './ScoreIcon';\nexport * from './TelegramIcon';\nexport * from './GitHubLogoIcon';\nexport * from './RSLogoIcon';\nexport { PublicSvgIcon } from './PublicSvgIcon';\n"
  },
  {
    "path": "client/src/shared/components/LoadingScreen.module.css",
    "content": ".loadingScreen {\n  inset: 0;\n  z-index: 1000;\n}\n"
  },
  {
    "path": "client/src/shared/components/LoadingScreen.tsx",
    "content": "import * as React from 'react';\nimport { Spin, theme } from 'antd';\nimport styles from './LoadingScreen.module.css';\n\ntype Props = React.PropsWithChildren<{ show: boolean }>;\n\nexport const LoadingScreen = (props: Props) => {\n  const { token } = theme.useToken();\n  if (!props.show) {\n    return <>{props.children}</>;\n  }\n  return (\n    <div\n      data-testid=\"loading-screen\"\n      className={styles.loadingScreen}\n      style={{\n        justifyContent: 'center',\n        alignItems: 'center',\n        display: 'flex',\n        width: '100vw',\n        position: 'fixed',\n        height: '100vh',\n        background: token.colorBgContainer,\n      }}\n    >\n      <Spin tip=\"Loading...\" size=\"default\">\n        <div style={{ padding: '50px' }} />\n      </Spin>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/shared/components/MentorSearch.tsx",
    "content": "import { CourseMentorsApi } from '@client/api';\nimport { UserSearch, UserProps } from '@client/shared/components/UserSearch';\nimport { useCallback } from 'react';\n\ntype Props = UserProps & {\n  courseId: number;\n};\n\nconst courseMentorsApi = new CourseMentorsApi();\n\nexport function MentorSearch(props: Props) {\n  const { courseId, ...otherProps } = props;\n  const handleSearch = useCallback(\n    async (value: string) => {\n      const { data } = await courseMentorsApi.searchMentors(courseId, value);\n      return data;\n    },\n    [courseId],\n  );\n\n  return <UserSearch {...otherProps} searchFn={handleSearch} />;\n}\n"
  },
  {
    "path": "client/src/shared/components/NonTouchTooltip.tsx",
    "content": "import { PropsWithChildren, useEffect, useState } from 'react';\nimport { Tooltip, TooltipProps } from 'antd';\n\ntype NonTouchTooltipProps = PropsWithChildren<{\n  title: string;\n  placement?: TooltipProps['placement'];\n}>;\n\nexport default function NonTouchTooltip({ title, placement, children }: NonTouchTooltipProps) {\n  const [isTouchDevice, setIsTouchDevice] = useState(false);\n\n  useEffect(() => {\n    const checkTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;\n    setIsTouchDevice(checkTouchDevice);\n  }, []);\n\n  if (isTouchDevice) {\n    return children;\n  }\n\n  return (\n    <Tooltip title={title} placement={placement || 'left'}>\n      {children}\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/PageLayout.tsx",
    "content": "import { PropsWithChildren, ReactNode } from 'react';\nimport { Header } from './Header';\nimport { Button, Col, Layout, Result, Row, Spin, theme } from 'antd';\nimport { AdminSider } from './Sider/AdminSider';\nimport { Course } from '@client/services/models';\n\ntype Props = {\n  loading: boolean;\n  error?: Error;\n  showCourseName?: boolean;\n  title?: string;\n  children?: ReactNode;\n  noData?: boolean;\n  background?: string;\n  withMargin?: boolean;\n};\n\nexport function PageLayout(props: Props) {\n  if (process.env.NODE_ENV !== 'production' && props.error) console.error(props.error);\n  const withMargin = props.withMargin ?? true;\n  const { token } = theme.useToken();\n\n  return (\n    <Layout\n      style={{\n        minHeight: '100vh',\n        background: props.background ? props.background : token.colorBgContainer,\n      }}\n    >\n      <Header title={props.title} showCourseName={props.showCourseName} />\n      {props.error ? (\n        <Result\n          status=\"500\"\n          title=\"500\"\n          subTitle=\"Sorry, something went wrong.\"\n          extra={\n            <Button type=\"primary\" href=\"/\">\n              Back Home\n            </Button>\n          }\n        />\n      ) : (\n        <Layout.Content style={withMargin ? { margin: 16 } : undefined}>\n          <Spin spinning={props.loading}>{props.children}</Spin>\n        </Layout.Content>\n      )}\n    </Layout>\n  );\n}\n\nexport function PageLayoutSimple(props: Props) {\n  const { token } = theme.useToken();\n  return (\n    <Layout\n      style={{\n        background: props.background ? props.background : token.colorBgContainer,\n      }}\n    >\n      <Header title={props.title} showCourseName={props.showCourseName} />\n      <Layout.Content>\n        {props.noData ? (\n          <div>no data</div>\n        ) : (\n          <Spin spinning={props.loading}>\n            <Row style={{ marginTop: 16 }}></Row>\n            <Row>\n              <Col flex={1} />\n              <Col xs={20} sm={16} md={16} lg={12} xl={12}>\n                {props.children}\n              </Col>\n              <Col flex={1} />\n            </Row>\n          </Spin>\n        )}\n      </Layout.Content>\n    </Layout>\n  );\n}\n\nexport function AdminPageLayout({\n  title,\n  loading,\n  children,\n  showCourseName,\n  courses,\n  styles,\n}: PropsWithChildren<{\n  title?: string;\n  showCourseName?: boolean;\n  loading: boolean;\n  courses: Course[];\n  background?: string;\n  styles?: React.CSSProperties;\n}>) {\n  return (\n    <Layout style={{ minHeight: '100vh' }}>\n      <Header title={title} showCourseName={showCourseName} />\n      <Layout>\n        <AdminSider courses={courses} />\n        <Layout.Content\n          style={{\n            margin: 16,\n            padding: 16,\n            ...styles,\n          }}\n        >\n          <Spin spinning={loading}>{children}</Spin>\n        </Layout.Content>\n      </Layout>\n    </Layout>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/PersonSelect.tsx",
    "content": "import * as React from 'react';\nimport { Select } from 'antd';\nimport { GithubAvatar } from './GithubAvatar';\nimport get from 'lodash/get';\nimport { SelectProps } from 'antd/lib/select';\n\ntype Props = SelectProps<string> & {\n  data: { id: number; githubId: string; name?: string }[];\n  keyField?: 'id' | 'githubId';\n  defaultValue?: string | number;\n};\n\nexport class PersonSelect extends React.PureComponent<Props> {\n  render() {\n    const { data, keyField, defaultValue, ...other } = this.props;\n    return (\n      <Select showSearch optionFilterProp=\"children\" defaultValue={defaultValue} placeholder=\"Select...\" {...other}>\n        {data.map(person => {\n          const id = keyField ? get(person, keyField) : person.id;\n          return (\n            <Select.Option key={id} value={id}>\n              <GithubAvatar size={24} githubId={person.githubId} /> {person.name} ({person.githubId})\n            </Select.Option>\n          );\n        })}\n      </Select>\n    );\n  }\n}\n\nexport { GithubAvatar } from './GithubAvatar';\n"
  },
  {
    "path": "client/src/shared/components/Rating.tsx",
    "content": "import { Rate, Space, Typography } from 'antd';\n\ntype Props = { rating: number; tooltips?: string[] };\n\nexport function Rating(props: Props) {\n  const { rating, tooltips } = props;\n\n  return (\n    <Space align=\"center\">\n      <Rate\n        tooltips={tooltips}\n        allowHalf={true}\n        value={roundHalf(rating)}\n        disabled={true}\n        style={{ display: 'flex', flexWrap: 'nowrap', fontSize: 'clamp(0.9em, 1.5vw, 1.5em)', fontWeight: 'bold' }}\n      />\n      {tooltips ? (\n        <Typography.Text>{tooltips[Math.round(rating) - 1]}</Typography.Text>\n      ) : (\n        <Typography.Text>{rating.toFixed(2)}</Typography.Text>\n      )}\n    </Space>\n  );\n}\n\nfunction roundHalf(num: number) {\n  return Math.round(num * 2) / 2;\n}\n"
  },
  {
    "path": "client/src/shared/components/ScoreCard.module.css",
    "content": ".card {\n  position: relative;\n  transform-style: preserve-3d;\n  width: 56px;\n  height: 40px;\n  background: var(--card-bg);\n  border: 2px solid var(--card-border);\n  border-radius: 12px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  font-size: 24px;\n  font-weight: bold;\n  cursor: pointer;\n  transform: rotateX(40deg);\n  transform-origin: bottom;\n  transition:\n    transform 0.5s ease,\n    box-shadow 0.5s ease,\n    background 0.5s ease,\n    border-color 0.5s ease;\n}\n\n.card:hover,\n.card.selected {\n  transform: rotateX(0deg) translateY(-5px);\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);\n}\n\n.selectedRed {\n  border-color: var(--card-error);\n  background: var(--card-error-bg);\n}\n\n.selectedYellow {\n  border-color: var(--card-warning);\n  background: var(--card-warning-bg);\n}\n\n.selectedGreen {\n  border-color: var(--card-success);\n  background: var(--card-success-bg);\n}\n\n.cardStick {\n  position: absolute;\n  bottom: -20px;\n  left: 50%;\n  transform: translateX(-50%);\n  width: 10px;\n  height: 20px;\n  background: #999;\n  border-radius: 0 0 3px 3px;\n  pointer-events: none;\n}\n\n.selectedRed .cardStick {\n  background: var(--card-error);\n}\n\n.selectedYellow .cardStick {\n  background: var(--card-warning);\n}\n\n.selectedGreen .cardStick {\n  background: var(--card-success);\n}\n"
  },
  {
    "path": "client/src/shared/components/ScoreCard.tsx",
    "content": "import { theme } from 'antd';\nimport type { CSSProperties } from 'react';\nimport styles from './ScoreCard.module.css';\n\ntype Props = {\n  value: number;\n  selected: boolean;\n  onSelect: (value: number) => void;\n};\n\nexport const ScoreCard: React.FC<Props> = ({ value, selected, onSelect }) => {\n  const getSelectedClass = () => {\n    if (!selected) return '';\n    if (value <= 4) return styles.selectedRed;\n    if (value <= 7) return styles.selectedYellow;\n    return styles.selectedGreen;\n  };\n  const { token } = theme.useToken();\n  const styleVars = {\n    '--card-bg': token.colorBgContainerDisabled,\n    '--card-border': token.colorBorder,\n    '--card-error': token.colorError,\n    '--card-error-bg': token.colorErrorBg,\n    '--card-warning': token.colorWarning,\n    '--card-warning-bg': token.colorWarningBg,\n    '--card-success': token.colorSuccess,\n    '--card-success-bg': token.colorSuccessBg,\n  } as CSSProperties;\n\n  return (\n    <div\n      className={`${styles.card} ${selected ? styles.selected : ''} ${getSelectedClass()}`}\n      onClick={() => onSelect(value)}\n      style={styleVars}\n    >\n      {value}\n      <div className={styles.cardStick} />\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/shared/components/ScoreSelector.module.css",
    "content": ".container {\n  display: flex;\n  justify-content: left;\n  gap: 12px;\n  padding: 10px 0 20px;\n  margin-bottom: 30px;\n  position: relative;\n}\n\n.scoreSloth {\n  position: absolute;\n  right: 0;\n  bottom: -100px;\n  width: 100px;\n  height: 100px;\n  background-image: url('https://cdn.rs.school/sloths/cleaned/its-a-good-job.svg');\n  background-size: contain;\n  background-repeat: no-repeat;\n  background-position: center;\n}\n\n.slothScoreCard {\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  height: 12px;\n  width: 24px;\n  text-align: center;\n  right: -3px;\n  top: 23px;\n  font-size: 12px;\n  font-weight: 900;\n}\n"
  },
  {
    "path": "client/src/shared/components/ScoreSelector.tsx",
    "content": "import { ScoreCard } from './ScoreCard';\nimport { theme } from 'antd';\nimport styles from './ScoreSelector.module.css';\n\ntype ScoreSelectorProps = {\n  value?: number;\n  onChange?: (value: number) => void;\n};\n\nexport const ScoreSelector: React.FC<ScoreSelectorProps> = ({ value, onChange }) => {\n  const { token } = theme.useToken();\n  return (\n    <div className={styles.container}>\n      {Array.from({ length: 10 }, (_, i) => i + 1).map(num => (\n        <ScoreCard key={num} value={num} selected={value === num} onSelect={() => onChange?.(num)} />\n      ))}\n      <div className={styles.scoreSloth}>\n        {value && (\n          <span className={styles.slothScoreCard} style={{ background: token.colorBgContainer }}>\n            {value}\n          </span>\n        )}\n      </div>\n    </div>\n  );\n};\n"
  },
  {
    "path": "client/src/shared/components/Sider/AdminSider.test.tsx",
    "content": "import { render, screen, fireEvent } from '@testing-library/react';\nimport { AdminSider } from './AdminSider';\nimport { SessionContext } from '@client/modules/Course/contexts';\nimport { Course } from '@client/services/models';\nimport { useLocalStorage } from 'react-use';\nimport { Session } from '@client/components/withSession';\nimport { getAdminMenuItems, getCourseManagementMenuItems } from './data/menuItems';\nimport router from 'next/router';\n\nvi.mock('next/router', () => ({\n  __esModule: true,\n  default: {\n    push: vi.fn(),\n  },\n}));\nvi.mock('react-use');\nvi.mock('./data/menuItems');\n\ndescribe('AdminSider', () => {\n  const mockCourses: Course[] = [\n    {\n      id: 1,\n      name: 'Test Course',\n      alias: 'test-course',\n      description: 'Test Description',\n      startDate: '2024-01-01',\n      endDate: '2024-12-31',\n      registrationEndDate: '2023-12-30',\n      completed: false,\n      planned: false,\n      inviteOnly: false,\n      createdDate: '2024-01-01',\n      updatedDate: '2024-01-01',\n      fullName: 'Test Course Full Name',\n      descriptionUrl: 'https://test.com',\n      year: 2024,\n      primarySkillId: 'skill1',\n      primarySkillName: 'Test Skill',\n      locationName: 'Test Location',\n      discordServerId: 123,\n      certificateIssuer: 'Test Issuer',\n      usePrivateRepositories: false,\n      personalMentoring: false,\n      personalMentoringStartDate: null,\n      personalMentoringEndDate: null,\n      logo: 'test-logo.png',\n      discipline: null,\n      minStudentsPerMentor: 5,\n      certificateThreshold: 60,\n      wearecommunityUrl: null,\n      certificateDisciplines: null,\n    },\n  ];\n\n  const mockSession: Session = {\n    id: 1,\n    githubId: 'test-user',\n    isAdmin: true,\n    isHirer: false,\n    courses: {},\n  };\n\n  const mockAdminMenuItems = [\n    { key: 'admin1', name: 'Admin Item 1', icon: <div />, href: '/admin1' },\n    { key: 'admin2', name: 'Admin Item 2', icon: <div />, href: '/admin2' },\n  ];\n\n  const mockCourseMenuItems = [\n    { key: 'course1', name: 'Course Item 1', icon: <div />, href: '/course1' },\n    { key: 'course2', name: 'Course Item 2', icon: <div />, href: '/course2' },\n  ];\n\n  beforeEach(() => {\n    vi.mocked(useLocalStorage).mockImplementation(key => {\n      if (key === 'isSiderCollapsed') return [false, vi.fn()];\n      if (key === 'openedSidebarItems') return [[], vi.fn()];\n      return [undefined, vi.fn()];\n    });\n\n    vi.mocked(getAdminMenuItems).mockReturnValue(mockAdminMenuItems);\n    vi.mocked(getCourseManagementMenuItems).mockReturnValue(mockCourseMenuItems);\n    vi.mocked(router.push).mockClear();\n  });\n\n  const renderComponent = (props = {}) => {\n    return render(\n      <SessionContext.Provider value={mockSession}>\n        <AdminSider courses={mockCourses} {...props} />\n      </SessionContext.Provider>,\n    );\n  };\n\n  it('renders correctly with default props', () => {\n    renderComponent();\n\n    expect(screen.getByTestId('admin-sider')).toBeInTheDocument();\n\n    expect(screen.getByText('Admin Area')).toBeInTheDocument();\n    expect(screen.getByText('Course Management')).toBeInTheDocument();\n  });\n\n  it('handles sidebar collapse toggle', () => {\n    const setIsSiderCollapsed = vi.fn();\n\n    vi.mocked(useLocalStorage).mockImplementation(key => {\n      if (key === 'isSiderCollapsed') return [false, setIsSiderCollapsed];\n      return [undefined, vi.fn()];\n    });\n\n    renderComponent();\n\n    const collapseButton = screen.getByRole('img', { name: 'menu-fold' });\n    fireEvent.click(collapseButton);\n\n    expect(setIsSiderCollapsed).toHaveBeenCalledWith(true);\n  });\n\n  it('navigates to correct route when menu item is clicked', () => {\n    renderComponent();\n\n    const adminArea = screen.getByText('Admin Area');\n    fireEvent.click(adminArea);\n\n    const adminItem = screen.getByText('Admin Item 1');\n    fireEvent.click(adminItem);\n\n    expect(router.push).toHaveBeenCalledWith('/admin1');\n  });\n\n  it('handles course management menu items correctly', () => {\n    renderComponent();\n\n    const courseManagement = screen.getByText('Course Management');\n    fireEvent.click(courseManagement);\n\n    const courseItem = screen.getByText('Course Item 1');\n    fireEvent.click(courseItem);\n\n    expect(router.push).toHaveBeenCalledWith('/course1');\n  });\n\n  it('renders correctly when no courses are provided', () => {\n    renderComponent({ courses: [] });\n\n    expect(screen.getByText('Admin Area')).toBeInTheDocument();\n    expect(screen.getByText('Course Management')).toBeInTheDocument();\n  });\n});\n"
  },
  {
    "path": "client/src/shared/components/Sider/AdminSider.tsx",
    "content": "import { CrownOutlined, MenuFoldOutlined, MenuUnfoldOutlined, ShareAltOutlined } from '@ant-design/icons';\nimport { Layout, Menu } from 'antd';\nimport type { MenuProps } from 'antd';\nimport Router from 'next/router';\nimport { useMemo, useContext } from 'react';\nimport { useLocalStorage } from 'react-use';\nimport { getAdminMenuItems, getCourseManagementMenuItems } from './data/menuItems';\nimport { SessionContext } from '@client/modules/Course/contexts';\nimport { Course } from '@client/services/models';\nimport { useActiveCourse } from '@client/modules/Home/hooks/useActiveCourse';\nconst { Sider } = Layout;\n\ntype Props = { courses: Course[]; activeCourse?: Course | null };\n\ntype MenuItem = Required<MenuProps>['items'][number];\n\nenum LocalStorage {\n  IsSiderCollapsed = 'isSiderCollapsed',\n  OpenedSidebarItems = 'openedSidebarItems',\n}\n\nfunction getItem(\n  key: React.Key,\n  label: React.ReactNode,\n  icon?: React.ReactNode,\n  children?: MenuItem[],\n  onClick?: () => void,\n): MenuItem {\n  return { key, label, icon, children, onClick };\n}\n\nexport function AdminSider(props: Props) {\n  const [isSiderCollapsed = true, setIsSiderCollapsed] = useLocalStorage<boolean>(LocalStorage.IsSiderCollapsed);\n  const [openedSidebarItems = [], setOpenedSidebarItems] = useLocalStorage<string[]>(LocalStorage.OpenedSidebarItems);\n  const [activeCourse] = useActiveCourse(props.courses);\n\n  const session = useContext(SessionContext);\n\n  const adminMenuItems = getAdminMenuItems(session);\n  const courseManagementMenuItems = useMemo(\n    () => getCourseManagementMenuItems(session, props.activeCourse ?? activeCourse),\n    [activeCourse, props.activeCourse],\n  );\n\n  const menuIconProps = {\n    onClick: () => {\n      setIsSiderCollapsed(!isSiderCollapsed);\n    },\n    style: { fontSize: '20px', display: 'block', lineHeight: '30px', padding: '20px 32px' },\n  };\n\n  const getMenuItems = () => {\n    const menuItems: MenuItem[] = [];\n    if (adminMenuItems.length) {\n      menuItems.push(\n        getItem(\n          'adminArea',\n          'Admin Area',\n          <CrownOutlined />,\n          adminMenuItems.map(item => getItem(item.key, item.name, item.icon, undefined, () => Router.push(item.href))),\n        ),\n      );\n    }\n    if (courseManagementMenuItems.length) {\n      menuItems.push(\n        getItem(\n          'courseManagement',\n          'Course Management',\n          <ShareAltOutlined />,\n          courseManagementMenuItems.map(item =>\n            getItem(item.key, item.name, item.icon, undefined, () => Router.push(item.href)),\n          ),\n        ),\n      );\n    }\n    return menuItems;\n  };\n\n  const onSidebarItemChanged: MenuProps['onOpenChange'] = (sidebarItemsKeys: string[]) => {\n    setOpenedSidebarItems(sidebarItemsKeys);\n  };\n\n  return (\n    <Sider data-testid=\"admin-sider\" trigger={null} collapsible collapsed={isSiderCollapsed} theme=\"light\" width={220}>\n      {isSiderCollapsed ? <MenuUnfoldOutlined {...menuIconProps} /> : <MenuFoldOutlined {...menuIconProps} />}\n      <Menu\n        mode=\"inline\"\n        items={getMenuItems()}\n        defaultOpenKeys={openedSidebarItems}\n        onOpenChange={onSidebarItemChanged}\n      />\n    </Sider>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Sider/data/menuItems.tsx",
    "content": "import AlertOutlined from '@ant-design/icons/AlertOutlined';\nimport AppstoreAddOutlined from '@ant-design/icons/AppstoreAddOutlined';\nimport BellOutlined from '@ant-design/icons/BellOutlined';\nimport GlobalOutlined from '@ant-design/icons/GlobalOutlined';\nimport HomeOutlined from '@ant-design/icons/HomeOutlined';\nimport IdcardFilled from '@ant-design/icons/IdcardFilled';\nimport NotificationFilled from '@ant-design/icons/NotificationFilled';\nimport ProfileFilled from '@ant-design/icons/ProfileFilled';\nimport TeamOutlined from '@ant-design/icons/TeamOutlined';\nimport UserOutlined from '@ant-design/icons/UserOutlined';\nimport FileTextOutlined from '@ant-design/icons/FileTextOutlined';\nimport ExclamationCircleOutlined from '@ant-design/icons/ExclamationCircleOutlined';\nimport QqOutlined from '@ant-design/icons/QqOutlined';\n\nimport { DiscordOutlined } from '@client/shared/components/Icons/DiscordOutlined';\nimport { Session } from '@client/components/withSession';\nimport {\n  isAdmin,\n  isAnyCourseManager,\n  isAnyCoursePowerUser,\n  isCourseManager,\n  isDementor,\n  isCourseSupervisor,\n  isHirer,\n  isAnyCourseDementor,\n  isTaskOwner,\n} from '@client/domain/user';\nimport { Course } from '@client/services/models';\n\nexport interface MenuItemsData {\n  name: string;\n  key: string;\n  icon?: JSX.Element;\n}\n\ninterface AdminMenuItemsData extends MenuItemsData {\n  access: (session: Session) => boolean;\n  href: string;\n}\n\ninterface CourseManagementMenuItemsData extends MenuItemsData {\n  getUrl: (course: Course) => string;\n  courseAccess: (session: Session, courseId: number) => boolean;\n}\n\nexport interface MenuItemsRenderData {\n  name: string;\n  key: string;\n  icon?: JSX.Element;\n  href: string;\n}\n\nconst some =\n  (...checks: ((session: Session, courseId: number) => boolean)[]) =>\n  (session: Session, courseId: number) =>\n    checks.some(check => check(session, courseId));\n\nconst adminMenuItems: AdminMenuItemsData[] = [\n  {\n    name: 'Main',\n    key: 'main',\n    icon: <HomeOutlined />,\n    href: '/',\n    access: session => isAdmin(session) || isAnyCoursePowerUser(session) || isAnyCourseDementor(session),\n  },\n  {\n    name: 'Disciplines',\n    key: 'disciplines',\n    icon: <AppstoreAddOutlined />,\n    href: '/admin/disciplines',\n    access: session => isAdmin(session),\n  },\n  {\n    name: 'Courses',\n    key: 'courses',\n    icon: <GlobalOutlined />,\n    href: '/admin/courses',\n    access: session => isAdmin(session) || isAnyCourseManager(session),\n  },\n  {\n    name: 'Tasks',\n    key: 'tasks',\n    icon: <AlertOutlined />,\n    href: '/admin/tasks',\n    access: session => isAdmin(session) || isAnyCourseManager(session),\n  },\n  {\n    name: 'Events',\n    key: 'events',\n    icon: <BellOutlined />,\n    href: '/admin/events',\n    access: session => isAdmin(session) || isAnyCourseManager(session),\n  },\n  {\n    name: 'Users',\n    key: 'users',\n    icon: <UserOutlined />,\n    href: '/admin/users',\n    access: session => isAdmin(session) || isAnyCourseManager(session),\n  },\n  {\n    name: 'Mentor Registry',\n    key: 'mentorRegistry',\n    icon: <IdcardFilled />,\n    href: '/admin/mentor-registry',\n    access: session => isAdmin(session) || isAnyCoursePowerUser(session),\n  },\n  {\n    name: 'Discord/Telegram',\n    key: 'discordServers',\n    icon: <DiscordOutlined />,\n    href: '/admin/discord-telegram',\n    access: session => isAdmin(session),\n  },\n  {\n    name: 'User Groups',\n    key: 'userGroups',\n    icon: <TeamOutlined />,\n    href: '/admin/user-group',\n    access: session => isAdmin(session),\n  },\n  {\n    name: 'Applicants',\n    key: 'applicants',\n    icon: <ProfileFilled />,\n    href: '/applicants',\n    access: session => isAdmin(session) || isHirer(session),\n  },\n  {\n    name: 'Notifications',\n    key: 'notifications',\n    icon: <NotificationFilled />,\n    href: '/admin/notifications',\n    access: session => isAdmin(session),\n  },\n  {\n    name: 'Prompts',\n    key: 'prompts',\n    icon: <FileTextOutlined />,\n    href: '/admin/prompts',\n    access: session => isAdmin(session),\n  },\n  {\n    name: 'Auto tests',\n    key: 'auto-test',\n    icon: <ExclamationCircleOutlined />,\n    href: '/admin/auto-test',\n    access: session => isAdmin(session) || isAnyCourseManager(session),\n  },\n  {\n    name: 'Students',\n    key: 'students',\n    icon: <QqOutlined />,\n    href: '/admin/students',\n    access: session => isAdmin(session) || isHirer(session),\n  },\n];\n\nexport function getAdminMenuItems(session: Session): MenuItemsRenderData[] {\n  return adminMenuItems.filter(item => item.access(session));\n}\n\nconst courseManagementMenuItems: CourseManagementMenuItemsData[] = [\n  {\n    name: 'Course Events',\n    key: 'courseEvents',\n    getUrl: (course: Course) => `/course/admin/events?course=${course.alias}`,\n    courseAccess: isCourseManager,\n  },\n  {\n    name: 'Course Tasks',\n    key: 'courseTasks',\n    getUrl: (course: Course) => `/course/admin/tasks?course=${course.alias}`,\n    courseAccess: isCourseManager,\n  },\n  {\n    name: 'Submit Scores',\n    key: 'submitScores',\n    getUrl: (course: Course) => `/course/submit-scores?course=${course.alias}`,\n    courseAccess: some(isTaskOwner, isAdmin, isCourseManager),\n  },\n  {\n    name: 'Course Students',\n    key: 'courseStudents',\n    getUrl: (course: Course) => `/course/admin/students?course=${course.alias}`,\n    courseAccess: some(isCourseManager, isCourseSupervisor, isDementor),\n  },\n  {\n    name: 'Course Mentors',\n    key: 'courseMentors',\n    getUrl: (course: Course) => `/course/admin/mentors?course=${course.alias}`,\n    courseAccess: some(isCourseManager, isCourseSupervisor),\n  },\n  {\n    name: 'Course Users',\n    key: 'courseUsers',\n    getUrl: (course: Course) => `/course/admin/users?course=${course.alias}`,\n    courseAccess: isCourseManager,\n  },\n  {\n    name: 'Cross-Check Table',\n    key: 'Cross-Check Table',\n    getUrl: (course: Course) => `/course/admin/cross-check-table?course=${course.alias}`,\n    courseAccess: some(isCourseManager, isDementor),\n  },\n  {\n    name: 'Technical Screening',\n    key: 'technicalScreening',\n    getUrl: (course: Course) => `/course/admin/stage-interviews?course=${course.alias}`,\n    courseAccess: some(isCourseManager, isCourseSupervisor),\n  },\n  {\n    name: 'CoreJs Interviews',\n    key: 'coreJsInterviews',\n    getUrl: (course: Course) => `/course/admin/interviews?course=${course.alias}`,\n    courseAccess: isCourseManager,\n  },\n  {\n    name: 'Reports',\n    key: 'reports',\n    getUrl: (course: Course) => `/course/admin/reports?course=${course.alias}`,\n    courseAccess: some(isAdmin, isCourseManager, isCourseSupervisor),\n  },\n  {\n    name: 'Mentor Tasks Review',\n    key: 'mentorTasksReview',\n    getUrl: (course: Course) => `/course/admin/mentor-tasks-review?course=${course.alias}`,\n    courseAccess: some(isCourseManager, isDementor),\n  },\n];\n\nexport function getCourseManagementMenuItems(session: Session, activeCourse: Course | null): MenuItemsRenderData[] {\n  return activeCourse\n    ? courseManagementMenuItems\n        .filter(route => isAdmin(session) || (route.courseAccess(session, activeCourse.id) ?? true))\n        .map(({ name, key, icon, getUrl }) => ({ name, icon, key, href: getUrl(activeCourse) }))\n    : [];\n}\n"
  },
  {
    "path": "client/src/shared/components/SolidarityUkraine.tsx",
    "content": "import Image from 'next/image';\n\nexport function SolidarityUkraine() {\n  return <Image src=\"/static/svg/solidarity-Ukraine.svg\" alt=\"Stand With Ukraine\" width={146} height={32} />;\n}\n"
  },
  {
    "path": "client/src/shared/components/StudentMentorModal.tsx",
    "content": "import { Form, Row, Col } from 'antd';\nimport { StudentSearch } from './StudentSearch';\nimport { ModalForm } from './Forms';\nimport { MentorSearch } from './MentorSearch';\n\ninterface FormValues {\n  studentGithubId: string;\n  mentorGithubId: string;\n}\n\ntype Props = {\n  visible: boolean;\n  courseId: number;\n  onCancel: () => void;\n  onOk: (studentGithubId: string, mentorGithubId: string) => void;\n};\n\nexport function StudentMentorModal(props: Props) {\n  const handleSubmit = async (values: FormValues) => {\n    props.onOk(values.studentGithubId, values.mentorGithubId);\n  };\n\n  if (!props.visible) {\n    return null;\n  }\n\n  return (\n    <ModalForm\n      title=\"Student/Mentor\"\n      data={{ studentGithubId: '', mentorGithubId: '' }}\n      submit={handleSubmit}\n      cancel={props.onCancel}\n    >\n      <Row gutter={24}>\n        <Col span={24}>\n          <Form.Item\n            name=\"studentGithubId\"\n            rules={[{ required: true, message: 'Please select student' }]}\n            label=\"Student\"\n          >\n            <StudentSearch keyField=\"githubId\" courseId={props.courseId} />\n          </Form.Item>\n        </Col>\n      </Row>\n      <Row gutter={24}>\n        <Col span={24}>\n          <Form.Item\n            name=\"mentorGithubId\"\n            rules={[{ required: true, message: 'Please select  mentor' }]}\n            label=\"Mentor\"\n          >\n            <MentorSearch keyField=\"githubId\" courseId={props.courseId} />\n          </Form.Item>\n        </Col>\n      </Row>\n    </ModalForm>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/StudentSearch.tsx",
    "content": "import { UserSearch, UserProps } from '@client/shared/components/UserSearch';\nimport { useCallback, useMemo } from 'react';\nimport { CourseService } from '@client/services/course';\n\ntype Props = UserProps & {\n  courseId: number;\n  onlyStudentsWithoutMentorShown?: boolean;\n};\n\nexport function StudentSearch(props: Props) {\n  const { courseId, ...otherProps } = props;\n  const courseService = useMemo(() => new CourseService(courseId), [courseId]);\n  const handleSearch = useCallback(\n    async (value: string, onlyStudentsWithoutMentorShown = false) =>\n      courseService.searchStudents(value, onlyStudentsWithoutMentorShown),\n    [courseService],\n  );\n\n  return <UserSearch {...otherProps} searchFn={handleSearch} />;\n}\n"
  },
  {
    "path": "client/src/shared/components/Table/PersonCell.tsx",
    "content": "import { GithubUserLink } from '@client/shared/components/GithubUserLink';\n\ntype Person = { name: string; githubId: string; cityName?: string | null; countryName?: string | null };\ntype Props = { value: Person; showCountry?: boolean };\nexport function PersonCell({ value, showCountry }: Props) {\n  const hasName = value.name;\n  const hasCity = value?.cityName != null;\n  return (\n    <div style={{ display: 'flex', flexDirection: 'column' }}>\n      <GithubUserLink value={value.githubId} />\n      <small>\n        {value.name}\n        {hasName && hasCity ? ', ' : ''}\n        {value?.cityName}\n        {showCountry ? `, ${value.countryName}` : ''}\n      </small>\n    </div>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Table/columns.tsx",
    "content": "import { SearchOutlined } from '@ant-design/icons';\nimport { Button, Input, InputRef } from 'antd';\nimport { ColumnType } from 'antd/lib/table';\nimport get from 'lodash/get';\n\nconst searchRef = { current: null as InputRef | null };\n\nexport function getColumnSearchProps<T = unknown>(\n  dataIndex: string | string[],\n  label?: string,\n): Pick<ColumnType<T>, 'filterDropdown' | 'filterIcon' | 'onFilter' | 'onFilterDropdownOpenChange'> {\n  return {\n    filterDropdown: ({\n      setSelectedKeys,\n      selectedKeys,\n      confirm,\n      clearFilters,\n    }: {\n      setSelectedKeys: (keys: React.Key[]) => void;\n      selectedKeys: React.Key[];\n      confirm: () => void;\n      clearFilters?: () => void;\n    }) => (\n      <div style={{ padding: 8 }}>\n        <Input\n          ref={node => {\n            searchRef.current = node;\n          }}\n          onKeyDown={e => (e.keyCode === 13 ? confirm() : undefined)}\n          placeholder={`Search ${label || dataIndex}`}\n          value={selectedKeys[0]}\n          onChange={e => {\n            setSelectedKeys([e.target.value]);\n          }}\n          style={{ width: 188, marginBottom: 8, display: 'block' }}\n        />\n        <Button\n          onClick={confirm}\n          type=\"primary\"\n          icon={<SearchOutlined />}\n          size=\"small\"\n          style={{ width: 90, marginRight: 8 }}\n        >\n          Search\n        </Button>\n        <Button\n          onClick={() => {\n            clearFilters?.();\n            confirm();\n          }}\n          size=\"small\"\n          style={{ width: 90 }}\n        >\n          Reset\n        </Button>\n      </div>\n    ),\n    filterIcon: (filtered: boolean) => <SearchOutlined style={{ color: filtered ? '#1677ff' : undefined }} />,\n    onFilter: (value: boolean | React.Key, record) => {\n      if (value == null) {\n        return false;\n      }\n      const fields = Array.isArray(dataIndex) ? dataIndex : [dataIndex];\n\n      const val = fields.some(field =>\n        (get(record, field) || '').toString().toLowerCase().includes(value.toString().toLowerCase()),\n      );\n      return val;\n    },\n    onFilterDropdownOpenChange: (visible: boolean) => {\n      if (visible) {\n        requestAnimationFrame(() => searchRef.current?.select());\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "client/src/shared/components/Table/index.ts",
    "content": "export * from './renderers';\nexport * from './sorters';\nexport * from './columns';\nexport { PersonCell } from './PersonCell';\n"
  },
  {
    "path": "client/src/shared/components/Table/renderers.tsx",
    "content": "import CheckCircleFilled from '@ant-design/icons/CheckCircleFilled';\nimport ChromeOutlined from '@ant-design/icons/ChromeOutlined';\nimport GithubOutlined from '@ant-design/icons/GithubOutlined';\nimport MinusCircleOutlined from '@ant-design/icons/MinusCircleOutlined';\nimport YoutubeOutlined from '@ant-design/icons/YoutubeOutlined';\nimport InfoCircleOutlined from '@ant-design/icons/InfoCircleOutlined';\nimport { Tag, Tooltip, Typography } from 'antd';\nimport { BaseType } from 'antd/lib/typography/Base';\nimport {\n  CourseScheduleItemDto,\n  CourseScheduleItemDtoTagEnum,\n  CheckerEnum,\n  TaskDto,\n  CrossCheckStatusEnum,\n} from '@client/api';\nimport dayjs from 'dayjs';\nimport utc from 'dayjs/plugin/utc';\nimport timezone from 'dayjs/plugin/timezone';\n\ndayjs.extend(utc);\ndayjs.extend(timezone);\n\nconst { Text, Link } = Typography;\n\nexport function dateRenderer(value: string | null) {\n  return value ? dayjs(value).format('YYYY-MM-DD') : '';\n}\n\nexport function dateUtcRenderer(value: string | null) {\n  return value ? dayjs.utc(value).format('YYYY-MM-DD') : '';\n}\n\nexport function crossCheckDateRenderer(value: string | null, { checker }: { checker: CheckerEnum }) {\n  if (checker !== CheckerEnum.CrossCheck) return 'N/A';\n  return value ? dayjs(value).tz('UTC').format('YYYY-MM-DD') : 'Not Set';\n}\n\nexport function crossCheckStatusRenderer(value: CrossCheckStatusEnum, { checker }: { checker: CheckerEnum }) {\n  return checker !== CheckerEnum.CrossCheck ? (\n    'N/A'\n  ) : value === CrossCheckStatusEnum.Initial ? (\n    'Not distributed'\n  ) : (\n    <span style={{ textTransform: 'capitalize' }}>{value}</span>\n  );\n}\n\nexport function timeRenderer(value: string) {\n  return value ? dayjs(value, 'HH:mm:ssZ').format('HH:mm') : '';\n}\n\nexport function dateTimeRenderer(value: string | null) {\n  return value ? dayjs(value).format('YYYY-MM-DD HH:mm') : '';\n}\n\nexport function shortDateTimeRenderer(value: string) {\n  return value ? dayjs(value).format('DD.MM HH:mm') : '';\n}\n\nexport const dateWithTimeZoneRenderer = (timeZone: string, format: string) => (value: string) =>\n  value ? dayjs.utc(value).tz(timeZone).format(format) : '';\n\nexport function boolRenderer(value: string) {\n  return value != null ? value.toString() : '';\n}\n\nexport function buildCheckBoxRenderer<T>(\n  dataIndex: string[],\n  onChange: (id: string[], record: T, checked: boolean) => void,\n  undefinedAsTrue?: boolean,\n) {\n  return function (value: boolean, record: T) {\n    const defaultValue = value ?? false;\n    return (\n      <input\n        type=\"checkbox\"\n        checked={undefinedAsTrue ? value === undefined || defaultValue : defaultValue}\n        onChange={event => onChange(dataIndex, record, event.target.checked)}\n      />\n    );\n  };\n}\n\nexport function boolIconRenderer(value: unknown) {\n  return value ? (\n    <CheckCircleFilled title={(!!value).toString()} />\n  ) : (\n    <MinusCircleOutlined title={(!!value).toString()} />\n  );\n}\n\nexport function colorTagRenderer(value: number | string, color?: string) {\n  return <span key={value}>{renderTag(value, color)}</span>;\n}\n\nexport function tagsRenderer(values: (number | string)[]) {\n  if (!Array.isArray(values)) {\n    return '';\n  }\n  return <span>{values.map(v => renderTag(v))}</span>;\n}\n\nexport function tagsCoursesRendererWithRemainingNumber(_: undefined, { courses }: TaskDto) {\n  if (!courses?.length) {\n    return;\n  }\n\n  const [firstCourse] = courses;\n  const firstTag = { value: firstCourse?.name ?? '', ...(firstCourse?.isActive && { color: 'blue' }) };\n  const remainingCoursesCount = courses.length - 1;\n\n  const tags = [firstTag];\n\n  if (remainingCoursesCount > 0) {\n    tags.push({ value: `+ ${remainingCoursesCount} More` });\n  }\n\n  return <span>{tags.map(({ value, color }) => renderTag(value, color))}</span>;\n}\n\nexport function renderTag(value: number | string, color?: string) {\n  return (\n    <Tag color={color} key={value}>\n      {value}\n    </Tag>\n  );\n}\n\nexport function stringTrimRenderer(value: string) {\n  return value && value.length > 20 ? `${value.slice(0, 20)}...` : value;\n}\n\nexport const idFromArrayRenderer =\n  <T extends { id: number; name: string }>(data: T[]) =>\n  (value: number) => {\n    const item = data.find(d => d.id === value);\n    return item ? item.name : '(Empty)';\n  };\n\nconst getUrlIcon = (url: string) => {\n  const lowerUrl = url.toLowerCase();\n  const isGithubLink = lowerUrl.includes('github');\n  const isYoutubeLink = lowerUrl.includes('youtube');\n  const isYoutubeLink2 = lowerUrl.includes('youtu.be');\n\n  if (isGithubLink) {\n    return <GithubOutlined />;\n  }\n\n  if (isYoutubeLink || isYoutubeLink2) {\n    return <YoutubeOutlined />;\n  }\n\n  return <ChromeOutlined />;\n};\n\nexport const urlRenderer = (url: string) =>\n  !!url && (\n    <Tooltip placement=\"topLeft\" title={url}>\n      <a target=\"_blank\" href={url}>\n        {getUrlIcon(url)}\n      </a>\n    </Tooltip>\n  );\n\nexport const weightRenderer = (weight: number | null) => {\n  if (weight === null) return null;\n\n  return <Text>×{+weight.toFixed(2)}</Text>;\n};\n\nexport const scoreRenderer = (item: CourseScheduleItemDto) => {\n  const { maxScore, score } = item;\n  if (maxScore == null) return null;\n\n  return (\n    <Text>\n      {score ?? 0} / {maxScore}\n    </Text>\n  );\n};\n\nexport const renderTask = (name: string, descriptionUrl: string | null) => {\n  if (!descriptionUrl) return name;\n\n  return (\n    <Link target=\"_blank\" href={descriptionUrl}>\n      {name}\n    </Link>\n  );\n};\n\nexport const coloredDateRenderer = (timeZone: string, format: string, date: 'start' | 'end', infoText: string) => {\n  const now = dayjs().utc();\n  return (value: string, { startDate, endDate, score, tag }: CourseScheduleItemDto) => {\n    let color: BaseType | undefined = undefined;\n    const start = dayjs.utc(startDate);\n    const end = dayjs.utc(endDate);\n\n    const isDeadlineSoon = now <= end && end.diff(now, 'hours') < 48 && !score;\n    const isCurrent = now >= start && now < end && !score;\n    const isDeadlineMissed = now >= end && end.diff(now, 'hours') >= -24 && !score;\n    const isPast = now > end || score;\n\n    if (isDeadlineSoon && date === 'end') color = 'warning';\n    else if (isCurrent && date === 'start') color = 'success';\n    else if (isDeadlineMissed && date === 'end') color = 'danger';\n    else if (isPast) color = 'secondary';\n\n    const text = dateWithTimeZoneRenderer(timeZone, format)(value);\n\n    if (tag == CourseScheduleItemDtoTagEnum.SelfStudy) {\n      return (\n        <Text type={color}>\n          {text}\n          <Tooltip placement=\"topLeft\" title={infoText}>\n            <InfoCircleOutlined className=\"ant-typography ant-typography-secondary\" style={{ marginLeft: 8 }} />\n          </Tooltip>\n        </Text>\n      );\n    }\n    return <Text type={color}>{text}</Text>;\n  };\n};\n"
  },
  {
    "path": "client/src/shared/components/Table/sorters.ts",
    "content": "import get from 'lodash/get';\n\ntype SortOrder = 'descend' | 'ascend' | null;\n\nexport function stringSorter<T>(field: string) {\n  return (a: T, b: T, order?: SortOrder) => {\n    const valueOne = get(a, field, '');\n    const valueTwo = get(b, field, '');\n\n    if (valueOne == valueTwo || (a == null && b == null) || (valueOne == null && valueTwo == null)) {\n      return 0;\n    }\n    if (a == null || valueOne == null) {\n      return order === 'ascend' ? 1 : -1;\n    } else if (b == null || valueTwo == null) {\n      return order === 'ascend' ? -1 : 1;\n    }\n    return valueOne.toString().toLowerCase().localeCompare(valueTwo.toString().toLowerCase());\n  };\n}\n\nexport function numberSorter<T>(field: string) {\n  return (a: T, b: T) => {\n    if (a == null && b == null) {\n      return 0;\n    }\n    if (a == null) {\n      return 1;\n    }\n    if (b == null) {\n      return -1;\n    }\n    const aValue = get(a, field, 0);\n    const bValue = get(b, field, 0);\n    return Number(bValue) - Number(aValue);\n  };\n}\n\nexport function boolSorter<T>(field: string) {\n  return (a: T, b: T) => {\n    if (a == null && b == null) {\n      return 0;\n    }\n    if (a == null) {\n      return 1;\n    }\n    if (b == null) {\n      return -1;\n    }\n    const aValue = !!get(a, field, 0);\n    const bValue = !!get(b, field, 0);\n    return Number(bValue) - Number(aValue);\n  };\n}\n\nexport function dateSorter<T>(field: string) {\n  return (a: T, b: T) => {\n    if (a == null && b == null) {\n      return 0;\n    }\n    if (a == null) {\n      return 1;\n    }\n    if (b == null) {\n      return -1;\n    }\n    const aValue = get(a, field, 0) as number;\n    const bValue = get(b, field, 0) as number;\n    return new Date(bValue).getTime() - new Date(aValue).getTime();\n  };\n}\n"
  },
  {
    "path": "client/src/shared/components/ThemeSwitch.tsx",
    "content": "import { Dropdown, Flex, MenuProps, theme as antTheme } from 'antd';\nimport { useTheme } from '@client/hooks';\nimport { AppTheme } from '@client/providers';\nimport { MoonOutlined, SkinOutlined, SunOutlined } from '@ant-design/icons';\nimport NonTouchTooltip from '@client/shared/components/NonTouchTooltip';\n\nconst THEME_CONFIG = {\n  [AppTheme.Dark]: {\n    icon: <MoonOutlined />,\n    label: 'Dark Theme',\n  },\n  [AppTheme.Light]: {\n    icon: <SunOutlined />,\n    label: 'Light Theme',\n  },\n  auto: {\n    icon: <SkinOutlined />,\n    label: 'Auto Theme',\n  },\n};\n\nexport default function ThemeSwitch() {\n  const { themeChange, theme, autoTheme, changeAutoTheme } = useTheme();\n  const { token } = antTheme.useToken();\n\n  const themeIcon = autoTheme ? THEME_CONFIG.auto.icon : THEME_CONFIG[theme].icon;\n\n  const items: MenuProps['items'] = [\n    {\n      label: (\n        <NonTouchTooltip title={THEME_CONFIG[AppTheme.Dark].label}>{THEME_CONFIG[AppTheme.Dark].icon}</NonTouchTooltip>\n      ),\n      key: 'dark',\n      onClick: () => themeChange(AppTheme.Dark),\n    },\n    {\n      label: (\n        <NonTouchTooltip title={THEME_CONFIG[AppTheme.Light].label}>\n          {THEME_CONFIG[AppTheme.Light].icon}\n        </NonTouchTooltip>\n      ),\n      key: 'light',\n      onClick: () => themeChange(AppTheme.Light),\n    },\n    {\n      label: <NonTouchTooltip title={THEME_CONFIG.auto.label}>{THEME_CONFIG.auto.icon}</NonTouchTooltip>,\n      key: 'auto',\n      onClick: () => changeAutoTheme(),\n    },\n  ];\n\n  return (\n    <Flex\n      vertical\n      align=\"center\"\n      justify=\"center\"\n      gap=\"small\"\n      style={{\n        fontSize: 18,\n        cursor: 'pointer',\n        color: token.colorTextLabel,\n      }}\n    >\n      <NonTouchTooltip title=\"Change color theme\">\n        <Dropdown menu={{ items }} placement=\"bottom\" trigger={['click']}>\n          {themeIcon}\n        </Dropdown>\n      </NonTouchTooltip>\n    </Flex>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/Timer.tsx",
    "content": "import { useEffect, useState } from 'react';\n\nexport function Timer({ onElapsed, seconds }: { onElapsed: () => void; seconds: number }) {\n  const [leftSeconds, setLeftSeconds] = useState(seconds);\n\n  useEffect(() => {\n    setLeftSeconds(seconds);\n  }, [seconds]);\n\n  useEffect(() => {\n    let timeout: NodeJS.Timer | undefined;\n\n    if (leftSeconds > 0) {\n      timeout = setTimeout(() => {\n        setLeftSeconds(leftSeconds => leftSeconds - 1);\n      }, 1000);\n    } else {\n      onElapsed();\n    }\n    return () => {\n      if (timeout) {\n        clearTimeout(timeout as NodeJS.Timeout);\n      }\n    };\n  }, [leftSeconds]);\n\n  return <>{leftSeconds} seconds</>;\n}\n"
  },
  {
    "path": "client/src/shared/components/TooltipedButton.tsx",
    "content": "import { Button, Tooltip } from 'antd';\n\nexport type TooltipedButtonProps = {\n  tooltipTitle: string;\n  buttonText: string;\n  open: boolean;\n  loading: boolean;\n  disabled: boolean;\n};\n\nexport function TooltipedButton(props: TooltipedButtonProps) {\n  const { tooltipTitle, open, loading, disabled, buttonText } = props;\n\n  return (\n    <Tooltip title={tooltipTitle} open={open}>\n      <Button loading={loading} type=\"primary\" htmlType=\"submit\" disabled={disabled}>\n        {buttonText}\n      </Button>\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "client/src/shared/components/UserSearch.tsx",
    "content": "import { useState, useEffect } from 'react';\nimport { Select, Typography } from 'antd';\nimport { GithubAvatar } from '@client/shared/components/GithubAvatar';\nimport { get, debounce } from 'lodash';\nimport { SelectProps } from 'antd/lib/select';\nimport type { SearchStudent } from '@client/services/course';\n\ntype Person = { id: number; githubId: string; name: string } | SearchStudent;\n\nexport type UserProps = SelectProps<string> & {\n  searchFn?: (value: string, onlyStudentsWithoutMentorShown?: boolean) => Promise<Person[]>;\n  defaultValues?: Person[];\n  keyField?: 'id' | 'githubId';\n  showMentor?: boolean;\n  onlyStudentsWithoutMentorShown?: boolean;\n};\n\nexport function UserSearch(props: UserProps) {\n  const [data, setData] = useState<Person[]>([]);\n  const {\n    searchFn = defaultSearch,\n    defaultValues,\n    keyField,\n    showMentor,\n    onlyStudentsWithoutMentorShown,\n    ...otherProps\n  } = props;\n\n  useEffect(() => {\n    setData(defaultValues ?? []);\n  }, [props.defaultValues]);\n\n  const handleSearch = debounce(async (value: string) => {\n    value = value.trim();\n    if (value) {\n      const data = await searchFn(value, onlyStudentsWithoutMentorShown);\n      setData(data);\n    } else {\n      setData(props.defaultValues ?? []);\n    }\n  }, 300);\n\n  return (\n    <Select\n      showSearch\n      allowClear\n      {...otherProps}\n      defaultValue={undefined}\n      defaultActiveFirstOption={false}\n      suffixIcon={defaultValues ? Boolean(defaultValues.length) : false}\n      filterOption={false}\n      onSearch={handleSearch}\n      placeholder={(defaultValues?.length ?? 0 > 0) ? 'Select...' : 'Search...'}\n      notFoundContent={null}\n    >\n      {data.map(person => {\n        const key = keyField ? get(person, keyField) : person.id;\n        return (\n          <Select.Option key={key} value={key}>\n            <GithubAvatar size={24} githubId={person.githubId} /> {person.name} ({person.githubId})\n            {showMentor && (person as SearchStudent).mentor ? (\n              <Typography.Paragraph type=\"warning\">\n                Current mentor: {(person as SearchStudent).mentor?.githubId}\n              </Typography.Paragraph>\n            ) : (\n              ''\n            )}\n          </Select.Option>\n        );\n      })}\n    </Select>\n  );\n\n  async function defaultSearch(value: string) {\n    return defaultValues?.filter(v => v.name.startsWith(value) || v.githubId.startsWith(value)) ?? [];\n  }\n}\n"
  },
  {
    "path": "client/src/shared/hooks/useMessage.tsx",
    "content": "import { useContext } from 'react';\nimport { MessageContext } from '@client/providers/MessageProvider';\n\nexport function useMessage() {\n  return useContext(MessageContext);\n}\n"
  },
  {
    "path": "client/src/shared/hooks/useModal/useModalForm.test.tsx",
    "content": "import { renderHook, act } from '@testing-library/react';\nimport { useModalForm } from './useModalForm';\n\ndescribe('useModalForm', () => {\n  it('should return the correct initial state', () => {\n    const { result } = renderHook(() => useModalForm<{ id: number }>());\n    expect(result.current.mode).toBe('create');\n    expect(result.current.open).toBe(false);\n    expect(result.current.formData).toBe(undefined);\n  });\n\n  it('should toggle the modal when toggle is called', () => {\n    const { result } = renderHook(() => useModalForm<{ id: number }>());\n    act(() => {\n      result.current.toggle();\n    });\n    expect(result.current.open).toBe(true);\n    act(() => {\n      result.current.toggle();\n    });\n    expect(result.current.open).toBe(false);\n  });\n\n  it('should set the mode to \"edit\" and form data when toggle is called with data', () => {\n    const { result } = renderHook(() => useModalForm<{ id: number }>());\n    act(() => {\n      result.current.toggle({ id: 1 });\n    });\n    expect(result.current.mode).toBe('edit');\n    expect(result.current.formData).toEqual({ id: 1 });\n  });\n\n  it('should set the mode to \"create\" and form data to null when toggle is called without data', () => {\n    const { result } = renderHook(() => useModalForm<{ id: number }>());\n    act(() => {\n      result.current.toggle({ id: 1 });\n    });\n    act(() => {\n      result.current.toggle();\n    });\n    expect(result.current.mode).toBe('create');\n    expect(result.current.formData).toBe(undefined);\n  });\n});\n"
  },
  {
    "path": "client/src/shared/hooks/useModal/useModalForm.tsx",
    "content": "import { useState } from 'react';\n\nexport type ModalFormMode = 'create' | 'edit';\n\nexport const useModalForm = <T,>() => {\n  const [mode, setMode] = useState<ModalFormMode>('create');\n  const [open, setOpen] = useState(false);\n  const [formData, setFormData] = useState<T>();\n\n  const toggle = (data?: T): void => {\n    if (data) {\n      setMode('edit');\n      setFormData(data);\n    } else {\n      setMode('create');\n      setFormData(undefined);\n    }\n    setOpen(prev => !prev);\n  };\n\n  return { mode, open, formData, toggle };\n};\n"
  },
  {
    "path": "client/src/shared/hooks/useTheme.tsx",
    "content": "import { useContext } from 'react';\nimport { ThemeContext } from '@client/providers';\n\nexport function useTheme() {\n  return useContext(ThemeContext);\n}\n"
  },
  {
    "path": "client/src/shared/hooks/useWindowDimensions.ts",
    "content": "import { useState, useEffect } from 'react';\n\nfunction getWindowDimensions() {\n  const { innerWidth: width, innerHeight: height } = window;\n  return {\n    width,\n    height,\n  };\n}\n\nfunction useWindowDimensions() {\n  const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());\n\n  useEffect(() => {\n    function handleResize() {\n      setWindowDimensions(getWindowDimensions());\n    }\n\n    window.addEventListener('resize', handleResize);\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  return windowDimensions;\n}\n\nexport default useWindowDimensions;\n"
  },
  {
    "path": "client/src/shared/utils/onlyDefined.ts",
    "content": "import pickBy from 'lodash/pickBy';\n\nexport function onlyDefined<T extends Record<string, unknown>>(data: T) {\n  return pickBy<T>(data, val => val !== undefined && val !== '' && val !== null);\n}\n"
  },
  {
    "path": "client/src/shared/utils/pagination.ts",
    "content": "export type IPaginationInfo = {\n  total?: number;\n  totalPages?: number;\n  current: number;\n  pageSize: number;\n};\n"
  },
  {
    "path": "client/src/shared/utils/queryParams-utils.test.ts",
    "content": "import { getQueryParams, getQueryString } from './queryParams-utils';\n\ndescribe('queryParams', () => {\n  describe('getQueryParams', () => {\n    const initialQueryParams = { example: 'value' };\n\n    it('Should be return initialQueryParams', () => {\n      expect(getQueryParams).toBeInstanceOf(Function);\n      expect(getQueryParams({})).toEqual({});\n      expect(getQueryParams({}, initialQueryParams)).toEqual(initialQueryParams);\n      expect(getQueryParams({ value: undefined }, initialQueryParams)).toEqual(initialQueryParams);\n      expect(getQueryParams({ value: null }, initialQueryParams)).toEqual(initialQueryParams);\n      expect(getQueryParams({ value: '' }, initialQueryParams)).toEqual(initialQueryParams);\n      expect(getQueryParams({ value: [''] }, initialQueryParams)).toEqual(initialQueryParams);\n    });\n\n    it('Should be return initialQueryParams and newQueryParams', () => {\n      const hello = 'hello';\n      const newQueryParams = { b: 'string', c: [hello], ['more string']: 'string 12' };\n      const expected = { ...newQueryParams, c: hello, ...initialQueryParams };\n\n      expect(getQueryParams(newQueryParams)).toEqual({ ...newQueryParams, c: hello });\n      expect(getQueryParams(newQueryParams, initialQueryParams)).toEqual(expected);\n      expect(getQueryParams({ ...newQueryParams, value: undefined }, initialQueryParams)).toEqual(expected);\n      expect(getQueryParams({ ...newQueryParams, value: null }, initialQueryParams)).toEqual(expected);\n      expect(getQueryParams({ ...newQueryParams, value: '' }, initialQueryParams)).toEqual(expected);\n      expect(getQueryParams({ ...newQueryParams, value: [''] }, initialQueryParams)).toEqual(expected);\n    });\n  });\n\n  describe('getQueryString', () => {\n    it('Should be return \"\"', () => {\n      expect(getQueryString).toBeInstanceOf(Function);\n      expect(getQueryString()).toBe('');\n      expect(getQueryString({ value: undefined })).toBe('');\n      expect(getQueryString({ value: null })).toBe('');\n      expect(getQueryString({ value: '' })).toBe('');\n    });\n\n    it('Should be return correct string', () => {\n      expect(getQueryString({ value: 'string' })).toBe('?value=string');\n      expect(getQueryString({ value: 100 })).toBe('?value=100');\n      expect(getQueryString({ value: true })).toBe('?value=true');\n      expect(getQueryString({ value: false })).toBe('?value=false');\n      expect(getQueryString({ value: [100] })).toBe('?value=100');\n      expect(getQueryString({ value: [100, 'string'] })).toBe('?value=100%2Cstring');\n      expect(getQueryString({ value: { b: 'string' } })).toBe('?value=%5Bobject+Object%5D');\n      expect(getQueryString({ ['more value']: 100 })).toBe('?more+value=100');\n    });\n  });\n});\n"
  },
  {
    "path": "client/src/shared/utils/queryParams-utils.ts",
    "content": "import isNil from 'lodash/isNil';\nimport { ParsedUrlQuery } from 'querystring';\nimport { onlyDefined } from './onlyDefined';\n\nexport const getQueryParams = (\n  queryParams: { [key: string]: string | string[] | null | undefined },\n  initialQueryParams: ParsedUrlQuery = {},\n): ParsedUrlQuery => {\n  let params = { ...initialQueryParams };\n  for (const [key, value] of Object.entries(queryParams)) {\n    if (!isNil(value)) {\n      if (Array.isArray(value)) {\n        const trimmedElements = value.map(elem => elem.trim()).join(',');\n        if (trimmedElements !== '') {\n          params = { ...params, [key]: trimmedElements };\n        }\n      } else if (typeof value === 'string' && value !== '') {\n        params = { ...params, [key]: value.trim() };\n      }\n    }\n  }\n\n  return params;\n};\n\nexport const getQueryString = (params = {}): string => {\n  const queryParams = new URLSearchParams({\n    ...(onlyDefined(params) as object),\n  });\n  const queryString = queryParams.toString();\n  return queryString && `?${queryString}`;\n};\n"
  },
  {
    "path": "client/src/shared/utils/text-utils.test.ts",
    "content": "import { filterLogin } from './text-utils';\n\ndescribe('filterLogin', () => {\n  it('Should be an instance of Function', () => {\n    expect(filterLogin).toBeInstanceOf(Function);\n  });\n\n  it('Should return clear login if login were passed', () => {\n    expect(filterLogin('mikhama/')).toBe('mikhama');\n    expect(filterLogin('mikhama')).toBe('mikhama');\n  });\n\n  it('Should return clear login if github-link were passed', () => {\n    expect(filterLogin('github.com/mikhama')).toBe('mikhama');\n    expect(filterLogin('http://github.com/mikhama/')).toBe('mikhama');\n    expect(filterLogin('http://github.com/mikhama')).toBe('mikhama');\n    expect(filterLogin('https://github.com/mikhama')).toBe('mikhama');\n    expect(filterLogin('https://github.com/Nastyasimanovich')).toBe('Nastyasimanovich');\n    expect(filterLogin('https://github.com/Sergursevich')).toBe('Sergursevich');\n  });\n\n  it('Should return clear login if github-link with get parameters were passed', () => {\n    expect(filterLogin('https://github.com/evsechicov?tab=repositories')).toBe('evsechicov');\n    expect(filterLogin('https://github.com/evsechicov?tab=repositories/')).toBe('evsechicov');\n    expect(filterLogin('http://github.com/evsechicov?tab=repositories')).toBe('evsechicov');\n    expect(filterLogin('github.com/evsechicov?tab=repositories')).toBe('evsechicov');\n  });\n\n  it('Should return clear login if github-link with tail were passed', () => {\n    expect(filterLogin('https://github.com/MikhamaZ/rsschool-cv')).toBe('MikhamaZ');\n  });\n\n  it('Should return an empty string when wrong github link were passed', () => {\n    expect(filterLogin('https://github.com/')).toBe('');\n    expect(filterLogin('https://github.com')).toBe('');\n    expect(filterLogin('http://github.com')).toBe('');\n    expect(filterLogin('github.com')).toBe('');\n  });\n\n  it('Should trim spaces', () => {\n    expect(filterLogin('Ytniza ')).toBe('Ytniza');\n    expect(filterLogin(' mikhama ')).toBe('mikhama');\n    expect(filterLogin('        mikhama   ')).toBe('mikhama');\n  });\n});\n"
  },
  {
    "path": "client/src/shared/utils/text-utils.ts",
    "content": "const githubIdMatch = '[a-z\\\\d](?:[a-z\\\\d]|-*(?=[a-z\\\\d])){0,38}';\nconst stringStartMatch = '(https?:\\\\/*\\\\/)?github.com(\\\\/)?|https?:\\\\/*\\\\/|^';\n\nconst LOGIN_FIND_REGEXP = new RegExp(`(${stringStartMatch})(${githubIdMatch})?`, 'i');\n\nexport const filterLogin = (login: string) => {\n  const matches = login.trim().match(LOGIN_FIND_REGEXP) || [];\n  const [foundLogin = ''] = matches.reverse();\n\n  return foundLogin;\n};\n"
  },
  {
    "path": "client/src/styles/main.css",
    "content": ":root {\n  --scroll-size: 8px;\n}\n\n:root:has(.dark) {\n  --bg-primary-color: #151515;\n  --text-primary-color: #ffffff;\n  --scroll-color: #434343;\n}\n\n:root:has(.light) {\n  --bg-primary-color: #fff;\n  --text-primary-color: #000;\n  --scroll-color: #bfbfbf;\n}\n\n:root::-webkit-scrollbar,\ntextarea::-webkit-scrollbar {\n  width: var(--scroll-size);\n  height: var(--scroll-size);\n}\n\n:root::-webkit-scrollbar-thumb,\ntextarea::-webkit-scrollbar-thumb {\n  border-radius: calc(var(--scroll-size) / 2);\n  background: var(--scroll-color);\n}\n\n:root::-webkit-scrollbar-track,\ntextarea::-webkit-scrollbar-track {\n  margin: calc(var(--scroll-size) / 2);\n}\n\n.ant-table .ant-table-container .ant-table-body,\n.ant-table .ant-table-container .ant-table-content {\n  scrollbar-width: thin;\n  scrollbar-color: var(--scroll-color) transparent;\n  scrollbar-gutter: stable;\n}\n\n.ant-modal-content {\n  scrollbar-width: thin;\n  scrollbar-color: var(--scroll-color) transparent;\n  scrollbar-gutter: stable;\n}\n\n.ant-drawer-body {\n  scrollbar-width: thin;\n  scrollbar-color: var(--scroll-color) transparent;\n  scrollbar-gutter: stable;\n}\n\ntextarea::-webkit-resizer {\n  border-width: 3px;\n  border-style: solid;\n  border-color: transparent var(--scroll-color) var(--scroll-color) transparent;\n  border-radius: 0 0 4px 0;\n  background: var(--bg-primary-color);\n}\n\n.dark .header-logo,\n.dark .login-image {\n  filter: invert(1);\n}\n\n/* Workaround for the incorrect background color when autofill suggestions are active*/\ninput:-webkit-autofill,\ninput:-webkit-autofill:hover,\ninput:-webkit-autofill:focus,\ninput:-webkit-autofill:active {\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: var(--text-primary-color);\n  transition: background-color 100s ease-in-out 0s;\n}\n\nhtml,\nbody,\n#root {\n  height: 100%;\n  background: var(--bg-primary-color);\n  color: var(--text-primary-color);\n}\n\n.ubuntu-font {\n  font-family: 'Ubuntu', sans-serif;\n}\n\n.rs-link {\n  color: #fff;\n  display: inline-block;\n  padding: 1px 2px;\n}\n\n.ant-layout-sider-dark h4 {\n  color: rgba(255, 255, 255, 0.6);\n  background: #07223b;\n  white-space: nowrap;\n}\n\n.ant-layout-sider-dark h4 span {\n  text-transform: uppercase;\n  font-size: 16px;\n  line-height: 30px;\n  padding: 0 0 0 24px;\n}\n\n.ant-layout-sider-collapsed h4 span {\n  display: none;\n}\n\n.ant-drawer-content-wrapper {\n  max-width: 85%;\n}\n\n.footer {\n  padding: 16px;\n}\n\n.rs-table-row-disabled {\n  opacity: 0.25;\n}\n\n.rs-table-row-cols-disabled td:not(.rs-table-row-cols-disabled td:last-of-type) {\n  opacity: 0.25;\n  border-bottom: 1px solid #cacaca;\n}\n\n.ant-table-tbody .rs-table-row-cols-disabled:hover td:last-of-type {\n  background: rgba(250, 250, 250, 0.25);\n}\n\ndiv.ant-table-column-sorters {\n  padding: 0;\n}\n\nspan.ant-table-filter-column-title {\n  padding-top: 8px;\n  padding-bottom: 8px;\n}\n\nspan.ant-radio + * {\n  display: inline-block;\n  white-space: normal;\n  vertical-align: top;\n}\n\nthead.ant-table-thead > tr > th {\n  font-size: 12px;\n}\n\n.crosscheck-submitted-link:focus {\n  text-decoration: underline;\n}\n\n/* .rc-virtual-list-scrollbar {\n  display: block !important;\n} */\n\n/* table-row-* are used for Schedule Table view - Split by week feature */\n.table-row-current {\n  background-color: #2f8e0017;\n}\n\n.table-row-current-day {\n  background-color: #2f8e002d;\n}\n\n.table-row-odd {\n  background-color: #f8f8f8;\n}\n\n.table-row-done {\n  background-color: #f6ffed;\n}\n\n@media print {\n  .no-print {\n    display: none !important;\n  }\n\n  .print-black {\n    color: black !important;\n    background-color: white !important;\n  }\n\n  .print-no-padding {\n    padding: 0 !important;\n    margin: 0 !important;\n  }\n}\n\n/* CV */\n\n.cv-card-section {\n  break-inside: avoid;\n}\n\n.cv-sidebar {\n  padding: 16px;\n  background-color: #000;\n  color: #fff;\n  break-inside: avoid;\n}\n\n@media print {\n  .cv-sidebar {\n    background-color: #fff;\n    color: #000;\n  }\n}\n"
  },
  {
    "path": "client/src/utils/dynamicWithSkeleton.tsx",
    "content": "import dynamic from 'next/dynamic';\nimport { Skeleton } from 'antd';\nimport { ComponentType } from 'react';\n\nexport const dynamicWithSkeleton = <T extends object>(\n  importFn: () => Promise<ComponentType<T> | { default: ComponentType<T> }>,\n): ComponentType<T> => {\n  return dynamic(importFn, {\n    ssr: false,\n    loading: () => <Skeleton active={true} />,\n  });\n};\n"
  },
  {
    "path": "client/src/utils/optionalQueryString.ts",
    "content": "export function optionalQueryString(value: string | string[] | undefined) {\n  if (Array.isArray(value)) {\n    const trimmedElements = value.map(elem => elem.trim()).join(',');\n    if (trimmedElements !== '') {\n      return trimmedElements;\n    }\n  } else if (typeof value === 'string') {\n    return String(value).trim();\n  }\n}\n"
  },
  {
    "path": "client/src/utils/profilePageUtils.ts",
    "content": "import { CoreJsInterviewFeedback, StudentStats } from '@common/models';\n\nexport const getStudentCoreJSInterviews = (stats?: StudentStats[]) => {\n  if (!stats || stats.length === 0) return;\n  return stats\n    .filter((student: StudentStats) => student.tasks.some(({ interviewFormAnswers }) => interviewFormAnswers))\n    .map(({ tasks, courseFullName, courseName, locationName }) => ({\n      courseFullName,\n      courseName,\n      locationName,\n      interviews: tasks\n        .filter(({ interviewFormAnswers }) => interviewFormAnswers)\n        .map(({ interviewFormAnswers, score, comment, interviewer, name, interviewDate }) => ({\n          score,\n          comment,\n          interviewer,\n          answers: interviewFormAnswers,\n          name,\n          interviewDate,\n        })),\n    })) as CoreJsInterviewFeedback[];\n};\n\nexport const checkIsProfileOwner = (githubId: string, requestedGithubId: string): boolean => {\n  return githubId === requestedGithubId;\n};\n"
  },
  {
    "path": "client/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"emitDecoratorMetadata\": true,\n    \"esModuleInterop\": true,\n    \"experimentalDecorators\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"resolveJsonModule\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"allowJs\": true,\n    \"declaration\": false,\n    \"isolatedModules\": true,\n    \"jsx\": \"react-jsx\",\n    \"target\": \"ES2015\",\n    \"module\": \"CommonJS\",\n    \"moduleResolution\": \"node\",\n    \"noEmit\": true,\n    \"skipLibCheck\": true,\n    \"lib\": [\"dom\", \"es2015\", \"es2016\", \"es2017\"],\n    \"paths\": {\n      \"@common/*\": [\"../common/*\"],\n      \"@client/*\": [\"./src/*\"]\n    },\n    \"incremental\": true,\n    \"types\": [\"vitest/globals\"]\n  },\n  \"include\": [\"./next-env.d.ts\", \"./lambda/**/*.ts\", \"./src/**/*.ts\", \"./src/**/*.tsx\"],\n  \"exclude\": [\n    \"./node_modules\",\n    \"./src/**/*.test.ts\",\n    \"./src/**/*.test.tsx\",\n    \"./src/**/__tests__/**\",\n    \"./src/**/__test__/**\",\n    \"./src/__mocks__/**\"\n  ]\n}\n"
  },
  {
    "path": "client/vitest.config.mts",
    "content": "import path from 'node:path';\nimport { defineConfig, mergeConfig } from 'vitest/config';\nimport shared from '../vitest.shared.mjs';\n\nexport default mergeConfig(\n  shared,\n  defineConfig({\n    resolve: {\n      alias: {\n        '@client/hooks': path.resolve(import.meta.dirname, 'src/__mocks__/hooks'),\n        '@client': path.resolve(import.meta.dirname, 'src'),\n        'next/config': path.resolve(import.meta.dirname, 'src/__mocks__/next/config'),\n      },\n    },\n    test: {\n      environment: 'jsdom',\n      include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],\n      setupFiles: ['src/setupTests.ts'],\n      testTimeout: 30000,\n      env: {\n        TZ: 'UTC',\n      },\n      css: false,\n      deps: {\n        optimizer: {\n          web: {\n            include: [\n              'react-markdown',\n              'vfile',\n              'unist-util-stringify-position',\n              'remark-parse',\n              'remark-rehype',\n              'mdast-util-from-markdown',\n              'mdast-util-to-hast',\n              'unified',\n              'bail',\n              'is-plain-obj',\n              'trough',\n              'micromark',\n              'parse-entities',\n              'character-entities',\n              'property-information',\n              'comma-separated-tokens',\n              'hast-util-whitespace',\n              'space-separated-tokens',\n              'decode-named-character-reference',\n              'ccount',\n              'escape-string-regexp',\n              'markdown-table',\n              'trim-lines',\n            ],\n          },\n        },\n      },\n    },\n  }),\n);\n"
  },
  {
    "path": "common/README.md",
    "content": "# DO NOT USE THIS DIRECTORY\n\n`common/` directory is deprecated and will be removed in the future."
  },
  {
    "path": "common/models/index.ts",
    "content": "export * from './user';\nexport * from './stage-interview-feedback';\nexport * from './interview';\nexport * from './profile';\n"
  },
  {
    "path": "common/models/interview.ts",
    "content": "export enum InterviewStatus {\n  NotCompleted,\n  Completed,\n  Canceled,\n}\n\nexport interface InterviewDetails {\n  id: number;\n  name: string;\n  completed: boolean;\n  status: InterviewStatus;\n  result: string | null;\n  descriptionUrl: string;\n  startDate: string;\n  endDate: string;\n  interviewer: { name: string; githubId: string };\n  student: { name: string; githubId: string };\n}\n\nexport interface InterviewPair {\n  id: number;\n  status: InterviewStatus;\n  result: string | null;\n  interviewer: { name: string; githubId: string };\n  student: { name: string; githubId: string };\n}\n\n/**\n * Question to the student, which represents either theory or practice part\n */\nexport type InterviewQuestion = {\n  id: string;\n  /**\n   * @optional - describes the topic, to which question relates to(ex. `Data structures`)\n   */\n  topic?: string;\n  /**\n   * Actual question title to the student\n   */\n  title: string;\n\n  /**\n   * Stores the answer from the student\n   */\n  value?: number;\n};\n\nexport type InterviewFeedbackValues = Record<string, string[] | string | number | InterviewQuestion[]>;\n\n/**\n * The structure is stored on db level. i.e we store only step id,\n * whether it is completed or not; and the pairs of questionId/answerId and\n * since the questions are dynamic(user can add/remove) we also store the submitted questions(the section of theory & practice are stored in db to persist the selected questions by the interviewer)\n */\nexport type InterviewFeedbackStepData = {\n  isCompleted: boolean;\n  values?: InterviewFeedbackValues;\n};\n"
  },
  {
    "path": "common/models/profile.ts",
    "content": "import { EnglishLevel, InterviewFeedbackValues } from './';\n\nexport interface Location {\n  cityName: string;\n  countryName: string;\n}\n\nexport interface PublicVisibilitySettings {\n  all: boolean;\n}\n\nexport interface PartialStudentVisibilitySettings extends PublicVisibilitySettings {\n  student: boolean;\n}\n\nexport interface ContactsVisibilitySettings extends PublicVisibilitySettings {\n  student: boolean;\n}\n\nexport interface VisibilitySettings extends PublicVisibilitySettings {\n  mentor: boolean;\n  student: boolean;\n}\n\nexport interface ConfigurableProfilePermissions {\n  isProfileVisible?: PublicVisibilitySettings;\n  isAboutVisible?: VisibilitySettings;\n  isEducationVisible?: VisibilitySettings;\n  isEnglishVisible?: PartialStudentVisibilitySettings;\n  isEmailVisible?: ContactsVisibilitySettings;\n  isTelegramVisible?: ContactsVisibilitySettings;\n  isSkypeVisible?: ContactsVisibilitySettings;\n  isPhoneVisible?: ContactsVisibilitySettings;\n  isContactsNotesVisible?: ContactsVisibilitySettings;\n  isLinkedInVisible?: VisibilitySettings;\n  isPublicFeedbackVisible?: VisibilitySettings;\n  isMentorStatsVisible?: VisibilitySettings;\n  isStudentStatsVisible?: PartialStudentVisibilitySettings;\n}\n\nexport interface GeneralInfo {\n  name: string;\n  githubId: string;\n  aboutMyself?: string | null;\n  location: Location;\n  educationHistory?: any | null;\n  englishLevel?: EnglishLevel | null;\n  languages: string[];\n}\n\nexport interface Contacts {\n  phone: string | null;\n  email: string | null;\n  epamEmail: string | null;\n  skype: string | null;\n  telegram: string | null;\n  notes: string | null;\n  linkedIn: string | null;\n  whatsApp: string | null;\n}\n\nexport interface Discord {\n  id: string;\n  username: string;\n  discriminator: string;\n}\n\nexport interface Student {\n  githubId: string;\n  name: string;\n  isExpelled: boolean;\n  totalScore: number;\n  repoUrl?: string;\n}\n\nexport interface MentorStats {\n  courseLocationName: string;\n  courseName: string;\n  students?: Student[];\n}\n\nexport interface StudentStats {\n  courseId: number;\n  courseName: string;\n  locationName: string;\n  courseFullName: string;\n  isExpelled: boolean;\n  isSelfExpelled: boolean;\n  expellingReason?: string;\n  certificateId: string | null;\n  isCourseCompleted: boolean;\n  totalScore: number;\n  rank: number | null;\n  mentor: {\n    githubId: string;\n    name: string;\n  };\n  tasks: StudentTasksDetail[];\n}\n\nexport interface StudentTasksDetail {\n  maxScore: number;\n  scoreWeight: number;\n  name: string;\n  descriptionUri: string;\n  githubPrUri: string;\n  score: number;\n  comment: string;\n  interviewDate?: string;\n  interviewer?: {\n    name: string;\n    githubId: string;\n  };\n  interviewFormAnswers?: {\n    questionId: string;\n    questionText: string;\n    answer?: boolean;\n  }[];\n}\n\nexport interface PublicFeedback {\n  feedbackDate: string;\n  badgeId: string;\n  comment: string;\n  fromUser: {\n    name: string;\n    githubId: string;\n  };\n}\n\nexport interface CoreJsInterviewFeedback {\n  courseFullName: string;\n  courseName: string;\n  locationName: string | null;\n  interviews: {\n    answers: {\n      answer?: boolean;\n      questionText: string;\n      questionId: string;\n    }[];\n    interviewer: {\n      name: string;\n      githubId: string;\n    };\n    comment: string;\n    score: number;\n    name: string;\n    interviewDate?: string;\n  }[];\n}\n\nexport interface StageInterviewDetailedFeedback {\n  decision: string;\n  isGoodCandidate: boolean;\n  courseName: string;\n  courseFullName: string;\n  score: number;\n  maxScore: number;\n  date: string;\n  version: number;\n  interviewer: {\n    name: string;\n    githubId: string;\n  };\n  // This type have to updated to refer to `InterviewFeedbackStepData`, when profile is migrated to nestjs\n  feedback:\n    | LegacyFeedback\n    | {\n        steps: Record<\n          string,\n          {\n            isCompleted: boolean;\n            values?: InterviewFeedbackValues;\n          }\n        >;\n      };\n}\n\nexport interface UserInfo {\n  generalInfo: GeneralInfo;\n  contacts?: Contacts;\n  discord: Discord | null;\n}\n\nexport type LegacyFeedback = {\n  english?: EnglishLevel;\n  comment: string;\n  programmingTask: {\n    task: string;\n    codeWritingLevel: number;\n    resolved: number;\n    comment: string;\n  };\n  skills: {\n    htmlCss: number;\n    common: number;\n    dataStructures: number;\n  };\n};\n"
  },
  {
    "path": "common/models/stage-interview-feedback.ts",
    "content": "interface StageInterviewFeedback {\n  common: {\n    reason: 'haveITEducation' | 'doNotWorkInIT' | 'whatThisCourseAbout' | 'other' | null;\n    reasonOther: string | null;\n    whenStartCoding: number | null;\n    schoolChallengesParticipaton: string | null;\n    whereStudied: string | null;\n    workExperience: string | null;\n    otherAchievements: string | null;\n    militaryService: 'served' | 'liable' | 'notLiable' | null;\n  };\n  skills?: {\n    [index: string]: any;\n    htmlCss: {\n      level: number | null;\n    };\n    dataStructures: {\n      array: number | null;\n      list: number | null;\n      stack: number | null;\n      queue: number | null;\n      tree: number | null;\n      hashTable: number | null;\n      heap: number | null;\n    };\n    common: {\n      binaryNumber: number | null;\n      oop: number | null;\n      bigONotation: number | null;\n      sortingAndSearchAlgorithms: number | null;\n    };\n    comment: string | null;\n  };\n  programmingTask: {\n    task: string | null;\n    codeWritingLevel: number | null;\n    resolved: number | null;\n    comment: string | null;\n  };\n  resume: {\n    verdict: StageInterviewFeedbackVerdict;\n    comment: string | null;\n    score: number;\n  };\n}\n\nexport type StageInterviewFeedbackVerdict = 'yes' | 'no' | 'noButGoodCandidate' | 'didNotDecideYet' | null;\n\nexport type EnglishLevel = 'a0' | 'a1' | 'a1+' | 'a2' | 'a2+' | 'b1' | 'b1+' | 'b2' | 'b2+' | 'c1' | 'c1+' | 'c2';\n\nexport interface StageInterviewFeedbackJson extends StageInterviewFeedback {\n  english: {\n    levelStudentOpinion: EnglishLevel | null;\n    levelMentorOpinion: EnglishLevel | null;\n    whereAndWhenLearned: string | null;\n    comment: string | null;\n  };\n}\n"
  },
  {
    "path": "common/models/user.ts",
    "content": "import { Discord } from './profile';\n\nexport interface UserBasic {\n  id: number;\n  githubId: string;\n  name: string;\n}\n\nexport interface MentorBasic extends UserBasic {\n  isActive: boolean;\n  cityName: string;\n  countryName: string;\n  students: (StudentBasic | { id: number })[];\n}\n\nexport interface StudentBasic extends UserBasic {\n  isActive: boolean;\n  cityName?: string | null;\n  countryName?: string | null;\n  mentor:\n    | MentorBasic\n    | { id: number }\n    | {\n        id: number;\n        githubId: string;\n        name: string;\n      }\n    | null;\n  discord: Discord | null;\n  totalScore: number;\n  rank?: number;\n}\n\nexport interface InterviewStatistics {\n  completed?: number;\n  total?: number;\n}\n\nexport interface MentorDetails extends MentorBasic {\n  cityName: string;\n  countryName: string;\n  maxStudentsLimit: number;\n  studentsPreference: 'any' | 'city' | 'country';\n  interviews?: InterviewStatistics;\n  screenings?: InterviewStatistics;\n  studentsCount?: number;\n  taskResultsStats?: {\n    lastUpdatedDate?: Date | string | null;\n    total: number;\n    checked: number;\n  };\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\nservices:\n  client:\n    container_name: client\n    image: ghcr.io/rolling-scopes/rsschool-app-client:master\n    ports:\n      - 8081:8080\n    environment:\n      RS_HOST: ${RS_HOST}\n      RSSHCOOL_UI_GCP_MAPS_API_KEY: ${RSSHCOOL_UI_GCP_MAPS_API_KEY}\n    restart: on-failure\n    depends_on:\n      - server\n      - nestjs\n    networks:\n      - shared-network\n\n  server:\n    container_name: server\n    image: ghcr.io/rolling-scopes/rsschool-app-server:master\n    ports:\n      - 8080:8080\n    environment:\n      RSSHCOOL_API_AUTH_CALLBACK: ${RSSHCOOL_API_AUTH_CALLBACK}\n      RSSHCOOL_API_AUTH_CLIENT_ID: ${RSSHCOOL_API_AUTH_CLIENT_ID}\n      RSSHCOOL_API_AUTH_CLIENT_SECRET: ${RSSHCOOL_API_AUTH_CLIENT_SECRET}\n      RSSHCOOL_API_AUTH_SUCCESS_REDIRECT: ${RSSHCOOL_API_AUTH_SUCCESS_REDIRECT}\n      RSSHCOOL_API_SESSION_KEY: ${RSSHCOOL_API_SESSION_KEY}\n      RSSHCOOL_PG_HOST: ${RSSHCOOL_PG_HOST}\n      RSSHCOOL_PG_USERNAME: ${RSSHCOOL_PG_USERNAME}\n      RSSHCOOL_PG_PASSWORD: ${RSSHCOOL_PG_PASSWORD}\n      RSSHCOOL_PG_DATABASE: ${RSSHCOOL_PG_DATABASE}\n      RSSHCOOL_API_ADMIN_USERNAME: ${RSSHCOOL_API_ADMIN_USERNAME}\n      RSSHCOOL_API_ADMIN_PASSWORD: ${RSSHCOOL_API_ADMIN_PASSWORD}\n      RSSHCOOL_API_AWS_SECRET_ACCESS_KEY: ${RSSHCOOL_API_AWS_SECRET_ACCESS_KEY}\n      RSSHCOOL_API_AWS_ACCESS_KEY_ID: ${RSSHCOOL_API_AWS_ACCESS_KEY_ID}\n      RSSHCOOL_API_AWS_REGION: ${RSSHCOOL_AWS_REGION}\n      RSSHCOOL_API_AWS_REST_API_URL: ${RSSHCOOL_AWS_REST_API_URL}\n      RSSHCOOL_API_AWS_REST_API_KEY: ${RSSHCOOL_AWS_REST_API_KEY}\n      RSSHCOOL_API_GITHUB_APP_ID: ${RSSHCOOL_API_GITHUB_APP_ID}\n      RSSHCOOL_API_GITHUB_APP_INSTALL_ID: ${RSSHCOOL_API_GITHUB_APP_INSTALL_ID}\n      RSSHCOOL_API_GITHUB_PRIVATE_KEY: ${RSSHCOOL_API_GITHUB_PRIVATE_KEY}\n      RSSHCOOL_API_USERS_CLOUD_USERNAME: ${RSSHCOOL_USERS_CLOUD_USERNAME}\n      RSSHCOOL_API_USERS_CLOUD_PASSWORD: ${RSSHCOOL_USERS_CLOUD_PASSWORD}\n      RSSHCOOL_HOST: https://app.rs.school\n    restart: on-failure\n    networks:\n      - shared-network\n\n  nestjs:\n    container_name: nestjs\n    image: ghcr.io/rolling-scopes/rsschool-app-nestjs:master\n    ports:\n      - 8082:8080\n    environment:\n      RSSHCOOL_HOST: ${RS_HOST}\n      RSSHCOOL_PG_HOST: ${RSSHCOOL_PG_HOST}\n      RSSHCOOL_PG_USERNAME: ${RSSHCOOL_PG_USERNAME}\n      RSSHCOOL_PG_PASSWORD: ${RSSHCOOL_PG_PASSWORD}\n      RSSHCOOL_PG_DATABASE: ${RSSHCOOL_PG_DATABASE}\n      RSSHCOOL_USERS_ADMINS: 'apalchys,dzmitry-varabei,mikhama,sonejka,aaliakseyenka,anik188,alreadybored'\n      RSSHCOOL_USERS_HIRERS: ${RSSHCOOL_USERS_HIRERS}\n      RSSHCOOL_AUTH_GITHUB_CALLBACK: ${RSSHCOOL_API_AUTH_CALLBACK}\n      RSSHCOOL_AUTH_GITHUB_CLIENT_ID: ${RSSHCOOL_API_AUTH_CLIENT_ID}\n      RSSHCOOL_AUTH_GITHUB_CLIENT_SECRET: ${RSSHCOOL_API_AUTH_CLIENT_SECRET}\n      RSSHCOOL_AUTH_GITHUB_WEBHOOK_ACTIVITY_SECRET: ${RSSHCOOL_AUTH_GITHUB_WEBHOOK_ACTIVITY_SECRET}\n      RSSHCOOL_AUTH_JWT_SECRET_KEY: ${RSSHCOOL_API_SESSION_KEY}\n      RSSHCOOL_AUTH_GITHUB_INTEGRATION_SITE_TOKEN: ${RSSHCOOL_AUTH_GITHUB_INTEGRATION_SITE_TOKEN}\n      RSSHCOOL_AWS_SECRET_ACCESS_KEY: ${RSSHCOOL_API_AWS_SECRET_ACCESS_KEY}\n      RSSHCOOL_AWS_ACCESS_KEY_ID: ${RSSHCOOL_API_AWS_ACCESS_KEY_ID}\n      RSSHCOOL_AWS_REGION: ${RSSHCOOL_AWS_REGION}\n      RSSHCOOL_AWS_REST_API_URL: ${RSSHCOOL_AWS_REST_API_URL}\n      RSSHCOOL_AWS_REST_API_KEY: ${RSSHCOOL_AWS_REST_API_KEY}\n      RSSHCOOL_USERS_CLOUD_USERNAME: ${RSSHCOOL_USERS_CLOUD_USERNAME}\n      RSSHCOOL_USERS_CLOUD_PASSWORD: ${RSSHCOOL_USERS_CLOUD_PASSWORD}\n      RSSHCOOL_OPENAI_API_KEY: ${RSSHCOOL_OPENAI_API_KEY}\n      SENTRY_DSN: ${SENTRY_DSN}\n    restart: on-failure\n    networks:\n      - shared-network\n    depends_on:\n      - server\n    logging:\n      driver: awslogs\n      options:\n        awslogs-region: eu-central-1\n        awslogs-group: rsschool-app\n        awslogs-stream: nestjs\n        awslogs-create-group: 'true'\n\n  nginx:\n    container_name: nginx\n    image: nginx:1.21-alpine\n    restart: unless-stopped\n    volumes:\n      - ./nginx:/etc/nginx\n      - ./letsencrypt:/etc/letsencrypt\n      - ./certbot:/var/www/certbot\n    command: '/bin/sh -c ''while :; do sleep 12h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'''\n    ports:\n      - 80:8080\n      - 443:443\n    networks:\n      - shared-network\n    depends_on:\n      - client\n      - server\n      - nestjs\n\n  certbot:\n    container_name: certbot\n    image: certbot/certbot\n    restart: unless-stopped\n    volumes:\n      - ./letsencrypt:/etc/letsencrypt\n      - ./certbot:/var/www/certbot\n    entrypoint: \"/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'\"\n    depends_on:\n      - nginx\n\nnetworks:\n  shared-network:\n"
  },
  {
    "path": "docs/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/CNAME",
    "content": "docs.app.rs.school"
  },
  {
    "path": "docs/README.md",
    "content": "# RS School App docs\n\nWelcome to the [RS School App](https://app.rs.school) docs!\n\nYou can read about `RS School App` features by clicking the links in the menu below or on the left side of the page. Search is available in the top left corner of the page.\n\n## Features\n\n- [CV](./features/cv.md)\n- [Choose kata languages](./features/choose-kata-languages.md)\n- [Cross-Check Scheduling](./features/cross-check-scheduling.md)\n\n## RS School App Platform\n\n- [About](./platform/about.md)\n- [Schedule](./platform/shedule.md)\n- [Submission of tasks in the RS School App](./platform/tasks.md)\n- [Cross-check](./platform/cross-check-flow.md)\n- [Notifications](./platform/notifications.md)\n- [The allocation of students into teams](./platform/team-allocation.md)\n- [Typical problems](./platform/typical-problems.md)\n- [Adding tests into RS School App](./platform/adding-tests.md)"
  },
  {
    "path": "docs/_sidebar.md",
    "content": "- ## [Main docs page](README.md)\n\n- RS School App Basics\n  - [About](platform/about.md)\n  - [Schedule](platform/shedule.md)\n  - [Submission of tasks in the RS School App](platform/tasks.md)\n  - [Cross-check](platform/cross-check-flow.md)\n  - [Notifications](platform/notifications.md)\n  - [CV](platform/cv.md)\n  - [Typical problems](platform/typical-problems.md)\n- Mentoring\n  - [Pull Request Review Process](platform/pull-request-review-process.md)\n- [Code of conduct](code-of-conduct.md)\n- RS School App Admin\n  - [Adding tests into RS School App](platform/adding-tests.md)\n  - [Choose kata languages](platform/choose-kata-languages.md)\n  - [Cross-Check Scheduling](platform/cross-check-scheduling.md)"
  },
  {
    "path": "docs/code-of-conduct.md",
    "content": "## Code of conduct\n\nThe Rolling Scopes School believes our community should be open for everyone.\n\nWe are expecting cooperation from all participants to help ensuring a friendly, safe, and welcoming environment for everybody, regardless of:\n\n- gender\n- sexual orientation\n- disability\n- ethnicity\n- religion\n- politic (war, elections, sanctions and so on)\n- preferred operating system\n- programming language\n- text editor\n\nWe expect all faculty, teachers, employees, mentors, students, guests, friends, basically everyone involved with the school, to help us create a safe, constructive and positive environment for everyone.\n\nLet’s build a place where we can achieve more together than we could ever achieve alone.\n\nWe expect everyone to:\n\n- Be considerate, respectful, and collaborative.\n- Refrain from demeaning, discriminatory, toxic or harassing behavior and speech.\n- Create a positive impact on everything around them.\n- Participate in an authentic and active way.\n\nBut if you need a more explicit list: Unacceptable behaviors include harassing, abusive, discriminatory, or derogatory conduct.\n\nHarassment includes:\n\n- offensive comments related to gender, sexual orientation, race, religion, disability\n- deliberate intimidation, stalking, or following\n- harassing photography or recording\n- sustained disruption of talks, topic channel conversations or events\n- unwelcome sexual attention\n\nThe toxic behavour includes:\n\n- unreasonable repetitive critic of the school, its program, mentors and students\n- rude language outside of random channel\n- unreasonable conspiracy theory spreaded on school channels\n\nAdditionally:\n\n- It is forbidden in any form to justify military aggression towards a peaceful state\n- It is forbidden to blame people for their indifference to a political conflict/crisis\n- It is forbidden to call other people to active illegal actions\n\nAny student who violates the Code of Conduct will be dismissed from Rolling Scopes School.\n\nBased and inspired by [code of conduct (flatironschool.com)](https://www.flatironschool.com/code-of-conduct/) and [code of conduct (rsconf.by)](https://rsconf.by/code-of-conduct.html)"
  },
  {
    "path": "docs/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Document</title>\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n    <meta name=\"description\" content=\"Description\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, minimum-scale=1.0\" />\n    <link rel=\"stylesheet\" href=\"//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css\" />\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script>\n      window.$docsify = {\n        repo: 'https://github.com/rolling-scopes/rsschool-app/tree/master/docs',\n        loadSidebar: true,\n        relativePath: false,\n        subMaxLevel: 0,\n        name: '',\n        repo: '',\n        search: {\n          maxAge: 46400000,\n          paths: 'auto',\n          placeholder: 'Search',\n          noData: 'Not found',\n          depth: 6,\n          hideOtherSidebarContent: false,\n        },\n      };\n    </script>\n    <!-- Docsify v4 -->\n    <script src=\"//cdn.jsdelivr.net/npm/docsify@4\"></script>\n    <script src=\"//unpkg.com/docsify/lib/plugins/search.min.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "docs/platform/about.md",
    "content": "## RS School App\n\nThe `RS School App` is an open-source platform used in the learning process at RS School and developed by community activists.\n\nLinks:\n\n- <span style=\"color:green_apple\">[app.rs.school](https://app.rs.school/)</span> - school training platform\n- <span style=\"color:green_apple\">[rs school app repository](https://github.com/rolling-scopes/rsschool-app)</span> - school training platform\n- <span style=\"color:green_apple\">[docs.rs.school](https://docs.rs.school/)</span> - school documentation\n- <span style=\"color:green_apple\">[documentation repository](https://github.com/rolling-scopes-school/docs)</span>\n\n## Main panels of the RS School App:\n\n- **Dashboard** - a panel for tracking the key performance indicators of student's learning\n- **Score** - a full list of students enrolled in the course, with the total sum of points scored. The list updates daily at 04:00 am Moscow time/Minsk\n- **Schedule** - course schedule, as well as task start dates, deadlines and links to task descriptions\n- **Cross-Check: Submit** - a section for submitting links to solutions that are checked using cross-check\n- **Cross-Check: Review** - a section with solutions that the student must check during cross-check\n- **Interviews** - list of scheduled mentor interviews and their results\n- **Auto-Test** - a section for the automatic checking of tasks (tests, algorithmic tasks and codewars tasks)\n- **Gratitude** (<span style=\"color:green_apple\">https://app.rs.school/gratitude</span>) — a section for expressing gratitude. Have you been helped with solving a problem, finding a bug, or just checking your work qualitatively? Feel free to write thanks!\n- **Heroes page** (<span style=\"color:green_apple\">https://app.rs.school/heroes</span>) — a list of students who are trying not only to gain knowledge, but also ready to share it! Thanks from the Gratitude page are here.\n\n## Q&A\n\n### How to enroll in the course in the RS School App?\n\nYou can sign up for the upcoming courses by following <span style=\"color:green_apple\">[the link](https://app.rs.school/registry/student)</span>.\n\n### How to understand that I have enrolled in the course successfully??\n\nGo to <span style=\"color:green_apple\">[RS School App](https://app.rs.school/)</span> and check that the course is displayed in the course list. Then, open the score tab.\nIn the header of the table after the inscription GitHub, click on the magnifying glass icon, enter your nickname on GitHub and press 'Enter'\nIf the nickname is found in the list of students, then you have successfully registered for the course. Congrats!\n\n### How do I know if it is possible to register for a course that has already started?\n\nIf the desired course is available in the list of courses <span style=\"color:green_apple\">[on the registration page](https://app.rs.school/registry/student)</span>, you can still enroll in it.\n\n### How to leave the course in the RS School App?\n\nTo leave the course, click the \"Leave course\" button in <span style=\"color:green_apple\">[your profile](https://app.rs.school/profile)</span>, in the \"Student Statistics\" section.\nThe course remains in the list, but you will not be able to complete tasks and track statistics.\n**Attention!** Clicking the \"Leave course\" button excludes the student from the course. You can re-enroll by clicking on the \"Back to Course\" button.\n\n#### My progress in the <span style=\"color:orange\">`Total`</span> field on the Score page after passing the test has not been updated, what should I do?\n\n<span style=\"color:orange\">'Total'</span> is updated once a day at 04:00 GMT+3\n\n## Overview\n\n<span style=\"color:green_apple\">[Webinar's record \"How to study in RS School App\" ](https://www.youtube.com/watch?v=v_69DaeZ7dM&feature=youtu.be)</span> - 1 hour\n\n## A unique opportunity to improve your karma right now\n\n1. Go to <span style=\"color:green_apple\">[RS School App repository](https://github.com/rolling-scopes/rsschool-app)</span>\n2. Click **:star: Star**\n3. Congrats! Your RS School karma has improved :innocent:"
  },
  {
    "path": "docs/platform/adding-tests.md",
    "content": "## Adding tests into the RS School App (for trainers and admins only)\n\n1. To add tests, enter the \"Manage Tasks\" page:\n   </br>![Manage Tasks Page](./img/adding-tests-1.png)\n\n2. Then click the \"Add Task\" button:\n   </br>![Add Task Button1](./img/adding-tests-2.png)\n\n3. Fill in the form and save it:\n   - \"Name\" - task title;\n   - \"Task Type\" - select \"RS School App Test\";\n   - \"Discipline\" - select course-related discipline;\n   - \"Tags\" - set tags (optional) to highlight which aspect of the course it is related to. For example, \"javascript\", \"html/css\", etc.;\n   - \"Description URL\" - link to the task description;\n   - \"JSON Attributes\" - copy the JSON file's content with test settings, questions and possible answers (<span style=\"color:green_apple\">[see below](https://docs.app.rs.school/#/platform/adding-tests?id=json-attributes)</span>).\n\n   </br>![RS School App Test Form](./img/adding-tests-3.png)\n\n4. In the course menu, select \"Course Tasks\":\n   </br>![Course Tasks](./img/adding-tests-4.png)\n\n5. Click the \"Add Task\" button too:\n   </br>![Add Task Button2](./img/adding-tests-5.png)\n\n6. Fill in the form again and save it:\n   - \"Task\" - select the task you have just created;\n   - \"Task Type\" - select \"RS School App Test\";\n   - \"Checker\" - select \"Auto-Test\";\n   - \"Task Owner\" - select the person who is responsible for this task;\n   - \"Start Date - End Date\" - set a date of issuing the task and the deadline;\n   - \"Score\" - set a maximum score a student can get for this task;\n   - \"Score Weight\" - task weight (1 by default). It is necessary for the manager of the course to correct it later, so do not change it.\n     </br>![Adding Task Details](./img/adding-tests-6.png)\n\n7. The task is added! Go back to the course menu and select \"Auto-Test\" in order to check if the task has been added or not.\n   </br>![Auto-Test Menu](./img/adding-tests-7.png)\n8. Select the test you have created in the drop-down list and check if anything is wrong:\n   </br>![Auto-Test Test Course](./img/adding-tests-8.png)\n\n### JSON Attributes\n\n#### Parameters:\n\n- \"strictAttemptsMode\" - strict mode by default, set \"false\" to switch off strict mode.\n- \"maxAttemptsNumber\" - in strict mode, after a certain amount of submissions, a student gets 0; otherwise, a student can submit it as many times as possible, but their final score will be divided by 2 after the max attempts have been exceeded.\n- \"numberOfQuestions\" - number of questions (it must be equal to the length of arrays of \"answers\" and \"public\".\"questions\").\n- \"tresholdPercentage\" - the percentage a student needs to reach for the task to be accepted (otherwise they get 0 and loses 1 trial).\n- \"oneAttemptPerNumberOfHours\" - if the number is positive integer, then submission will be restricted by 1 attempt per amount of the given number of hours, 0 is used by default and means that this option is disabled.\n- \"public\".\"questions\" - questions and answers. To add a multiple choice question, set \"true\" for \"multiple\".\n- \"answers\" - an array of numbers of correct answers (the order of numbers must be equal to the order in \"public\".\"questions\"!).\n\nNB! student's resubmission implies they get the last score obtained; it is possible to get 0, finally.\n\n#### An Example:\n\n    {\n        \"public\": {\n            \"tresholdPercentage\": 70,\n            \"numberOfQuestions\": 10,\n            \"maxAttemptsNumber\": 2,\n            \"strictAttemptsMode\": false,\n            \"questions\": [\n                {\n                    \"question\": \"Do you like html?\",\n                    \"answers\": [\"Yes\", \"No\", \"Maybe\"],\n                    \"multiple\": false\n                },\n                {\n                    \"question\": \"What is css?\",\n                    \"answers\": [\"Cascade style sheets\", \"cosmic super solutions\", \"cool super stuff\"],\n                    \"multiple\": true\n                },\n                {\n                    \"question\": \"What is not a css selector?\",\n                    \"answers\": [\"<div>\", \"div\", \"a.a\", \"p<div>\"],\n                    \"multiple\": true\n                }\n            ]\n        },\n        \"answers\": [\n            [0],\n            [0, 2],\n            [0, 3]\n        ]\n    }\n\n#### PRs for test functionality with added descriptions and examples\n\n- <span style=\"color:green_apple\">[Basic example (see above)](https://github.com/rolling-scopes/rsschool-app/pull/530)</span>\n- <span style=\"color:green_apple\">[If you need pictures for tests](https://github.com/rolling-scopes/rsschool-app/pull/798)</span>\n- <span style=\"color:green_apple\">[If you need to restrict attempts per number of hours](https://github.com/rolling-scopes/rsschool-app/issues/1053)</span>"
  },
  {
    "path": "docs/platform/choose-kata-languages.md",
    "content": "# Choose kata language\n\nA small feature that provides an additional option to the `codewars` tasks.\nTo count only katas that were solved in a specific language, the `kataLanguages` ​​property containing an `array` of allowed languages ​​must be passed to the `JSON attributes` of the task.\nYou can configure `JSON attributes` to the settings of a specific task.\n\n![Task settings](./img/choose-kata-languages/task-settings.JPG)\n\nFor the task with `JSON attributes` ​​from the example below, only solutions in `Python` and `Java` will be counted.\n\n![Task settings](./img/choose-kata-languages/task-json.JPG)\n\nIf `kataLanguages` ​​is not passed, then kata solved in `Javascript` or `Typescript` are counted."
  },
  {
    "path": "docs/platform/cross-check-flow.md",
    "content": "## Cross-check\n\nCross-check is a peer-review of tasks by students according to given criteria.\n\n## Process\n\n### Step #1 Sending task to cross-check\n\nTo send the task to cross-check, enter <span style=\"color:green_apple\">[app.rs.school](https://app.rs.school/)</span> and follow the link <span style=\"color:orange\">`Cross-Check: Submit`</span>. Depending on the task requirements, show either a link to the working and deployed website or a link to the original code/GitHub repository. It is possible to submit the link several times - only the last one is saved. All who fail to meet the deadline and do not provide their link on time get 0 points for that task. Deadlines are shown in the schedule.\n\n### Step #2 Generating peers\n\nAfter the assignment's deadline, peers for cross-checking are generated. Every student has to check 4 other students' work.\n\n### Step #3 Checking others' work\n\nAll who send their assignment for cross-checking check each others' work. To do so, enter <span style=\"color:green_apple\">[app.rs.school](https://app.rs.school/)</span> and follow the link <span style=\"color:orange\">`Cross-Check: Review`</span>.  \n**NB!** You won't be able to submit a review after the cross-check is completed, keep an eye on the deadlines!  \nHow to check:\n\n1. Select the task (For example, Portfolio).\n2. Select the student.\n3. After selecting the student, the link to their assignment will appear. If the link is not working, please contact the student and ask for a working link.\n4. Check the assignment according to the assessment criteria of the task.\n5. Enter the mark in the form.\n6. Leave a comment, explaining the mark given.\n7. Send the form.\n8. Every student checks 4 other students' assignments.\n\n### Step #4 Receiving the score and the comment\n\nThe cross-check score is an average of all marks given by reviewers. If the number of students who checked the assignment is 4, the lowest mark is tossed away, and the average of the remaining three marks is calculated as the overall score. If fewer than 4 students checked the assignmnet, all scores contribute to its final score. If you **do not check all 4 assignments before the deadline, your own assignment gets a score of 0**.\n\n## Quality of cross-checking\n\nThe goal of the cross-check is not only about checking others' work and giving them marks, but also learning for yourself and giving useful feedback to your peers. While checking, try to stick to the following rules:\n\n- if the work you are checking is of high-quality, let the author know about it;\n- if you think the work you are checking deserves others' attention, add the work's link to the form of the best works. If your own work seems to have been performed at a very high level, you can add it to the form too.\n- if you see any drawbacks in the work you are checking, do not simply list them. Try to help the author avoid such mistakes in the future by telling them how to correct them.\n- if the reviewer has left you detailed and useful feedback with tips on how to improve your work, you can say thanks via <span style=\"color:green_apple\">[rs app](https://app.rs.school/gratitude)</span>. NB: show your gratitude, not for high marks, but for valuable comments and tips.\n- The most useful comments can be shared in discord in the #crosscheck channel (the reviewer's contacts must be hidden)\n\n## Marking approach\n\n- if you think the assessment criteria are fully satisfied, mark it as 100%\n- If you think there are considerable deviations from what has been asked and it definitely cannot be treated as a feature, mark it as 50%\n- If the criteria are not fulfilled, mark it as 0\n- If the criteria imply an interconnected score, mark it either 100% or 0\n- If the deviations are not critical, mark them for the student's benefit\n- **If the score you are giving is not the highest possible, be polite and**\n  - leave a detailed comment on why the score is reduced\n  - leave any contacts (Discord, Telegram, etc.) so that the reviewer could change the mark\n  - you may always set a new score before the deadline with a comment on why you have changed it\n- If you think the work is copied, you need to:\n  - inform the moderator of the course (with a private message)\n  - mark it as 0\n\nPlease be kind in conversations. If any inappropriate correspondence is noticed (aggression, obscenities, insults, sexism, discrimination, etc.), regardless of whether the score is biased or unfair, the administration will take strict measures, up to expulsion from the course, provided that screenshots of the correspondence are available.\n\n## Responsibility for commenting on reduced scores\n\nThis is about comments such as:\n\n1. \"good job, here is 50 out of 100\"\n2. \".....................................\"\n3. \"just many spaces\" etc.\n\nAdministrators can check the reasons for the score reduction during the appeal (administrators can see names of anonymous reviewers). Cross-checkers who leave comments like the ones shown above will receive:\n\n1. A warning in the #moderation channel - for the first time.\n2. Expulsion from the course - for the second time.\n\n## Communication rules in the Discord cross-check channel:\n\nIf you think the score given to you by the reviewer is wrong, and you want to make sure if it is fair or not, you can do so in the #cross-check channel.\n\nYour message must contain:\n\n1. A link to your work.\n2. Your self-assessment regarding controversial points.\n3. Reviewer's mark and feedback.\n\nThese might help other students in the chat to help you notice your mistakes in case they exist, or agree with your point of view.\n\nMessages in the channel #cross-check must be directed towards the discussion of the work (especially the task's bullet points), but not towards the reviewer or reviewee.\n\n## Appeal\n\n- takes place if there are any activists, who want to drive the process.\n- takes place if you DIDN'T make changes to your work after the deadline.\n- takes place after all checks of your work and step #4 completion (see above).\n- takes place if your expected score and obtained average score (step #4) differ by 10 or more percent.\n- the score can be increased as well as reduced without any further discussion.\n- in case the difference between the score after the appeal and the score obtained in step #4 is less than 10%, the final score will not be increased.\n\nTo appeal:\n\n1. Wait for all the checks of your work and completion of step #4 (see above). Check if all criteria are met in order to appeal (see above).\n2. Create a new issue <span style=\"color:green_apple\">[github.com/rolling-scopes-school/support/issues](https://github.com/rolling-scopes-school/support/issues)</span>. Call it by the following template: Cross-Check 'task title' - 'Your GitHub name'.\n3. Fill in the issue with the following template (if the issue has a title, but is not filled in correctly, it will not be reviewed):\n\n- A link to your deployed project.\n- A link to the project repository on GitHub.\n- A link to the task.\n- A link to the assessment form (if there is any).\n- A screenshot of cross-checking marks (reviewers' contacts must be hidden).\n- A final score after self-assessment, with comments.\n- A cross-check score of your Score.\n\n## Appeal process\n\n- The process starts after the cross-check deadline.\n- There is a guide-mark \"help wanted\" for a valid issue (Labels -> \"help wanted\").\n- Free activist chooses the issue - <span style=\"color:green_apple\">[https://github.com/rolling-scopes-school/support/issues](https://github.com/rolling-scopes-school/support/issues)</span>.\n- The activist sets a guide-mark \"review started\" (Labels -> \"review started\").\n- The activist checks the work following the assessment criteria of the task. Leaves his/her comments and mark. All see it; everything is transparent.\n- The activist sets a guide-mark \"review completed\" (Labels -> \"review completed\").\n- The activist notes the trainer (who assigned the task) in the comments of the issue.\n- The trainer submits the score to the RS APP.\n- The new score replaces the current score.\n\n## Common decencies\n\nAfter the appeal process, thank the activist for their work and time. It is desirable if you use RS App Gratitude (<span style=\"color:green_apple\">[https://app.rs.school/gratitude](https://app.rs.school/gratitude)</span>)\n\n## Why Cross-check?\n\n### Advantages\n\n- checking all students' works before the deadline\n- 100% understanding of how many people completed the task\n- 95% of students accurately check assignments.\n  Responsible students leave valuable comments.\n- an excellent way of checking layouts\n- cross-check allows students to compare their work with others', and understand at what level they are in comparison with other students\n- there is a possibility to see how different students approach the same problem\n- task assessment teaches students to read technical requirements of the tasks accurately\n- students learn to assess tasks, as many of them come back to become mentors in RS School\n- there is a possibility to look at the task from the user's point of view, learn something new about the app (e.g., some behavior that you haven't noticed while making the app)\n- students are more responsible for reading the task requirements as they know that other students will check their work too\n- practical skills of testing the app, which students can use in their projects in the future.\n\n### Disadvantages\n\n- assessments are not always fair\n- an absence of the opportunity to contact the reviewer and discuss disputable moments\n- not all are responsible while checking. Some write several lines, while others several pages of feedback\n- a lot of time spent on checking\n- need exact assessment criteria\n\n## Thanks to the contributors of this page:\n\n- <span style=\"color:green_apple\">[https://github.com/irinainina](https://github.com/irinainina)</span>\n- <span style=\"color:green_apple\">[https://github.com/yuliaHope](https://github.com/yuliaHope)</span>"
  },
  {
    "path": "docs/platform/cross-check-scheduling.md",
    "content": "# Cross Check Scheduling\n\nThis functionality allows you to automatically **distribute** a Cross-Check and **complete** it. The distribution buttons for manual Cross-Check distribution and completion are also still available.\n\nThe functionality for starting and ending a Cross-Check complements the existing one.\nNow Cross-Check starts **automatically after the deadline date**. The crosscheck completion date is set when a `CourseTask` is created or when it is edited from the menu on the `course/admin/tasks?course={courseAlias}` page.\n\n![CourseTask Modal](./img/cross-check-scheduling/course-task-modal.JPG)\n\nThe end date of the Cross-Check is set up to a day.\n\nFor example, setting the date to **03/20/2022** means that the Cross-Check deadline is **23:59 03/20/2022 +00 UTC**.\n\nEveryday Cross-Check functionality at the beginning of the day starts 2 jobs:\n\n- Finds tasks that have passed the submission deadline and that have not yet been distributed and distributes them.\n- Finds tasks that have already been distributed and whose Cross-Check deadline has ended and completes the Cross-Check.\n\nTo track the status of a task, the CourseTask entity has been extended with 2 new properties:\n\n- `crossCheckStatus` - displays the current status of the task. Includes the following options:\n  - **initial** - task submission deadline has not yet ended and it has not been distributed\n  - **distributed** - task has been distributed for Cross-Check, the Cross-Check deadline has not yet arrived\n  - **completed** - Cross-Check deadline has ended, the cross-check of the task has been completed\n- `crossCheckEndDate` - datestring with planned Cross-Check deadline date"
  },
  {
    "path": "docs/platform/cv.md",
    "content": "# CV\n\nCV is a 2-part functionality:\n\n- For users who want to find a job it helps create their own resume and present it to a potential employer\n- For employers who want to find an employee it grants access to user's resumes.\n\n## Part 1: CV\n\nUser can get access to the CV from the dropdown menu in the header\n\n<kbd>![CV link in Header](./img/cv/header-dropdown.JPG)</kbd>\n\nAfter this user needs to click on button \"Create CV\"\n\n<kbd>![No consent view](./img/cv/no-consent.JPG)</kbd>\n\nThere user will be asked to give his consent to the use of user data, after which the CV will become available to user (but not yet available to employers).\n\n<kbd>![No consent view](./img/cv/no-consent-modal.JPG)</kbd>\n\nCV has 2 views: `View` and `Form`.\nView is the “front” side of the CV and displays the available information, `Form` allows user to add, modify and delete user information in the CV.\n\nThis is how CV `View` looks like:\n\n<kbd>![Filled CV View](./img/cv/cv-view-filled.JPG)</kbd>\n\nThere are 3 types of data presented in the CV:\n\n- Information that the user can freely change directly\n  - `General info`\n    - `Name` - name\n    - `Desired position` - prefered position\n    - `Self introduction video` - link to a video with a short self-presentation\n    - `Link to avatar` - link to the image that will be used as an avatar\n    - `English level` - user's English level\n    - `Military service` - current military service status\n    - `Ready to start work from` - the date from which the user is ready to start work\n    - `Ready to work full time` - is the user ready to work full time\n    - `About me` - a few sentences to tell about yourself\n    - `Locations` - locations where the user is able to work (3 locations can be added, each comma-separated, the extra ones will be truncated)\n\n  - `Contacts`\n    - `Phone` - phone number (international format)\n    - `Email` - email address\n    - `Skype id` - skype id\n    - `Telegram public name` - Telegram public name (contained in the link to Telegram after `t.me/`)\n    - `LinkedIn` - link to the user's profile in LinkedIn\n    - `GitHub username `- the github id of the account that user wants to show to the employer (it may differ from the one used for authentication in RS School App)\n    - `Website` - a link to a website (with a portfolio, some profile etc.)\n  - `Visible courses` - the courses user wants to show in his CV and on the Employer page (he can only select courses for which he has registered). User can explicitly choose which courses to show, all courses are shown if no `visible courses` were selected.\n\n- Information that is pulled from RS School App and cannot be changed directly\n  - `Courses` - information about the user's RSS courses, includes (for each course that the student is currently taking or has taken and decided to show):\n    - `Course name` - course name\n    - `Course status` - user's status on the course (in progress, completed, completed with certificate, etc.)\n    - `Mentor` - mentor's name and a link to his profile, if the student had a mentor, \"No mentor\" otherwise\n    - `Score` - total score\n    - `Position` - usert's position in score\n  - `Feedback` - mentor's feedback\n  - `Gratitudes` - public feedback on the user (gratitudes). Displays only the date and text of the feedback, as well as the total. By default, the last 5 records are shown, and it can be expanded by clicking `Show all` button (after that user will be able to click `Show partially` button and the last 5 records will be shown again)\n\nIf the user is the owner of the CV that they opens (if the `githubid` under which the user is logged in matches the `githubid` of the requested CV) user sees control buttons (Edit CV, Share, Delete)\n\n<kbd>![CV Form](./img/cv/view-control-buttons.JPG)</kbd>\n\n- `Edit CV` - when user clicks on this button, CV switches to the `Form` mode\n- `Share` - when user clicks on this button, public link is copied to clipboard\n- `Delete` - when user clicks on this button, delete confirmation popup appears  \n  <kbd>![CV Form](./img/cv/view-delete-cv.JPG)</kbd>\n\nWhen user clicks on Edit CV button, the `Form` will appear where user can fill in the data for the CV\n\n<kbd>![CV Form](./img/cv/cv-form-filled.JPG)</kbd>\n<kbd>![CV Form](./img/cv/cv-form-filled-1.JPG)</kbd>\n\nAll this data doesn't overlap with user profile/student data/etc, it's associated with CV only.\n\nAlso there are few buttons at the bottom.\n\n<kbd>![CV Form buttons](./img/cv/edit-cv-buttons.JPG)</kbd>\n\n- `Save` button saves the entered CV data on the server\n- `Cancel` button switches CV to the `View` mode without saving the data\n\nOther features:\n\n- CV `View` can be printed, special styles are applied via `@media print` when attempting to print\n\n<kbd>![CV View print](./img/cv/cv-view-print.JPG)</kbd>\n\n- Only CV owner and users with roles `admin` or `hirer` have access to student's CV\n\n---\n\n## Part 2: Applicants Page\n\nThe grouping table at `/applicants` page contains information for applicantss to find users interested in employment, as well as links to their CVs.\n\nOnly users with roles `admin` or `hirer` have access to this table. In the UI, the link to that page is in the Admin Sider.\n\n<kbd>![Admin Sider Applicants page link](./img/cv/applicants-page-link.JPG)</kbd>\n\nThe grouping table includes only those users who have filled in the minimum amount of information to display at the `/applicants` page and whose CV has not yet expired.\n\nFor each record, the following columns are displayed:\n\n- `Name` - user name and link to CV\n- `CV Expires` - the date until CV is valid\n- `Desired position` - preferred position\n- `Locations` - locations where user is able to work\n- `English level` - user's English level\n- `Full time` - is the user ready to work full time\n- `Start from` - the date from which the user is ready to start work\n\n<kbd>![Admin Sider Applicants page link](./img/cv/applicants-table.JPG)</kbd>\n\nOther features:\n\n- The administrator can delete or hide the record about the user in the table.\n- There are search and sorting by columns."
  },
  {
    "path": "docs/platform/notifications.md",
    "content": "## RS School App Notifications\n\n## Notification types\n\n- As a mentor, you are approved to take part in the course (contains links of approval, **does not require subscription**)\n- Your task has been checked by mentor/cross-check (contains the score, task's maximum score, and weight of the task)\n\n## Channels\n\n- Telegram: Notifications come from <span style=\"color:orange\">`@rsschool_bot`</span>\n- Email: Notifications come to the email\n\n## How to subscribe to/unsubscribe from notifications\n\n**In the <span style=\"color:orange\">`Contacts`</span> card fill in <span style=\"color:orange\">`email`</span> and <span style=\"color:orange\">`Telegram`</span> fields without <span style=\"color:orange\">`@`</span> in your [profile](https://app.rs.school/profile). Contacts are interconnected when registered.**\n\n1. Enter <span style=\"color:green_apple\">[edit](https://app.rs.school/profile#edit)</span> mode in the profile\n2. In the <span style=\"color:orange\">`Consents`</span> card, set a checkmark in front of the channel you want to subscribe to.\n\n**To change the state of <span style=\"color:orange\">Telegram</span> subscription in the profile, it is necessary to subscribe/unsubscribe with <span style=\"color:orange\">`@rsschool_bot`</span>**"
  },
  {
    "path": "docs/platform/pull-request-review-process.md",
    "content": "## Task Checking Process by Mentor\n\n1. A student completes an assignment in a private repository.\n2. The student creates and submits a Pull Request before the deadline.\n   - The PR rules are specified [below](https://docs.app.rs.school/#/platform/pull-request-review-process?id=pull-request-description-must-contain-the-following)\n   - Penalties for deadline violations are listed [below](https://docs.app.rs.school/#/platform/pull-request-review-process?id=deadlines-for-students)\n3. Until the final grade is given by the mentor, the student can continue to implement remaining features\n4. The mentor checks the PR, leaves his comments and recommendations on the quality of the code (copy-paste, magic numbers, project structure, etc.) and the implemented functionality. Leaves a comment with a preliminary score.\n   - The score is set by the mentor based on the assessment criteria specified for each task\n   - When giving a score, all implemented functionality must be taken into account. E.g. a student did not 100% fulfill the minimum (basic) requirements, but fulfilled some of the additional ones - all requirements must be taken into account\n   - The mentor can set a preliminary score in advance, taking into account that the student will correct all the comments afterwards\n5. The student addresses the comments within 5 days.\n   - If the mentor's comment to the PR is pending the student's answer - the student writes the answer as a comment's reply\n   - If the student has committed some changes, the student must leave a comment about what exactly has changed\n6. Based on the results of the code review and corresponding changes, the mentor sets the final grade in Score (`RS APP > Submit-review`).\n   - It is up to the mentor to decide whether to deduct points or not for the functionality implemented by the student after the deadline.\n   - If the student has not addressed the mentor's comments, the mentor may further reduce the mark. The size of the penalty is at the discretion of the mentor, maximum -50 points.\n\n## Pull Request Requirements (PR)\n\nPull Request is a place to discuss contributor's code. It should not be a monologue but rather a fruitful collaboration between a contributor and a reviewer. Stay professional, respect each other's time and efforts.\n\n### Pull Request description must contain the following:\n\n1. Task URL.\n2. Screenshot showing the result of Task's completion. The screenshot is added to a Pull Request as an image attachment. To achieve that you can just dra-and-drop the screenshot to the Description text area.\n3. Deployment URL of your application. For frontend - Website URL, for backend - API Endpoint URL. To create deployment you can use the following:\n   - gh-pages (if you have access to a private RS School repo)\n   - web hosting, like [netlify.com](https://app.netlify.com/drop) (if you don't have access to a private RS School repo or can't deploy to `gh-pages` because of permissions)\n   - other static assets storage with web serving capabilities, like [S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html)\n   - serverless or self-hosted solutions for your API (make sure URL is public and accessible by other people)\n   - naming scheme: GitHub account name - Task name.\n4. Submittion Date / Deadline Date.\n5. Your self-check of Task's completion result and opinion on the achieved Score.\n\n### Description Example\n\n```\n1. Task: https://github.com/rolling-scopes-school/tasks/blob/master/tasks/fancy-weather.md\n2. Screenshot:\n   ![](https://docs.app.rs.school/images/fancy-weather.png)\n3. Deployment: https://chakapega-fancy-weather.netlify.com/\n4. Done 28.05.2020 / deadline 31.05.2020\n5. Score: 220 / 300\n- Markup, design, UI (15/30)\n  - [x] minimum page width at which it is displayed correctly – 320 рх (10)\n  - [±] application's appearancecorresponds to the layout and/or its improved version (5/10)\n  - [ ] aaplication works and looks correctly with any language (0)\n- Section \"Today's weather\" displays the following data (15/20)\n  - [x] use weather data and location (10)\n  - [±] clock, refreshed each second (5/10)\n ...\n```\n\n### Pull Request must not contain the following:\n\n- Commented code\n- Leftover and/or irrelevant files, auto-generated code, node_modules, etc.\n\n## Code Review Process Recommendations\n\nFor a more efficient process of code review of student PRs, it is recommended to conduct it in 2 stages:\n\n- Review of the \"Draft\" version of the task. The student creates a PR when the \"Draft\" version is ready, which shows the main concept of the task and implements the main parts of the application. This version may not cover additional requirements.\n- Review of the \"Release\" version of the task. This is a completed and refactored version of the task, which contains all the changes addressing the comments and suggestions mentioned during the review of the \"Draft\" version.\n\nThis approach solves several problems that are usually encountered when reviewing and implementing large tasks:\n\n- Reducing the one-time review load on the mentor.\n  - It is often quite difficult to carefully look into the entire code of the task in one go and clarify all the concepts that the student missed within the large code base.\n  - It is often much easier to find time for 2 smaller reviews, although in general, the total time may be increased.\n- Catching architectural mishaps at the very beginning, which lead to \"expensive\", in terms of refactoring, problems\n- Learn how to work with Git and GitHub\n- Motivation to meet the deadline\n\n## Code Review Process Example\n\n1. Code Review process can be started with a check of the PR format, the naming of the commits, and a sufficient number of them.\n\n- [Commit Requirements](https://docs.app.rs.school/#/platform/git-convention)\n- [Pull Request Requirements](https://docs.app.rs.school/#/platform/pull-request-review-process?id=pull-request-description-must-contain-the-following)\n\n2. Next, clone the repository, install dependencies, and check if the project is buildable / runnable.\n\n3. If the requirements included the use of a linter or tests, check for the presence of a prepared scripts to start, lint and/or test the app and whether errors occur after running them.\n\n4. It is worth checking the overall functionality of the application, whether there are no errors in the console, whether requests are processed correctly, etc.\n\nAt this stage, you can pay attention to possible UI / UX problems:\n\n- whether clickable elements are highlighted\n- whether there is an overlap of elements / text\n- possible recommendations for improving visual appearance of the app if there was no clearly defined design in the task\n\n5. Now's the time to review the code itself. Here are some potential code quality improvement points:\n\n### General\n\n- DRY - if there are repeating pieces of code, it's better to put them in a separate class/function\n- KISS - try to keep the structure simple and clear\n- comments should explain the purpose of the code instead of describe it\n\n```\nBad\n\n# method to find min value\nfunction findMinValue() {...}\n```\n\n- no code formatter used, which helps maintain the same code style across the team (e.g. prettier)\n- it is better not to use abbreviations for naming variables / classes / functions, this will improve overall understanding and readability of the code\n\n### HTML\n\n- no alt attribute for the `img` tag, it is also recommended to set the height and width, which is useful when the image does not load for some reason\n- lack or insufficient semantics\n- redundant blocks\n\n```html\n<div class=\"container\">\n  <div class=\"wrapper\">\n    <ul>\n      ...\n    </ul>\n  </div>\n</div>\n```\n\n- use class names in kebab-case for HTML\n\n```html\n<!-- BAD -->\n<div class=\"containerWrapper\"></div>\n```\n\n```html\n<!-- GOOD -->\n<div class=\"container-wrapper\"></div>\n```\n\n- if you need to make some kind of heading in the upper case in your HTML, it's better to do it with CSS\n- using inline styles or JS code in HTML, i.e. in the form of an `onclick` function, is also a mistake\n\n### CSS\n\n- try to apply dynamic styles with CSS classes, not with the use of JS\n- try to write styles with a small degree of nesting (no more than 2x) - this simplifies maintenance and eases refactoring of the style if necessary\n- it is preferable to use the same units of measurement (`px`,`rem` or `em`) which makes it easier to introduce changes (when using preprocessors, you can write a function that will convert `px` to `rem`)\n\n### JS\n\n- use event delegation if applicable\n- use parentheses for `if else / for` blocks\n- magic numbers and magic strings - try to put them into separate constants\n- use enums objects\n\n```js\nconst KEY_CODES = {\n\tSpace: 32,\n\tEnter: 13,\n\t...\n}\n\nif (key === KEY_CODES.Enter) {\n  ...\n}\n```\n\n- file size (large files are difficult to read and maintain, if the file is more than 200 - 400 lines, then you should think about splitting)\n- monitor the degree of nesting of conditional blocks, blocks with a nesting level of 3 or more are difficult to read and perceive, perhaps they can be moved into a separate function\n- try to use pure functions (they are better tested)\n- use named arguments if the number of arguments is 3 or more, this allows you not to follow the order and when reading the code it is more clear what is passed to the function\n\n```js\nfunction insertElement({ parent, tag, class, value }){...}\n\ninsertElement({ parent: parentElement, tag: 'p', class: 'class', value: 'Some value' })\n```\n\n### Deadlines for Students\n\n- Deadlines for all tasks are indicated in the course schedule.\n- If student did not have time to turn in the assignment on time, mentor, at his own discretion, can apply the following penalties:\n  - -10 score points if you are late up to 3 days, inclusive\n  - -30% score percentage if you are late up to 7 days, inclusive\n  - -70% score percentage if more than a week late\n  - penalties can be omitted if there is a good reason (hospital, army training, etc.)\n  - rounding occurs in favor of the student, when applying penalty coefficients\n\n### Deadlines for Mentors\n\nMentor is expected to review student's work within one or two weeks of the student's submission. But the sooner the better. Deadline dates for students are indicated in the schedule.\n\n## Recommended Links\n\n- [How to write the perfect Pull Request](https://github.com/blog/1943-how-to-write-the-perfect-pull-request)"
  },
  {
    "path": "docs/platform/shedule.md",
    "content": "## RS School App Schedule\n\nTo help you successfully complete the selected course, the **RS School App** has a **schedule** which can be accessed from the main menu\n\n1. Select the desired course\n2. Follow the link\n\n![schedule](./img/schedule-1.png)\n\n#### General view of the schedule\n\n![schedule view](./img/schedule-2.png)\n\n#### Schedule Control Panel\n\n![schedule control panel](./img/schedule-3.png)\n\n1. Select the type of schedule. The selection is saved for the device/browser\n   - **Table** - the most detailed view of the schedule (opens by default)\n   - **List** - the most compact view for mobile devices (opens by default on mobile)\n   - **Calendar** - common calendar for schedule review\n\n2. Time zone selection\n3. Switch to hide past events\n4. Customizing event tag colors. For each event/task, you can choose a color from a predefined set of colors. The selection is saved for the device/browser\n\n**Table** has some more advanced settings, where you can hide some columns and events/tasks by type\n\n![table customizing](./img/schedule-4.png)\n\n#### Event/Task Details\n\nThe event/task details page displays the most detailed data.\n\n![event/task details](./img/schedule-5.png)\n\n#### Using the schedule by students\n\nIt is possible to search by the name of the event/task or the organizer. It is possible to sort by date (type **Table**). To open the details of the event/task, click on the name of the event/task.\n\n### Additional schedule features for the administration\n\nFor the convenient usage of the schedule, the following editing options are provided:\n\n1. Extended Control Panel\n\n![schedule control panel admin](./img/schedule-6.png)\n\n    **1** - a button to export the course schedule to a csv file for editing the whole course\n    **2** - a button to import the course schedule from a csv file (new events/tasks are added or existing ones are updated)\n    **3** - a button to add a single event/task\n\n2. Quick edit of the **Table view**\n\n![schedule table edit](./img/schedule-7.png)\n![schedule table edit](./img/schedule-9.png)\n\n3. Add a new event/task\n\n![add new entity](./img/schedule-8.png)\n\n4. Event/task edit is also available on the event/task details page.\n\n![edit entity](./img/schedule-10.png)\n\n#### Some features of filling out the schedule\n\n- The main fields in events/tasks change immediately **in all courses**. E.g. **Name**, **Type**, **Url**.\n\n- The field **Special** allows you to specify the type of event/task. For example, make it optional using the _optional_ tag. It is possible to add tags not only from the provided list."
  },
  {
    "path": "docs/platform/tasks.md",
    "content": "# Submission of tasks in the RS School App\n\nMost of the tasks, after they are completed, have to be submitted to the RS School App **before the deadline**:\n\n- Auto-checkable tasks are to be submitted on the Auto-Test page. Tasks that are checked automatically: tests, algorithmic tasks, codewars tasks.\n- In case of Cross-Check task checking, you need to submit a link to your work in the `Cross-check: Submit` page before the deadline. The link can be submitted several times - only the last one is saved. After submitting a link to the completed task, it can be edited/completed until the deadline. If you do not submit your work, you will get 0 points for that task.\n\n## Tests\n\n- Tests submitted in <span style=\"color:green_apple\">[RS School App](https://app.rs.school/)</span> can be solved after authorization in the application.\n- The minimum passing score is usually 90% of the maximum possible number of points.\n- You can take the test as many times as specified, and only the last result is counted.\n- If specified, you can try the test even more times, but the score for the test will be halved.\n- The result of passing the test will be displayed immediately, and will be added to the score page the next day after passing.\n\n## Algorithmic tasks\n\n- <span style=\"color:green_apple\">[Example of the task](https://github.com/AlreadyBored/basic-js)</span>\n- The scores of these tasks are summed up and contribute to the total score with a coefficient from 0.1 to 0.5 (a maximum of 50 points can be obtained for solving one task).\n- Copying solutions ⇒ expulsion. Think thoroughly before submitting someone else's code for the sake of getting 10 points. We do not require the solution of all tasks.\n- If you do not know how you had solved the task during the interview ⇒ copying detected ⇒ expulsion.\n- If you know how you had solved the task during the interview, but you cannot solve a simpler task ⇒ copying detected ⇒ expulsion.\n\n#### Will it be possible to resubmit algorithmic tasks?\n\nYou can submit as many times as you want before the deadline.\n\n#### How to find an error while solving algorithmic tasks?\n\n- console.log() input parameters at the beginning of the solution\n- you can run only one test to reduce the number of logs\n  `mocha ./test/<TEST NAME>.test.js`\n  or\n  `npm run test ./test/task-name.test.js`\n- You can comment out everything in the test except for the test that fails\n- You can configure the debug in VSC and track what is wrong step by step <span style=\"color:green_apple\">[https://code.visualstudio.com/docs/nodejs/nodejs-debugging](https://code.visualstudio.com/docs/nodejs/nodejs-debugging)</span>\n- You can use this service to visually see the code <span style=\"color:green_apple\">[http://pythontutor.com/javascript.html#mode=edit](http://pythontutor.com/javascript.html#mode=edit)</span>\n\n## Codewars\n\nSome tasks require solving several tasks on the website <span style=\"color:green_apple\">[https://www.codewars.com/](https://www.codewars.com/)</span>\n\nAfter you complete the task, log in to the RS School App <span style=\"color:green_apple\">[https://app.rs.school/](https://app.rs.school/)</span>, select **Auto-Test**, select **Codewars {Task Name}** from the drop-down list, click **Submit**. The result of the check is displayed on the right.\n\nYou can submit a task as many times as you want. Each subsequent submission overwrites the previous one.\n\nYour username on the Codewars website should be changed to the provided username on the submitting page.\nYou can change your Codewars username by following this link <span style=\"color:green_apple\">[https://www.codewars.com/users/edit](https://www.codewars.com/users/edit)</span>.\nFill `Username` field with provided username and click `Update` button at the page bottom.\n\n![edit username](./img/tasks-1.jpg)\n\n## Cross-check\n\nDescription in a separate <span style=\"color:green_apple\">[file](cross-check-flow.md)</span>\n\n### CodeJam\n\nCodeJam is a task without a description provided in advance, and with a limited time for execution (from 60 minutes to 48 hours).\nFor example, on Friday at 21:00, everyone receives a link with a task that takes 48 hours to complete.\n\n## FAQ\n\n### What should I do if I can't pass the test/task on time?\n\nSkip it and try to complete the rest of the tasks for the maximum score.\n\n### Am I allowed to take the solution of the task from the Internet?\n\nYou may take the idea, but do not copy the solution."
  },
  {
    "path": "docs/platform/typical-problems.md",
    "content": "## Action plan: \"What to do if something does not work in RS School App?\"\n\n1. Firstly, check if anybody else has faced the same problem in <span style=\"color:green_apple\">[https://github.com/rolling-scopes/rsschool-app/issues](https://github.com/rolling-scopes/rsschool-app/issues)</span>. If the problem has already been described there, you need to leave a comment that this problem still needs to be resolved. If there is no such problem described, move to the 2nd point below.\n2. Ask about the problem in Discord channels (all tasks have their channels). Wait for answers from students/activists. If the problem has not been solved, move to the 3rd point below.\n3. Ask about the problem in the Discord channel **#questions-to-rsapp**. Wait for comments from activists/admins. If the problem has not been solved, move to the 4th point below.\n4. Create a new issue with a description of the problem in the repository <span style=\"color:green_apple\">[https://github.com/rolling-scopes/rsschool-app/issues](https://github.com/rolling-scopes/rsschool-app/issues)</span>.\n\n## Typical problems and their solutions\n\n1. \"No access\" problem\n\n   ![No Access Error](./img/no-access.png)\n\n   **Solution**: register for the course you need via <span style=\"color:green_apple\">[link](https://app.rs.school/registry/student)</span>.\n\n2. I do not know how to see my mistakes in the task after **Auto-test**.\n\n   **Solution**: enter <span style=\"color:green_apple\">[app.rs.school](https://app.rs.school/)</span> and follow the **Auto-test** section and choose the task you need in the drop-down list. Details of the check are shown in the **Details** column of the **Verification Results table**:\n\n   ![Auto-test details](./img/autotest-details.jpg)\n\n3. I submitted the task and got my mark, but the score has not changed yet.\n\n   **Solution**: wait. For the changes to be updated some time is needed - usually, it takes 5 minutes."
  },
  {
    "path": "eslint.config.mjs",
    "content": "import eslint from '@eslint/js';\nimport turbo from 'eslint-config-turbo/flat';\nimport vitest from '@vitest/eslint-plugin';\nimport globals from 'globals';\nimport tsEslint from 'typescript-eslint';\nimport { isAgent } from 'std-env';\n\nexport default tsEslint.config(\n  eslint.configs.recommended,\n  tsEslint.configs.recommended,\n  ...turbo,\n  {\n    files: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],\n    ...vitest.configs.recommended,\n  },\n  {\n    ignores: ['node_modules', 'dist'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.browser,\n      },\n    },\n    rules: {\n      '@typescript-eslint/no-explicit-any': isAgent ? 'off' : 'warn',\n      '@typescript-eslint/no-unused-vars': [\n        'error',\n        {\n          args: 'all',\n          argsIgnorePattern: '^_',\n          caughtErrors: 'all',\n          caughtErrorsIgnorePattern: '^_',\n          destructuredArrayIgnorePattern: '^_',\n          varsIgnorePattern: '^_',\n          ignoreRestSiblings: true,\n        },\n      ],\n      'no-else-return': ['error'],\n    },\n  },\n);\n"
  },
  {
    "path": "nestjs/.dockerignore",
    "content": "node_modules\n.turbo\n"
  },
  {
    "path": "nestjs/.swcrc",
    "content": "{\n  \"jsc\": {\n    \"target\": \"es2021\",\n    \"parser\": {\n      \"syntax\": \"typescript\",\n      \"decorators\": true\n    },\n    \"transform\": {\n      \"legacyDecorator\": true,\n      \"decoratorMetadata\": true\n    },\n    \"keepClassNames\": true\n  },\n  \"module\": {\n    \"type\": \"es6\"\n  },\n  \"sourceMaps\": true\n}\n"
  },
  {
    "path": "nestjs/Dockerfile",
    "content": "FROM node:24-alpine\n\nEXPOSE 8080\n\nENV NODE_ENV=production\nENV NODE_PORT=8080\nENV TZ=utc\nENV RS_ENV=production\n\nWORKDIR /app\n\nCOPY nestjs/package.json /app/nestjs/\nCOPY package.json /app\nCOPY package-lock.json /app\n\nRUN npm install --production --no-optional\n\nCOPY nestjs/dist /app/nestjs/dist\n\nCMD [ \"node\", \"/app/nestjs/dist/nestjs/src/main\" ]\n"
  },
  {
    "path": "nestjs/Dockerfile.lambda",
    "content": "FROM node:24-bullseye-slim AS builder\n\nWORKDIR /container_out\n\nCOPY package.json package.json\nCOPY package-lock.json package-lock.json\nCOPY nestjs/.env nestjs/.env\nCOPY nestjs/package.json nestjs/package.json\n\nRUN npm ci --production --no-optional\n\nCOPY nestjs/dist nestjs/dist\n\n# Lambda Container with AWS Lambda Web Adapter\nFROM node:24-bullseye-slim\n\nENV NODE_ENV=production\nENV TZ=utc\nENV AWS_LAMBDA=true\nENV RS_ENV=staging\nENV RSSCHOOL_DEV_TOOLS=true\nENV NODE_PORT=8080\nENV PORT=8080\nENV AWS_LWA_PORT=8080\nENV AWS_LWA_REMOVE_BASE_PATH=/api/v2\nENV DOTENV_CONFIG_PATH=/var/task/nestjs/.env\n\nWORKDIR /var/task\n\nCOPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.0-rc1 /lambda-adapter /opt/extensions/lambda-adapter\n\nCOPY --from=builder /container_out /var/task/\n\nCMD [ \"node\", \"-r\", \"dotenv/config\", \"/var/task/nestjs/dist/nestjs/src/main.js\" ]\n"
  },
  {
    "path": "nestjs/README.md",
    "content": "# NestJS\n\nThis workspace contains the active NestJS backend for RS School App.\n\n## Scope\n\n- REST API for the app\n- OpenAPI spec used to generate the client SDK\n\n## Setup\n\n- Follow root `README.md` for database setup and running the full app\n- Copy `nestjs/.env.example` to `nestjs/.env`\n- Frontend DevTools module depends on the `.env` variable `RSSCHOOL_DEV_TOOLS` on the backend.\n\n## OpenAPI client\n\n- Run `npm run openapi` to regenerate `client/src/api` and `nestjs/src/spec.json`\n- Commit the generated changes\n\n## Notes\n\n- The deprecated Koa backend lives in `server/`"
  },
  {
    "path": "nestjs/eslint.config.mjs",
    "content": "import defaultConfig from '../eslint.config.mjs';\nexport default defaultConfig;\n"
  },
  {
    "path": "nestjs/nest-cli.json",
    "content": "{\n  \"collection\": \"@nestjs/schematics\",\n  \"sourceRoot\": \"src\",\n  \"entryFile\": \"nestjs/src/main.js\"\n}\n"
  },
  {
    "path": "nestjs/openapitools.json",
    "content": "{\n  \"$schema\": \"node_modules/@openapitools/openapi-generator-cli/config.schema.json\",\n  \"spaces\": 2,\n  \"generator-cli\": {\n    \"version\": \"5.4.0\"\n  }\n}\n"
  },
  {
    "path": "nestjs/package.json",
    "content": "{\n  \"name\": \"nestjs\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"author\": \"\",\n  \"private\": true,\n  \"license\": \"Mozilla Public License 2.0\",\n  \"scripts\": {\n    \"prebuild\": \"rimraf dist\",\n    \"build\": \"nest build\",\n    \"compile\": \"tsc --noEmit\",\n    \"start\": \"nest start --watch | pino-pretty -i time,remoteAddress,req,reqId\",\n    \"start:debug\": \"nest start --debug --watch\",\n    \"start:prod\": \"node dist/nestjs/src/main.js\",\n    \"lint\": \"eslint src\",\n    \"test\": \"vitest run --passWithNoTests\",\n    \"test:ci\": \"vitest run\",\n    \"test:watch\": \"vitest watch\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"test:debug\": \"vitest run --inspect-brk --no-file-parallelism\",\n    \"openapi\": \"npm run openapi:spec && npm run openapi:sdk && npm run openapi:format\",\n    \"openapi:spec\": \"ts-node --project tsconfig.json ./src/openapi-spec.ts\",\n    \"openapi:format\": \"oxfmt src/spec.json\",\n    \"openapi:sdk\": \"npx openapi-generator-cli generate -i ./src/spec.json -g typescript-axios -o ../client/src/api --additional-properties=supportsES6=true\"\n  },\n  \"dependencies\": {\n    \"@apalchys/pino-cloudwatch\": \"0.9.0\",\n    \"@aws-sdk/client-s3\": \"3.749.0\",\n    \"@nestjs/axios\": \"4.0.0\",\n    \"@nestjs/cache-manager\": \"2.3.0\",\n    \"@nestjs/common\": \"10.4.16\",\n    \"@nestjs/config\": \"4.0.0\",\n    \"@nestjs/core\": \"10.4.15\",\n    \"@nestjs/event-emitter\": \"3.0.1\",\n    \"@nestjs/mapped-types\": \"2.1.0\",\n    \"@nestjs/passport\": \"11.0.5\",\n    \"@nestjs/platform-express\": \"10.4.15\",\n    \"@nestjs/schedule\": \"5.0.1\",\n    \"@nestjs/swagger\": \"8.1.1\",\n    \"@nestjs/typeorm\": \"11.0.0\",\n    \"@sentry/node\": \"8.55.0\",\n    \"cache-manager\": \"5.7.6\",\n    \"class-transformer\": \"0.5.1\",\n    \"class-validator\": \"0.14.1\",\n    \"cookie-parser\": \"1.4.7\",\n    \"date-fns\": \"2.30.0\",\n    \"date-fns-tz\": \"2.0.1\",\n    \"express\": \"4.21.2\",\n    \"handlebars\": \"4.7.8\",\n    \"ical-generator\": \"5.0.1\",\n    \"json2csv\": \"5.0.7\",\n    \"jsonwebtoken\": \"9.0.2\",\n    \"nanoid\": \"3.3.8\",\n    \"nestjs-pino\": \"4.6.0\",\n    \"openai\": \"5.12.2\",\n    \"passport\": \"0.6.0\",\n    \"passport-custom\": \"1.1.1\",\n    \"passport-github2\": \"0.1.12\",\n    \"passport-http\": \"0.3.0\",\n    \"passport-jwt\": \"4.0.1\",\n    \"pg\": \"8.11.3\",\n    \"pino-http\": \"11.0.0\",\n    \"pino\": \"10.3.1\",\n    \"reflect-metadata\": \"0.2.2\",\n    \"rimraf\": \"6.0.1\",\n    \"rxjs\": \"7.8.1\",\n    \"swagger-ui-express\": \"5.0.1\"\n  },\n  \"devDependencies\": {\n    \"@nestjs/cli\": \"^10\",\n    \"@nestjs/schematics\": \"^10\",\n    \"@nestjs/testing\": \"^10\",\n    \"@openapitools/openapi-generator-cli\": \"2.20.0\",\n    \"@types/cookie-parser\": \"1.4.6\",\n    \"@types/express\": \"4.17.21\",\n    \"@types/json2csv\": \"5.0.7\",\n    \"@types/jsonwebtoken\": \"9.0.5\",\n    \"@types/passport-github2\": \"1.2.9\",\n    \"@types/passport-http\": \"0.3.11\",\n    \"@types/passport-jwt\": \"3.0.13\",\n    \"@types/supertest\": \"2.0.16\",\n    \"pino-pretty\": \"10.2.3\",\n    \"supertest\": \"6.3.3\",\n    \"ts-loader\": \"9.5.2\",\n    \"tsconfig-paths\": \"4.2.0\"\n  }\n}\n"
  },
  {
    "path": "nestjs/src/activity/activity.controller.ts",
    "content": "import {\n  Controller,\n  Get,\n  UseGuards,\n  Req,\n  Post,\n  Body,\n  UnauthorizedException,\n  BadRequestException,\n  NotFoundException,\n} from '@nestjs/common';\nimport * as crypto from 'crypto';\nimport type { Request } from 'express';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, CurrentRequest } from 'src/auth';\nimport { UsersService } from 'src/users/users.service';\nimport { ConfigService } from 'src/config';\nimport { ActivityDto } from './dto/activity.dto';\nimport { CreateActivityDto } from './dto/create-activity.dto';\nimport { CreateActivityWebhookDto } from './dto/create-activity-webhook.dto';\n\n@Controller('activity')\n@ApiTags('activity')\nexport class ActivityController {\n  constructor(\n    private userService: UsersService,\n    private config: ConfigService,\n  ) {}\n\n  @Get('/')\n  @UseGuards(DefaultGuard)\n  @ApiOperation({ operationId: 'getActivity' })\n  @ApiOkResponse({ type: ActivityDto })\n  public async getActivity(@Req() req: CurrentRequest): Promise<ActivityDto> {\n    const {\n      user: { id },\n    } = req;\n    const { lastActivityTime, isActive } = await this.userService.getUserByUserId(id);\n    return { isActive, lastActivityTime };\n  }\n\n  @Post('/')\n  @UseGuards(DefaultGuard)\n  @ApiOperation({ operationId: 'createActivity' })\n  @ApiOkResponse({ type: ActivityDto })\n  @ApiBody({ type: CreateActivityDto })\n  public async createActivity(@Body() body: CreateActivityDto, @Req() req: CurrentRequest): Promise<ActivityDto> {\n    const {\n      user: { id },\n    } = req;\n    const user = await this.userService.getUserByUserId(id);\n    const { isActive } = body;\n    const now = Date.now();\n    await this.userService.updateUser(user.id, { lastActivityTime: now, isActive });\n    return { isActive, lastActivityTime: now };\n  }\n\n  @Post('/webhook')\n  @ApiOperation({ operationId: 'createActivityWebhook' })\n  @ApiOkResponse({ type: ActivityDto })\n  @ApiBody({ type: CreateActivityWebhookDto })\n  public async createActivityWebhook(\n    @Body() body: CreateActivityWebhookDto,\n    @Req() req: Request,\n  ): Promise<ActivityDto> {\n    const signature = req.headers['x-hub-signature'] as string;\n\n    if (!signature) {\n      throw new UnauthorizedException('x-hub-signature is missing');\n    }\n\n    const comparisonSignature = createComparisonSignature(body, this.config.auth.github.activityWebhookSecret);\n    if (!compareSignatures(signature, comparisonSignature)) {\n      throw new UnauthorizedException(\"Signatures didn't match\");\n    }\n\n    const { sender } = body;\n    if (!sender || !sender.login) {\n      throw new BadRequestException();\n    }\n\n    const { githubId } = sender.login;\n    const user = await this.userService.getByGithubId(githubId);\n    if (!user) {\n      throw new NotFoundException(`User with GitHub id ${githubId} not found`);\n    }\n\n    const now = Date.now();\n    const isActive = true;\n    await this.userService.updateUser(user.id, { lastActivityTime: now, isActive });\n    return { isActive, lastActivityTime: now };\n  }\n}\n\nfunction createComparisonSignature(body: unknown, secret: string) {\n  const hmac = crypto.createHmac('sha1', secret);\n  const selfSignature = hmac.update(JSON.stringify(body)).digest('hex');\n  return `sha1=${selfSignature}`;\n}\n\nfunction compareSignatures(signature: string, comparisonSignature: string) {\n  const source = Buffer.from(signature);\n  const comparison = Buffer.from(comparisonSignature);\n  return crypto.timingSafeEqual(source, comparison);\n}\n"
  },
  {
    "path": "nestjs/src/activity/activity.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from 'src/config';\nimport { UsersModule } from 'src/users';\nimport { ActivityController } from './activity.controller';\n\n@Module({\n  controllers: [ActivityController],\n  imports: [UsersModule, ConfigModule],\n})\nexport class ActivityModule {}\n"
  },
  {
    "path": "nestjs/src/activity/dto/activity.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsNumber } from 'class-validator';\n\nexport class ActivityDto {\n  @ApiProperty()\n  @IsNumber()\n  public lastActivityTime: number;\n\n  @ApiProperty()\n  @IsBoolean()\n  public isActive: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/activity/dto/create-activity-webhook.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { IsObject, IsString, ValidateNested } from 'class-validator';\n\nclass SenderLoginDto {\n  @ApiProperty()\n  @IsString()\n  public githubId: string;\n}\nclass SenderDto {\n  @ApiProperty({ type: SenderLoginDto })\n  @IsObject()\n  @ValidateNested()\n  @Type(() => SenderLoginDto)\n  public login: SenderLoginDto;\n}\n\nexport class CreateActivityWebhookDto {\n  @ApiProperty({ type: SenderDto })\n  @IsObject()\n  @ValidateNested()\n  @Type(() => SenderDto)\n  public sender: SenderDto;\n}\n"
  },
  {
    "path": "nestjs/src/activity/dto/create-activity.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean } from 'class-validator';\n\nexport class CreateActivityDto {\n  @ApiProperty()\n  @IsBoolean()\n  public isActive: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/alerts/alerts.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  DefaultValuePipe,\n  Delete,\n  Get,\n  Param,\n  ParseBoolPipe,\n  ParseIntPipe,\n  Patch,\n  Post,\n  Query,\n  UseGuards,\n} from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { AlertsService } from './alerts.service';\nimport { AlertDto, CreateAlertDto, UpdateAlertDto } from './dto';\n\n@Controller('alerts')\n@ApiTags('alerts')\nexport class AlertsController {\n  constructor(private readonly alertService: AlertsService) {}\n\n  @Post('/')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'createAlert' })\n  @ApiOkResponse({ type: AlertDto })\n  public async create(@Body() createAlertDto: CreateAlertDto) {\n    const result = await this.alertService.create(createAlertDto);\n    return new AlertDto(result);\n  }\n\n  @Get('/')\n  @UseGuards(DefaultGuard)\n  @ApiOperation({ operationId: 'getAlerts' })\n  @ApiOkResponse({ type: [AlertDto] })\n  public async getAll(\n    @Query('enabled', new DefaultValuePipe(true), ParseBoolPipe) enabled: boolean,\n  ): Promise<AlertDto[]> {\n    const data = await this.alertService.findAll({ enabled });\n    return data.map(item => new AlertDto(item));\n  }\n\n  @Delete('/:id')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'deleteAlert' })\n  @ApiOkResponse({})\n  public async remove(@Param('id', ParseIntPipe) id: number) {\n    return this.alertService.remove(id);\n  }\n\n  @Patch('/:id')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'updateAlert' })\n  @ApiOkResponse({ type: AlertDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() updateAlertDto: UpdateAlertDto) {\n    const result = await this.alertService.update(id, updateAlertDto);\n    return new AlertDto(result);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/alerts/alerts.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Alert } from '@entities/alert';\nimport { AlertsController } from './alerts.controller';\nimport { AlertsService } from './alerts.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Alert])],\n  controllers: [AlertsController],\n  providers: [AlertsService],\n})\nexport class AlertsModule {}\n"
  },
  {
    "path": "nestjs/src/alerts/alerts.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CreateAlertDto } from './dto/create-alert.dto';\nimport { UpdateAlertDto } from './dto/update-alert.dto';\nimport { Alert, AlertType } from '@entities/alert';\nimport { omitBy, isUndefined } from 'lodash';\n\nconst fields: (keyof Alert)[] = ['id', 'text', 'type', 'courseId', 'enabled'];\n\nconst clean = <T extends Record<string, unknown>>(alert: T) => omitBy(alert, isUndefined) as Partial<T>;\n\n@Injectable()\nexport class AlertsService {\n  constructor(\n    @InjectRepository(Alert)\n    private alertsRepository: Repository<Alert>,\n  ) {}\n\n  public async create(createAlertDto: CreateAlertDto) {\n    const { text, type, courseId, enabled = false } = createAlertDto;\n    const { id } = await this.alertsRepository.save({\n      text,\n      type: type as AlertType,\n      courseId,\n      enabled,\n    });\n    return this.alertsRepository.findOneByOrFail({ id });\n  }\n\n  public async findAll({ enabled }: { enabled: boolean }): Promise<Alert[]> {\n    const items = await this.alertsRepository.find({\n      where: { enabled },\n      select: fields,\n      cache: 5 * 60 * 1000,\n    });\n    return items;\n  }\n\n  public async update(id: number, updateAlertDto: UpdateAlertDto) {\n    const { text, type, courseId, enabled } = updateAlertDto;\n    await this.alertsRepository.update(\n      id,\n      clean({\n        text,\n        type: type as AlertType,\n        courseId,\n        enabled,\n      }),\n    );\n    return this.alertsRepository.findOneByOrFail({ id });\n  }\n\n  public async remove(id: number) {\n    await this.alertsRepository.delete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/alerts/dto/alert.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Alert } from '@entities/alert';\n\nexport class AlertDto {\n  constructor(alert: Alert) {\n    this.id = alert.id;\n    this.type = alert.type;\n    this.text = alert.text;\n    this.enabled = alert.enabled;\n    this.courseId = alert.courseId;\n    this.createdDate = alert.createdDate;\n    this.updatedDate = alert.updatedDate;\n  }\n\n  @ApiProperty()\n  id: number;\n  @ApiProperty()\n  type: string;\n  @ApiProperty()\n  text: string;\n  @ApiProperty()\n  enabled: boolean;\n  @ApiProperty({ type: Number, nullable: true })\n  courseId: number | null;\n  @ApiProperty()\n  updatedDate: string;\n  @ApiProperty()\n  createdDate: string;\n}\n"
  },
  {
    "path": "nestjs/src/alerts/dto/create-alert.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class CreateAlertDto {\n  @ApiProperty()\n  type: string;\n  @ApiProperty()\n  text: string;\n  @ApiProperty({ required: false })\n  enabled?: boolean;\n  @ApiProperty({ required: false })\n  courseId?: number;\n}\n"
  },
  {
    "path": "nestjs/src/alerts/dto/index.ts",
    "content": "export * from './alert.dto';\nexport * from './create-alert.dto';\nexport * from './update-alert.dto';\n"
  },
  {
    "path": "nestjs/src/alerts/dto/update-alert.dto.ts",
    "content": "import { PartialType } from '@nestjs/mapped-types';\nimport { CreateAlertDto } from './create-alert.dto';\n\nexport class UpdateAlertDto extends PartialType(CreateAlertDto) {}\n"
  },
  {
    "path": "nestjs/src/app.module.ts",
    "content": "import { Logger, Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { ScheduleModule as NestScheduleModule } from '@nestjs/schedule';\nimport { EventEmitterModule } from '@nestjs/event-emitter';\nimport { LoggerModule } from 'nestjs-pino';\n\nimport { AlertsModule } from './alerts/alerts.module';\nimport { AuthModule } from './auth/auth.module';\nimport { ConfigModule, ConfigService } from './config';\nimport { LoggingMiddleware, NoCacheMiddleware } from './core/middlewares';\nimport { getPinoHttp } from './core/pino';\nimport { CoursesModule } from './courses/courses.module';\nimport { DisciplinesModule } from './disciplines';\nimport { RegistryModule } from './registry/registry.module';\nimport { NotificationsModule } from './notifications/notifications.module';\nimport config from './ormconfig';\nimport { ProfileModule } from './profile';\nimport { UsersModule } from './users';\nimport { CertificatesModule } from './certificates/certificates.module';\nimport { DiscordServersModule } from './discord-servers/discord-servers.module';\nimport { CrossCheckModule } from './cross-check/cross-check.module';\nimport { OpportunitiesModule } from './opportunities/opportunities.module';\nimport { UserGroupsModule } from './user-groups/user-groups.module';\nimport { ScheduleModule } from './schedule/schedule.module';\nimport { ActivityModule } from './activity/activity.module';\nimport { UsersNotificationsModule } from './users-notifications';\nimport { GratitudesModule } from './gratitudes';\nimport { CloudApiModule } from './cloud-api/cloud-api.module';\nimport { EventsModule } from './events/events.module';\nimport { TasksModule } from './tasks/tasks.module';\nimport { PromptsModule } from './prompts/prompts.module';\nimport { AutoTestModule } from './auto-test/auto-test.module';\nimport { ContributorsModule } from './contributors';\nimport { ListenersModule } from './listeners';\nimport { MentorsHallOfFameModule } from './mentors-hall-of-fame';\nimport { RepositoriesModule } from './repositories';\nimport { SessionModule } from './session/session.module';\nimport { DevtoolsModule } from './devtools/devtools.module';\n\n@Module({\n  imports: [\n    LoggerModule.forRoot({\n      pinoHttp: getPinoHttp(),\n    }),\n    TypeOrmModule.forRoot({\n      ...config,\n      autoLoadEntities: true,\n    }),\n    EventEmitterModule.forRoot({\n      delimiter: '.',\n      wildcard: true,\n    }),\n    NestScheduleModule.forRoot(),\n    ActivityModule,\n    ConfigModule,\n    AlertsModule,\n    UsersModule,\n    CoursesModule,\n    AuthModule,\n    ProfileModule,\n    DisciplinesModule,\n    NotificationsModule,\n    RegistryModule,\n    CertificatesModule,\n    DiscordServersModule,\n    CrossCheckModule,\n    OpportunitiesModule,\n    UserGroupsModule,\n    ScheduleModule,\n    UsersNotificationsModule,\n    GratitudesModule,\n    CloudApiModule,\n    EventsModule,\n    TasksModule,\n    PromptsModule,\n    AutoTestModule,\n    ContributorsModule,\n    ListenersModule,\n    MentorsHallOfFameModule,\n    RepositoriesModule,\n    SessionModule,\n    DevtoolsModule.forRoot(),\n  ],\n  controllers: [],\n  providers: [Logger, ConfigService],\n})\nexport class AppModule {\n  configure(consumer: MiddlewareConsumer) {\n    consumer.apply(LoggingMiddleware).forRoutes({ path: '/*', method: RequestMethod.ALL });\n    consumer.apply(NoCacheMiddleware).forRoutes({ path: '/*', method: RequestMethod.GET });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/auth-user.model.spec.ts",
    "content": "import { CourseUser } from '@entities/courseUser';\nimport { CourseRole } from '@entities/session';\nimport { AuthUser } from './auth-user.model';\nimport type { AuthDetails } from './auth.service';\n\ndescribe('AuthUser', () => {\n  it('creates user with supervisor role', () => {\n    const user = new AuthUser({ courseUsers: [{ courseId: 1, isSupervisor: true } as CourseUser] } as AuthDetails);\n    expect(user.courses).toStrictEqual({\n      1: { roles: [CourseRole.Supervisor] },\n    });\n  });\n\n  it('creates user with manager role', () => {\n    const user = new AuthUser({ courseUsers: [{ courseId: 2, isManager: true } as CourseUser] } as AuthDetails);\n    expect(user.courses).toStrictEqual({\n      2: { roles: [CourseRole.Manager] },\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/auth/auth-user.model.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { CourseUser } from '@entities/courseUser';\nimport { CourseRole } from '@entities/session';\nimport type { AuthDetails } from './auth.service';\n\nexport enum Role {\n  Admin = 'admin',\n  User = 'user',\n  Hirer = 'hirer',\n}\n\nexport { CourseRole };\n\nexport type CourseRoles = Record<string, CourseRole[]>;\n\nexport interface CourseInfo {\n  mentorId?: number;\n  studentId?: number;\n  isExpelled?: boolean;\n  roles: CourseRole[];\n}\n\nexport interface JwtToken {\n  id: number;\n  githubId: string;\n  isAdmin: boolean;\n  isHirer: boolean;\n}\n\nexport class AuthUser {\n  public readonly roles: Record<string, 'mentor' | 'student'>;\n  public readonly isAdmin: boolean;\n  public readonly isHirer: boolean;\n  public readonly id: number;\n  public readonly appRoles: Role[];\n  public readonly githubId: string;\n  public readonly courses: Record<number, CourseInfo>;\n\n  constructor(user: AuthDetails, courseTasks: CourseTask[] = [], admin: boolean = false, hirer: boolean = false) {\n    const roles: { [key: string]: 'student' | 'mentor' } = {};\n    const courses: Record<number, CourseInfo> = {};\n\n    user.students?.forEach(student => {\n      roles[student.courseId] = 'student';\n      const info = courses[student.courseId] ?? { roles: [] };\n      info.studentId = student.id;\n      info.roles.push(CourseRole.Student);\n      info.isExpelled = student.isExpelled || undefined;\n      courses[student.courseId] = info;\n    });\n    user.mentors?.forEach(mentor => {\n      roles[mentor.courseId] = 'mentor';\n      const info = courses[mentor.courseId] ?? { roles: [] };\n      info.mentorId = mentor.id;\n      info.roles.push(CourseRole.Mentor);\n      courses[mentor.courseId] = info;\n    });\n\n    const userId = user.id;\n\n    const coursesInfo = this.populateCourseInfo(courses, user.courseUsers ?? [], courseTasks);\n\n    this.id = userId;\n    this.isAdmin = admin;\n    this.isHirer = hirer;\n    this.githubId = user.githubId;\n    this.appRoles = [this.isAdmin ? Role.Admin : Role.User];\n    //fix openapi generator issue\n    if (this.isHirer) {\n      this.appRoles.push(Role.Hirer);\n    }\n    this.roles = roles;\n    this.courses = coursesInfo;\n    return this;\n  }\n\n  static createAdmin() {\n    return new AuthUser({ courseUsers: [], githubId: '', id: 0, mentors: [], students: [] }, [], true);\n  }\n\n  private populateCourseInfo(\n    courseInfo: Record<number, CourseInfo>,\n    courseUsers: CourseUser[],\n    taskOwner: CourseTask[],\n  ) {\n    return courseUsers\n      .flatMap(({ courseId, isDementor, isManager, isSupervisor }) => {\n        const result: { courseId: number; role: CourseRole }[] = [];\n        if (isManager) {\n          result.push({ courseId, role: CourseRole.Manager });\n        }\n        if (isSupervisor) {\n          result.push({ courseId, role: CourseRole.Supervisor });\n        }\n        if (isDementor) {\n          result.push({ courseId, role: CourseRole.Dementor });\n        }\n        return result;\n      })\n      .concat(taskOwner.map(({ courseId }) => ({ courseId, role: CourseRole.TaskOwner })))\n      .reduce((acc, { courseId, role }) => {\n        if (!acc[courseId]) {\n          acc[courseId] = { roles: [] };\n        }\n        if (!acc[courseId]?.roles.includes(role)) {\n          acc[courseId]?.roles.push(role);\n        }\n        return acc;\n      }, courseInfo);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/auth.controller.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { AuthService } from '.';\nimport { AuthController } from './auth.controller';\nimport { GithubStrategy } from './strategies/github.strategy';\n\ndescribe('AuthController', () => {\n  let controller: AuthController;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        { provide: AuthService, useValue: {} },\n        { provide: GithubStrategy, useValue: {} },\n      ],\n      controllers: [AuthController],\n    }).compile();\n\n    controller = module.get<AuthController>(AuthController);\n  });\n\n  it('should be defined', () => {\n    expect(controller).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "nestjs/src/auth/auth.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  ForbiddenException,\n  Get,\n  Logger,\n  Param,\n  ParseIntPipe,\n  Post,\n  Req,\n  Res,\n  UseGuards,\n} from '@nestjs/common';\nimport { AuthGuard } from '@nestjs/passport';\nimport { ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { addHours } from 'date-fns';\nimport { Response } from 'express';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from '.';\nimport { AuthService, CurrentRequest } from './auth.service';\nimport { JWT_COOKIE_NAME } from './constants';\nimport { AuthConnectionDto } from './dto/auth-connection.dto';\nimport { GithubStrategy } from './strategies/github.strategy';\n\nconst isDev = process.env.NODE_ENV !== 'production';\nconst COOKIE_DOMAIN = isDev ? undefined : 'rs.school';\nconst COOKIE_PATH = '/';\nconst twoDaysMs = 1000 * 60 * 60 * 24 * 2;\n\n@Controller('auth')\n@ApiTags('auth')\nexport class AuthController {\n  private logger = new Logger(AuthController.name);\n\n  constructor(\n    private readonly authService: AuthService,\n    private githubStrategy: GithubStrategy,\n  ) {}\n\n  @Get('github/login')\n  @ApiOperation({ operationId: 'githubLogin' })\n  @UseGuards(AuthGuard(isDev ? 'dev' : 'github'))\n  githubLogin() {}\n\n  @Get('github/callback')\n  @ApiOperation({ operationId: 'githubCallback' })\n  @UseGuards(AuthGuard(isDev ? 'dev' : 'github'))\n  async githubCallback(@Req() req: CurrentRequest, @Res() res: Response) {\n    try {\n      const token = this.authService.validateGithub(req);\n\n      res.cookie(JWT_COOKIE_NAME, token, {\n        expires: new Date(Date.now() + twoDaysMs),\n        httpOnly: true,\n        secure: true,\n        domain: COOKIE_DOMAIN,\n        sameSite: 'none',\n      });\n\n      const { loginState } = req;\n\n      if (loginState?.channelId) {\n        await this.authService.onConnectionComplete(loginState, req.user.id);\n        res.redirect(`/profile/connection-confirmed?connectionType=${loginState.channelId}`);\n      } else {\n        res.redirect(this.authService.getRedirectUrl(loginState));\n      }\n    } catch (err) {\n      const error = err as Error;\n      this.logger.error(`Auth error: ${error.message}`, error);\n      throw err;\n    }\n  }\n\n  @Get('github/logout')\n  @ApiOperation({ operationId: 'githubLogout' })\n  githubLogout(@Res() res: Response) {\n    res.clearCookie(JWT_COOKIE_NAME, { domain: COOKIE_DOMAIN, path: COOKIE_PATH });\n    res.redirect('/login');\n  }\n\n  @Post('github/connect')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  async createConnectLinkViaGithub(@Body() dto: AuthConnectionDto) {\n    const link = await this.githubStrategy.getAuthorizeUrl({\n      data: dto,\n      expires: addHours(new Date(), 1).toISOString(),\n    });\n    return {\n      link,\n    };\n  }\n\n  @Delete('cache/:userId')\n  @ApiOperation({ operationId: 'clearAuthUserSessionCache' })\n  @UseGuards(DefaultGuard)\n  async clearAuthUserSessionCache(@Param('userId', ParseIntPipe) userId: number, @Req() req: CurrentRequest) {\n    if (req.user.id !== userId) throw new ForbiddenException();\n    await this.authService.clearAuthUserSessionCache(userId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/auth.module.ts",
    "content": "import { LoginState } from '@entities/loginState';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { HttpModule } from '@nestjs/axios';\nimport { Module } from '@nestjs/common';\nimport { PassportModule } from '@nestjs/passport';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { ConfigModule } from '../config/config.module';\nimport { CoursesModule } from '../courses/courses.module';\nimport { UsersModule } from '../users/users.module';\nimport { AuthController } from './auth.controller';\nimport { AuthService } from './auth.service';\nimport { BasicStrategy } from './strategies/basic.strategy';\nimport { DevStrategy } from './strategies/dev.strategy';\nimport { GithubStrategy } from './strategies/github.strategy';\nimport { JwtStrategy } from './strategies/jwt.strategy';\nimport { CoreModule } from '../core/core.module';\nimport { CacheModule } from '@nestjs/cache-manager';\n\n@Module({\n  imports: [\n    CacheModule.register(),\n    CoreModule,\n    PassportModule.register({}),\n    UsersModule,\n    CoursesModule,\n    ConfigModule,\n    HttpModule,\n    TypeOrmModule.forFeature([LoginState, NotificationUserConnection]),\n  ],\n  controllers: [AuthController],\n  providers: [AuthService, GithubStrategy, JwtStrategy, BasicStrategy, DevStrategy],\n  exports: [GithubStrategy, AuthService],\n})\nexport class AuthModule {}\n"
  },
  {
    "path": "nestjs/src/auth/auth.service.spec.ts",
    "content": "import type { Mocked } from 'vitest';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { UsersService } from '../users/users.service';\nimport { CourseTasksService } from '../courses';\nimport { AuthService } from './auth.service';\nimport { JwtService } from '../core/jwt/jwt.service';\nimport { ConfigService } from '../config';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { HttpService } from '@nestjs/axios';\nimport { UserNotificationsService } from '../users-notifications/users.notifications.service';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { LoginState } from '@entities/loginState';\nimport { CACHE_MANAGER } from '@nestjs/cache-manager';\nimport type { Profile } from 'passport';\nimport { AuthUser } from './auth-user.model';\nimport { User } from '@entities/user';\n\ndescribe('AuthService', () => {\n  let service: AuthService;\n  let usersService: Mocked<UsersService>;\n  let courseTasksService: Mocked<CourseTasksService>;\n\n  const mockProfile: Profile = {\n    provider: 'github',\n    id: '12345',\n    displayName: 'John Doe',\n    username: 'johndoe',\n    emails: [{ value: 'john@example.com', primary: true }] as unknown as Profile['emails'],\n    name: { givenName: 'John', familyName: 'Doe' },\n  };\n\n  const mockUser = {\n    id: 1,\n    githubId: 'johndoe',\n    provider: 'github',\n    providerUserId: '12345',\n    primaryEmail: 'john@example.com',\n    firstName: 'John',\n    lastName: 'Doe',\n  } as Partial<User> as User;\n\n  const mockAuthDetails = {\n    id: 1,\n    githubId: 'johndoe',\n    students: [],\n    mentors: [],\n    courseUsers: [],\n  };\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        AuthService,\n        { provide: JwtService, useValue: {} },\n        {\n          provide: CourseTasksService,\n          useValue: {\n            getByOwner: vi.fn(),\n          },\n        },\n        {\n          provide: UsersService,\n          useValue: {\n            getUserByProvider: vi.fn(),\n            getByGithubId: vi.fn(),\n            saveUser: vi.fn(),\n            updateUser: vi.fn(),\n          },\n        },\n        {\n          provide: ConfigService,\n          useValue: {\n            users: { admins: ['admin'], hirers: ['hirer'] },\n          },\n        },\n        { provide: getRepositoryToken(LoginState), useValue: {} },\n        { provide: UserNotificationsService, useValue: {} },\n        { provide: HttpService, useValue: {} },\n        { provide: getRepositoryToken(NotificationUserConnection), useValue: {} },\n        { provide: CACHE_MANAGER, useValue: { del: vi.fn() } },\n      ],\n    }).compile();\n\n    service = module.get<AuthService>(AuthService);\n    usersService = module.get(UsersService);\n    courseTasksService = module.get(CourseTasksService);\n  });\n\n  it('should be defined', () => {\n    expect(service).toBeDefined();\n  });\n\n  describe('createAuthUser', () => {\n    beforeEach(() => {\n      vi.spyOn(service, 'getAuthDetails').mockResolvedValue(mockAuthDetails);\n      courseTasksService.getByOwner.mockResolvedValue([]);\n    });\n\n    it('should create a new user when user does not exist', async () => {\n      usersService.getUserByProvider.mockResolvedValue(null);\n      usersService.getByGithubId.mockResolvedValue(null);\n      usersService.saveUser.mockResolvedValue({} as User);\n\n      const result = await service.createAuthUser(mockProfile);\n\n      expect(usersService.getUserByProvider).toHaveBeenCalledWith('github', '12345');\n      expect(usersService.getByGithubId).toHaveBeenCalledWith('johndoe');\n      expect(usersService.saveUser).toHaveBeenCalledWith({\n        githubId: 'johndoe',\n        providerUserId: '12345',\n        provider: 'github',\n        primaryEmail: 'john@example.com',\n        firstName: 'John',\n        lastName: 'Doe',\n        lastActivityTime: expect.any(Number),\n      });\n      expect(result).toBeInstanceOf(AuthUser);\n    });\n\n    it('should update user provider info when user exists but has no provider', async () => {\n      const existingUser = { ...mockUser, provider: null as never, providerUserId: null as never } satisfies User;\n      usersService.getUserByProvider.mockResolvedValue(null);\n      usersService.getByGithubId.mockResolvedValue(existingUser);\n      usersService.saveUser.mockResolvedValue({} as User);\n\n      await service.createAuthUser(mockProfile);\n\n      expect(usersService.saveUser).toHaveBeenCalledWith({\n        id: 1,\n        provider: 'github',\n        providerUserId: '12345',\n        githubId: 'johndoe',\n      });\n    });\n\n    it('should update user githubId when it differs from username', async () => {\n      const existingUser = { ...mockUser, githubId: 'oldjohndoe' };\n      usersService.getUserByProvider.mockResolvedValue(existingUser as User);\n      usersService.saveUser.mockResolvedValue({} as User);\n\n      await service.createAuthUser(mockProfile);\n\n      expect(usersService.saveUser).toHaveBeenCalledWith({\n        id: 1,\n        provider: 'github',\n        providerUserId: '12345',\n        githubId: 'johndoe',\n      });\n    });\n\n    it('should update primary email when user exists but has no primary email', async () => {\n      const existingUser = { ...mockUser, primaryEmail: null };\n      usersService.getUserByProvider.mockResolvedValue(existingUser as User);\n      usersService.updateUser.mockResolvedValue();\n\n      await service.createAuthUser(mockProfile);\n\n      expect(usersService.updateUser).toHaveBeenCalledWith(1, {\n        primaryEmail: 'john@example.com',\n      });\n    });\n\n    it('should not update email when user already has primary email', async () => {\n      usersService.getUserByProvider.mockResolvedValue(mockUser as User);\n\n      await service.createAuthUser(mockProfile);\n\n      expect(usersService.updateUser).not.toHaveBeenCalled();\n    });\n\n    it('should handle profile with lowercase username', async () => {\n      const profileWithUppercase = { ...mockProfile, username: 'JohnDoe' };\n      usersService.getUserByProvider.mockResolvedValue(null);\n      usersService.getByGithubId.mockResolvedValue(null);\n      usersService.saveUser.mockResolvedValue({} as User);\n\n      await service.createAuthUser(profileWithUppercase);\n\n      expect(usersService.getByGithubId).toHaveBeenCalledWith('johndoe');\n      expect(usersService.saveUser).toHaveBeenCalledWith(\n        expect.objectContaining({\n          githubId: 'johndoe',\n        }),\n      );\n    });\n\n    it('should handle profile without emails', async () => {\n      const profileWithoutEmail = { ...mockProfile, emails: undefined };\n      usersService.getUserByProvider.mockResolvedValue(null);\n      usersService.getByGithubId.mockResolvedValue(null);\n      usersService.saveUser.mockResolvedValue({} as User);\n\n      await service.createAuthUser(profileWithoutEmail);\n\n      expect(usersService.saveUser).toHaveBeenCalledWith(\n        expect.objectContaining({\n          primaryEmail: undefined,\n        }),\n      );\n    });\n\n    it('should use first email when multiple emails exist', async () => {\n      const profileWithMultipleEmails = {\n        ...mockProfile,\n        emails: [\n          { value: 'first@example.com' },\n          { value: 'second@example.com' },\n          { value: 'third@example.com' },\n        ] as unknown as Profile['emails'],\n      };\n      usersService.getUserByProvider.mockResolvedValue(null);\n      usersService.getByGithubId.mockResolvedValue(null);\n      usersService.saveUser.mockResolvedValue({} as User);\n\n      await service.createAuthUser(profileWithMultipleEmails);\n\n      expect(usersService.saveUser).toHaveBeenCalledWith(\n        expect.objectContaining({\n          primaryEmail: 'first@example.com',\n        }),\n      );\n    });\n\n    it('should handle profile without name', async () => {\n      const profileWithoutName = { ...mockProfile, name: undefined };\n      usersService.getUserByProvider.mockResolvedValue(null);\n      usersService.getByGithubId.mockResolvedValue(null);\n      usersService.saveUser.mockResolvedValue({} as User);\n\n      await service.createAuthUser(profileWithoutName);\n\n      expect(usersService.saveUser).toHaveBeenCalledWith(\n        expect.objectContaining({\n          firstName: '',\n          lastName: '',\n        }),\n      );\n    });\n\n    it('should return AuthUser with admin privileges when admin is true', async () => {\n      usersService.getUserByProvider.mockResolvedValue(mockUser as User);\n\n      const result = await service.createAuthUser(mockProfile, true);\n\n      expect(result).toBeInstanceOf(AuthUser);\n      expect(result.isAdmin).toBe(true);\n    });\n\n    it('should return AuthUser with admin privileges when user is in admin list', async () => {\n      const adminProfile = { ...mockProfile, username: 'admin' };\n      const adminAuthDetails = { ...mockAuthDetails, githubId: 'admin' };\n      usersService.getUserByProvider.mockResolvedValue(mockUser as User);\n      vi.spyOn(service, 'getAuthDetails').mockResolvedValue(adminAuthDetails);\n\n      const result = await service.createAuthUser(adminProfile, false);\n\n      expect(result).toBeInstanceOf(AuthUser);\n      expect(result.isAdmin).toBe(true);\n    });\n\n    it('should return AuthUser without admin privileges for regular users', async () => {\n      usersService.getUserByProvider.mockResolvedValue(mockUser as User);\n\n      const result = await service.createAuthUser(mockProfile, false);\n\n      expect(result).toBeInstanceOf(AuthUser);\n      expect(result.isAdmin).toBe(false);\n    });\n\n    it('should prefer getUserByProvider over getByGithubId when provider exists', async () => {\n      usersService.getUserByProvider.mockResolvedValue(mockUser as User);\n      usersService.getByGithubId.mockResolvedValue(null);\n\n      await service.createAuthUser(mockProfile);\n\n      expect(usersService.getUserByProvider).toHaveBeenCalled();\n      expect(usersService.getByGithubId).not.toHaveBeenCalled();\n    });\n\n    it('should fallback to getByGithubId when getUserByProvider returns null', async () => {\n      usersService.getUserByProvider.mockResolvedValue(null);\n      usersService.getByGithubId.mockResolvedValue(mockUser as User);\n\n      await service.createAuthUser(mockProfile);\n\n      expect(usersService.getUserByProvider).toHaveBeenCalled();\n      expect(usersService.getByGithubId).toHaveBeenCalled();\n    });\n\n    it('should call getAuthUser with correct parameters', async () => {\n      usersService.getUserByProvider.mockResolvedValue(mockUser as User);\n      const getAuthUserSpy = vi.spyOn(service, 'getAuthUser');\n\n      await service.createAuthUser(mockProfile, true);\n\n      expect(getAuthUserSpy).toHaveBeenCalledWith('johndoe', true);\n    });\n\n    it('should not update user when all fields are already correct', async () => {\n      usersService.getUserByProvider.mockResolvedValue(mockUser as User);\n\n      await service.createAuthUser(mockProfile);\n\n      expect(usersService.saveUser).not.toHaveBeenCalled();\n      expect(usersService.updateUser).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/auth/auth.service.ts",
    "content": "import { LoginData, LoginState } from '@entities/loginState';\nimport { User } from '@entities/user';\nimport { HttpService } from '@nestjs/axios';\nimport { Inject, Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport type { Request } from 'express';\nimport { customAlphabet } from 'nanoid/async';\nimport type { Profile } from 'passport';\nimport { MoreThanOrEqual, Repository } from 'typeorm';\nimport { ConfigService } from '../config';\nimport { CourseTasksService } from '../courses';\nimport { UsersService } from '../users/users.service';\nimport { AuthUser } from './auth-user.model';\nimport { JwtService } from '../core/jwt/jwt.service';\nimport { lastValueFrom } from 'rxjs';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { CourseUser } from '@entities/courseUser';\nimport { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';\n\nconst nanoid = customAlphabet('1234567890abcdef', 10);\n\nexport type CurrentRequest = Request & {\n  user: AuthUser;\n  loginState?: LoginData;\n};\n\nexport type LoginStateParams = {\n  userId?: number;\n  expires?: string;\n  data: LoginData;\n};\n\nexport type AuthDetails = {\n  id: number;\n  githubId: string;\n  students: { courseId: number; id: number; isExpelled: boolean | null }[];\n  mentors: { courseId: number; id: number }[];\n  courseUsers: CourseUser[];\n};\n\n@Injectable()\nexport class AuthService {\n  private readonly admins: string[] = [];\n  private readonly hirers: string[] = [];\n\n  constructor(\n    private readonly jwtService: JwtService,\n    readonly courseTaskService: CourseTasksService,\n    readonly userService: UsersService,\n    readonly configService: ConfigService,\n    @InjectRepository(LoginState)\n    private readonly loginStateRepository: Repository<LoginState>,\n    @InjectRepository(NotificationUserConnection)\n    private notificationUserConnectionRepository: Repository<NotificationUserConnection>,\n    private httpService: HttpService,\n    @Inject(CACHE_MANAGER) private cacheManager: Cache,\n  ) {\n    this.admins = configService.users.admins;\n    this.hirers = configService.users.hirers;\n  }\n\n  public async createAuthUser(profile: Profile, admin = false): Promise<AuthUser> {\n    const username = profile.username?.toLowerCase();\n    const providerUserId = profile.id.toString();\n    const provider = profile.provider.toString();\n    const result =\n      (provider ? await this.userService.getUserByProvider(provider, providerUserId) : undefined) ??\n      (await this.userService.getByGithubId(username!));\n\n    if (result != null && (result.githubId !== username || !result.provider)) {\n      await this.userService.saveUser({\n        id: result.id,\n        provider: provider,\n        providerUserId: providerUserId,\n        githubId: username,\n      });\n    }\n\n    const email = this.extractEmailFromProfile(profile);\n\n    if (result != null && !result.primaryEmail && email) {\n      await this.userService.updateUser(result.id, { primaryEmail: email });\n    }\n\n    if (result == null) {\n      const user: Partial<User> = {\n        githubId: username,\n        providerUserId,\n        provider,\n        primaryEmail: email,\n        firstName: profile.name ? profile.name.givenName : '',\n        lastName: profile.name ? profile.name.familyName : '',\n        lastActivityTime: Date.now(),\n      };\n      await this.userService.saveUser(user);\n    }\n\n    const authUser = await this.getAuthUser(username!, admin);\n    return authUser;\n  }\n\n  private extractEmailFromProfile(profile: Profile): string | undefined {\n    return profile.emails?.slice(0, 1)?.[0]?.value || undefined;\n  }\n\n  public async getAuthUser(username: string, admin = false) {\n    const [authInfo, courseTasks] = await Promise.all([\n      this.getAuthDetails(username),\n      this.courseTaskService.getByOwner(username),\n    ]);\n    const isAdmin = this.admins.includes(username) || admin;\n    const isHirer = this.hirers.includes(username);\n    return new AuthUser(authInfo, courseTasks, isAdmin, isHirer);\n  }\n\n  public validateGithub(req: CurrentRequest) {\n    if (!req.user) {\n      return null;\n    }\n\n    return this.jwtService.createToken(req.user);\n  }\n\n  public async createLoginState(params: LoginStateParams) {\n    const id = await nanoid();\n    const { data, expires, userId } = params;\n\n    await this.loginStateRepository.save({\n      id,\n      data,\n      userId,\n      expires,\n    });\n\n    return id;\n  }\n\n  public getLoginStateById(id: string) {\n    return this.loginStateRepository.findOne({\n      where: {\n        id,\n        expires: MoreThanOrEqual(new Date().toISOString()),\n      },\n      order: {\n        createdDate: 'DESC',\n      },\n    });\n  }\n\n  public getLoginStateByUserId(id: number) {\n    return this.loginStateRepository.findOne({\n      where: {\n        userId: id,\n        expires: MoreThanOrEqual(new Date().toISOString()),\n      },\n      order: {\n        createdDate: 'DESC',\n      },\n    });\n  }\n\n  public deleteLoginState(id: string) {\n    return this.loginStateRepository.delete(id);\n  }\n\n  public getRedirectUrl(loginData?: LoginData) {\n    return loginData?.redirectUrl ? decodeURIComponent(loginData.redirectUrl) : '/';\n  }\n\n  public async onConnectionComplete(loginData: LoginData, userId: number) {\n    const { channelId, externalId } = loginData;\n    if (!channelId || !externalId) {\n      return;\n    }\n\n    this.notificationUserConnectionRepository.save({\n      channelId,\n      enabled: true,\n      externalId,\n      userId,\n    });\n\n    const { restApiKey, restApiUrl } = this.configService.awsServices;\n\n    if (channelId === 'telegram') {\n      await lastValueFrom(\n        this.httpService.post(\n          `${restApiUrl}/connection/complete`,\n          {\n            channelId,\n            externalId,\n          },\n          {\n            headers: { 'x-api-key': restApiKey },\n          },\n        ),\n      );\n    }\n  }\n\n  public async getAuthDetails(githubId: string): Promise<AuthDetails> {\n    const query = this.loginStateRepository.manager\n      .createQueryBuilder(User, 'user')\n      .select('user.id', 'id')\n      .addSelect('user.githubId', 'githubId')\n      .addSelect(\n        qb =>\n          qb\n            .select(`jsonb_agg(json_build_object('id', mentor.id, 'courseId', mentor.\"courseId\"))`)\n            .from('mentor', 'mentor')\n            .where('mentor.userId = user.id'),\n        'mentors',\n      )\n      .addSelect(\n        qb =>\n          qb\n            .select(\n              `jsonb_agg(json_build_object('id', student.id, 'courseId', student.\"courseId\", 'isExpelled', student.\"isExpelled\"))`,\n            )\n            .from('student', 'student')\n            .where('student.userId = user.id'),\n        'students',\n      )\n      .addSelect(\n        qb => qb.select('jsonb_agg(\"courseUser\")').from(CourseUser, 'courseUser').where('courseUser.userId = user.id'),\n        'courseUsers',\n      )\n      .where({\n        githubId,\n      });\n\n    const result = await query.getRawOne();\n    if (result == null) {\n      throw new NotFoundException('User not found');\n    }\n    return {\n      id: result.id,\n      githubId: result.githubId,\n      students: result.students ?? [],\n      mentors: result.mentors ?? [],\n      courseUsers: result.courseUsers ?? [],\n    };\n  }\n\n  public async clearAuthUserSessionCache(userId: number) {\n    const cacheKey = `auth-user-${userId}`;\n    await this.cacheManager.del(cacheKey);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/constants.ts",
    "content": "export const JWT_COOKIE_NAME = 'auth-token';\nexport const JWT_TOKEN_EXPIRATION = '2d';\n"
  },
  {
    "path": "nestjs/src/auth/course.guard.ts",
    "content": "import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';\nimport { CurrentRequest } from './auth.service';\n\n@Injectable()\nexport class CourseGuard implements CanActivate {\n  public canActivate(context: ExecutionContext) {\n    const req = context.getArgs<[CurrentRequest]>()[0];\n    const { user, params } = req;\n\n    if (user.isAdmin) {\n      return true;\n    }\n\n    return Boolean(user.courses[Number(params.courseId)]);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/default.guard.ts",
    "content": "import { AuthGuard } from '@nestjs/passport';\n\nexport const DefaultGuard = AuthGuard(['jwt', 'basic']);\n"
  },
  {
    "path": "nestjs/src/auth/dto/auth-connection.dto.ts",
    "content": "import { NotificationChannelId } from '@entities/notificationChannel';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty } from 'class-validator';\n\nexport class AuthConnectionDto {\n  @IsNotEmpty()\n  @ApiProperty()\n  channelId: NotificationChannelId;\n\n  @IsNotEmpty()\n  @ApiProperty()\n  externalId: string;\n}\n"
  },
  {
    "path": "nestjs/src/auth/index.ts",
    "content": "export * from './role.decorator';\nexport * from './role.guard';\nexport * from './default.guard';\nexport * from './auth-user.model';\nexport * from './auth.service';\nexport * from './course.guard';\n"
  },
  {
    "path": "nestjs/src/auth/role.decorator.ts",
    "content": "import { SetMetadata } from '@nestjs/common';\nimport { Role, CourseRole } from './auth-user.model';\n\nexport const REQUIRED_ROLES_KEY = 'requiredRoles';\n\nexport const RequiredRoles = (roles: (CourseRole | Role)[], requireCourseMatch = false) =>\n  SetMetadata(REQUIRED_ROLES_KEY, { roles, requireCourseMatch });\n"
  },
  {
    "path": "nestjs/src/auth/role.guard.ts",
    "content": "import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport { AuthUser } from '.';\nimport { Role, CourseRole } from './auth-user.model';\nimport { CurrentRequest } from './auth.service';\nimport { REQUIRED_ROLES_KEY } from './role.decorator';\n\nconst appRoles = Object.values(Role);\nconst courseRoles = Object.values(CourseRole);\n\n@Injectable()\nexport class RoleGuard implements CanActivate {\n  constructor(private readonly reflector: Reflector) {}\n\n  public canActivate(context: ExecutionContext) {\n    const handler = context.getHandler();\n    const cls = context.getClass();\n\n    const { requireCourseMatch, roles = [] } =\n      this.reflector.getAllAndOverride<{ roles: (CourseRole | Role)[]; requireCourseMatch: boolean }>(\n        REQUIRED_ROLES_KEY,\n        [handler, cls],\n      ) ?? {};\n\n    const req = context.getArgs<[CurrentRequest]>()[0];\n    const { user, params } = req;\n\n    const requiredAppRoles = roles.filter(role => appRoles.includes(role as Role)) as Role[];\n    const requiredCourseRoles = roles.filter(role => courseRoles.includes(role as CourseRole)) as CourseRole[];\n\n    if (requiredAppRoles.length === 0 && requiredCourseRoles.length === 0) {\n      return true;\n    }\n\n    if (requiredAppRoles.length && requiredAppRoles.some(requiredRole => user.appRoles.includes(requiredRole))) {\n      return true;\n    }\n\n    if (requiredCourseRoles.length) {\n      if (requireCourseMatch && params.courseId) {\n        return checkUserHasCourseRole(requiredCourseRoles, user, Number(params.courseId));\n      }\n\n      return checkUserHasRoleInAnyCourse(requiredCourseRoles, user);\n    }\n\n    return false;\n  }\n}\n\nfunction checkUserHasCourseRole(requiredCourseRoles: CourseRole[], user: AuthUser, courseId: number) {\n  if (!courseId) {\n    return false;\n  }\n  return requiredCourseRoles.some(courseRole => user.courses[courseId]?.roles.includes(courseRole));\n}\n\nfunction checkUserHasRoleInAnyCourse(requiredCourseRoles: CourseRole[], user: AuthUser) {\n  const allCourseRoles = Object.values(user.courses);\n  const hasRole = requiredCourseRoles.some(requiredRole =>\n    allCourseRoles.some(({ roles }) => roles.includes(requiredRole)),\n  );\n  return hasRole;\n}\n"
  },
  {
    "path": "nestjs/src/auth/strategies/basic.strategy.ts",
    "content": "import { Injectable, UnauthorizedException } from '@nestjs/common';\nimport { PassportStrategy } from '@nestjs/passport';\nimport { BasicStrategy as Strategy } from 'passport-http';\nimport { ConfigService } from 'src/config';\nimport { AuthUser } from '..';\n\n@Injectable()\nexport class BasicStrategy extends PassportStrategy(Strategy, 'basic') {\n  constructor(private readonly configService: ConfigService) {\n    super({ passReqToCallback: true });\n  }\n\n  public async validate(_: unknown, username: string, password: string) {\n    const { root } = this.configService.users;\n\n    if (root.username === username && root.password === password) {\n      return AuthUser.createAdmin();\n    }\n    throw new UnauthorizedException();\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/strategies/dev.strategy.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { PassportStrategy } from '@nestjs/passport';\nimport { CurrentRequest } from '../auth.service';\nimport { Strategy } from 'passport-custom';\nimport { ConfigService } from '../../config';\n\nimport { AuthService } from '../auth.service';\nimport { JWT_COOKIE_NAME } from '../constants';\nimport { type Profile } from 'passport';\n\n@Injectable()\nexport class DevStrategy extends PassportStrategy(Strategy, 'dev') {\n  constructor(\n    private readonly authService: AuthService,\n    private readonly config: ConfigService,\n  ) {\n    super();\n  }\n\n  public async validate(req: CurrentRequest): Promise<unknown> {\n    const profile = {\n      provider: '',\n      id: '',\n      username: this.config.auth.dev.username,\n    } as Profile;\n\n    const user = await this.authService.createAuthUser(profile, this.config.auth.dev.admin);\n    req.user = user;\n    const token = this.authService.validateGithub(req);\n    if (!token) {\n      throw new Error('Invalid token');\n    }\n    req.res?.writeHead(302, {\n      'Set-Cookie': `${JWT_COOKIE_NAME}=${encodeURI(token)}; HttpOnly; path=/;`,\n      Location: '/',\n    });\n    return true;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/strategies/github.strategy.ts",
    "content": "import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';\nimport { PassportStrategy } from '@nestjs/passport';\nimport { ConfigService } from '../../config';\nimport { Strategy, Profile } from 'passport-github2';\nimport { AuthService, LoginStateParams } from '../auth.service';\nimport { AuthUser, CurrentRequest } from '..';\nimport { addHours } from 'date-fns';\nimport passport from 'passport';\n\n@Injectable()\nexport class GithubStrategy extends PassportStrategy(Strategy, 'github') {\n  private readonly logger = new Logger(GithubStrategy.name);\n\n  constructor(\n    private config: ConfigService,\n    private readonly authService: AuthService,\n  ) {\n    super({\n      clientID: config.auth.github.clientId,\n      clientSecret: config.auth.github.clientSecret,\n      callbackURL: config.auth.github.callbackUrl,\n      scope: config.auth.github.scope,\n      passReqToCallback: true,\n    });\n  }\n\n  async authenticate(req: CurrentRequest, options: passport.AuthenticateOptions) {\n    const { url, code } = req.query;\n    const opts = { ...options };\n\n    if (!code) {\n      const id = await this.authService.createLoginState({\n        data: {\n          redirectUrl: url as string,\n        },\n        expires: addHours(new Date(), 1).toISOString(),\n      });\n      opts.state = id;\n    }\n\n    super.authenticate(req, opts);\n  }\n\n  public async validate(\n    request: CurrentRequest,\n    _accessToken: string,\n    _refreshToken: string,\n    profile: Profile,\n  ): Promise<AuthUser> {\n    const state = await this.authService.getLoginStateById(request.query.state as string);\n    if (!state) {\n      throw new UnauthorizedException();\n    }\n    const [user] = await Promise.all([\n      this.authService.createAuthUser(profile, this.config.auth.dev.admin),\n      this.authService.deleteLoginState(state.id),\n    ]);\n\n    this.logger.log({ message: `Logged in: [${user.githubId}]`, githubId: user.githubId });\n\n    request.loginState = state.data;\n\n    return user;\n  }\n\n  public async getAuthorizeUrl(params: LoginStateParams) {\n    const id = await this.authService.createLoginState(params);\n\n    const url = this._oauth2.getAuthorizeUrl({\n      redirect_uri: this.config.auth.github.callbackUrl,\n      state: id,\n      scope: this.config.auth.github.scope,\n    });\n    return url;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auth/strategies/jwt.strategy.ts",
    "content": "import { Inject, Injectable } from '@nestjs/common';\nimport { ConfigService } from '../../config';\nimport { PassportStrategy } from '@nestjs/passport';\nimport { Request } from 'express';\nimport { ExtractJwt, Strategy } from 'passport-jwt';\nimport { JWT_COOKIE_NAME } from '../constants';\nimport { AuthService } from '../auth.service';\nimport { AuthUser, JwtToken } from '../auth-user.model';\nimport { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';\n\n@Injectable()\nexport class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {\n  constructor(\n    @Inject(CACHE_MANAGER) private cacheManager: Cache,\n    config: ConfigService,\n    private authService: AuthService,\n  ) {\n    super({\n      jwtFromRequest: (req: Request) => req.cookies?.[JWT_COOKIE_NAME] || ExtractJwt.fromAuthHeaderAsBearerToken()(req),\n      ignoreExpiration: false,\n      secretOrKey: config.auth.jwt.secretKey,\n    });\n  }\n\n  public async validate(payload: JwtToken): Promise<AuthUser> {\n    const cacheKey = `auth-user-${payload.id}`;\n    const cached = await this.cacheManager.get<AuthUser>(cacheKey);\n    if (cached) {\n      return cached;\n    }\n    const authUser = await this.authService.getAuthUser(payload.githubId);\n    this.cacheManager.set(cacheKey, authUser, 1000 * 60 * 10);\n    return authUser;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auto-test/auto-test.controller.ts",
    "content": "import { Controller, Get, NotFoundException, Param, ParseIntPipe } from '@nestjs/common';\nimport { AutoTestService } from './auto-test.service';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole } from '@entities/session';\nimport { RequiredRoles, Role } from 'src/auth';\nimport { AutoTestTaskDto } from './dto/auto-test-task.dto';\nimport { BasicAutoTestTaskDto } from './dto/basic-auto-test-task.dto';\n\n@Controller('auto-test')\n@ApiTags('auto-tests')\nexport class AutoTestController {\n  constructor(private readonly service: AutoTestService) {}\n\n  @Get()\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'getBasicAutoTests' })\n  @ApiOkResponse({ type: [BasicAutoTestTaskDto] })\n  async getBasicAutoTests() {\n    return (await this.service.getAll()).map(autoTest => new BasicAutoTestTaskDto(autoTest));\n  }\n\n  @Get('/:id')\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'getAutoTest' })\n  @ApiOkResponse({ type: AutoTestTaskDto })\n  async getAutoTestTask(@Param('id', ParseIntPipe) id: number) {\n    const task = await this.service.findById(id);\n    if (!task) {\n      throw new NotFoundException(\"Couldn't find task with id = \" + id);\n    }\n    return new AutoTestTaskDto(task);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auto-test/auto-test.module.ts",
    "content": "import { Task } from '@entities/index';\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { AutoTestController } from './auto-test.controller';\nimport { AutoTestService } from './auto-test.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Task])],\n  controllers: [AutoTestController],\n  providers: [AutoTestService],\n})\nexport class AutoTestModule {}\n"
  },
  {
    "path": "nestjs/src/auto-test/auto-test.service.ts",
    "content": "import { Task, TaskType } from '@entities/task';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\n\n@Injectable()\nexport class AutoTestService {\n  constructor(@InjectRepository(Task) private repository: Repository<Task>) {}\n\n  public async getAll() {\n    return this.repository.find({\n      select: ['id', 'name', 'attributes'],\n      where: {\n        type: TaskType.SelfEducation,\n      },\n      relations: {\n        discipline: true,\n        courseTasks: { course: true },\n      },\n      order: {\n        updatedDate: 'DESC',\n      },\n    });\n  }\n\n  public async findById(id: number) {\n    return this.repository.findOne({\n      where: {\n        type: TaskType.SelfEducation,\n        id,\n      },\n      relations: {\n        discipline: true,\n        courseTasks: { course: true },\n      },\n      order: {\n        updatedDate: 'DESC',\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/auto-test/dto/auto-test-task.dto.ts",
    "content": "import { Task, TaskType } from '@entities/task';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { uniqBy } from 'lodash';\nimport { IdNameDto } from 'src/core/dto';\nimport { UsedCourseDto } from 'src/courses/dto/used-course.dto';\n\nclass QuestionDto {\n  @ApiProperty()\n  question: string;\n\n  @ApiProperty()\n  multiple: boolean;\n\n  @ApiProperty({ type: [String] })\n  answers: string[];\n\n  @ApiProperty({ required: false })\n  questionImage?: string;\n\n  @ApiProperty({ required: false })\n  answersType?: string;\n}\n\nclass PublicAttributesDto {\n  @ApiProperty()\n  maxAttemptsNumber: number;\n\n  @ApiProperty()\n  numberOfQuestions: number;\n\n  @ApiProperty()\n  strictAttemptsMode: boolean;\n\n  @ApiProperty()\n  tresholdPercentage: number;\n\n  @ApiProperty({ type: [QuestionDto] })\n  questions: QuestionDto[];\n}\n\nclass AutoTestAttributesDto {\n  @ApiProperty({ type: PublicAttributesDto })\n  public: PublicAttributesDto;\n\n  @ApiProperty({ type: 'array', items: { type: 'array', items: { type: 'number' } } })\n  answers: number[][];\n}\n\nexport class AutoTestTaskDto {\n  constructor(task: Task) {\n    this.id = task.id;\n    this.name = task.name;\n    this.type = task.type;\n    this.descriptionUrl = task.descriptionUrl;\n    this.description = task.description;\n    this.githubRepoName = task.githubRepoName;\n    this.sourceGithubRepoUrl = task.sourceGithubRepoUrl;\n    this.discipline = task.discipline ? new IdNameDto(task.discipline) : null;\n    this.courses = task.courseTasks\n      ? uniqBy(\n          task.courseTasks\n            .filter(task => !task.disabled)\n            .map(({ course }) => new UsedCourseDto({ name: course.name, isActive: !course.completed })),\n          course => course.name,\n        ).sort((a, b) => {\n          if (a.isActive === b.isActive) {\n            return a.name.localeCompare(b.name);\n          }\n          return Number(b.isActive) - Number(a.isActive);\n        })\n      : [];\n    this.githubPrRequired = task.githubPrRequired;\n    this.tags = task.tags;\n    this.skills = task.skills;\n    this.attributes = task.attributes as AutoTestAttributesDto;\n    this.createdDate = task.createdDate;\n    this.updatedDate = task.updatedDate;\n  }\n\n  @ApiProperty({ enum: TaskType })\n  public type: TaskType;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public descriptionUrl: string;\n\n  @ApiProperty()\n  public description: string;\n\n  @ApiProperty()\n  public githubRepoName: string;\n\n  @ApiProperty()\n  public sourceGithubRepoUrl: string;\n\n  @ApiProperty({ type: IdNameDto })\n  public discipline: IdNameDto | null;\n\n  @ApiProperty()\n  public githubPrRequired: boolean;\n\n  @ApiProperty()\n  public createdDate: string;\n\n  @ApiProperty()\n  public updatedDate: string;\n\n  @ApiProperty()\n  public tags: string[];\n\n  @ApiProperty()\n  public skills: string[];\n\n  @ApiProperty({ type: AutoTestAttributesDto })\n  public attributes: AutoTestAttributesDto;\n\n  @ApiProperty({ type: [UsedCourseDto] })\n  public courses: UsedCourseDto[];\n}\n"
  },
  {
    "path": "nestjs/src/auto-test/dto/basic-auto-test-task.dto.ts",
    "content": "import { Task } from '@entities/task';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class BasicAutoTestTaskDto {\n  constructor(task: Task) {\n    this.id = task.id;\n    this.name = task.name;\n    this.maxAttemptsNumber = isNaN(task?.attributes?.public?.maxAttemptsNumber)\n      ? null\n      : Number(task?.attributes?.public?.maxAttemptsNumber);\n    this.numberOfQuestions = isNaN(task?.attributes?.public?.numberOfQuestions)\n      ? null\n      : Number(task?.attributes?.public?.numberOfQuestions);\n    this.strictAttemptsMode = !!task?.attributes?.public?.strictAttemptsMode;\n    this.thresholdPercentage = isNaN(task?.attributes?.public?.tresholdPercentage)\n      ? null\n      : Number(task?.attributes?.public?.tresholdPercentage);\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty({ nullable: true, type: Number })\n  public maxAttemptsNumber: number | null;\n\n  @ApiProperty({ nullable: true, type: Number })\n  public numberOfQuestions: number | null;\n\n  @ApiProperty({ nullable: true, type: Number })\n  public strictAttemptsMode: boolean | null;\n\n  @ApiProperty({ nullable: true, type: Number })\n  public thresholdPercentage: number | null;\n}\n"
  },
  {
    "path": "nestjs/src/auto-test/dto/task-solution.dto.ts",
    "content": "import { TaskSolution } from '@entities/taskSolution';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nexport class TaskSolutionDto {\n  constructor(taskSolution: TaskSolution) {\n    this.id = taskSolution.id;\n    this.url = taskSolution.url;\n    this.courseTaskId = taskSolution.courseTaskId;\n  }\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  id: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  courseTaskId: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsString()\n  url: string;\n}\n"
  },
  {
    "path": "nestjs/src/certificates/certificates.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  Delete,\n  Get,\n  NotFoundException,\n  Param,\n  ParseIntPipe,\n  Post,\n  Res,\n  UseGuards,\n} from '@nestjs/common';\nimport { ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { Response } from 'express';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { StudentsService } from '../courses/students';\nimport { UserNotificationsService } from 'src/users-notifications/users.notifications.service';\nimport { CertificationsService } from './certificates.service';\nimport { SaveCertificateDto } from './dto/save-certificate-dto';\n\n@Controller('certificate')\n@ApiTags('certificate')\nexport class CertificatesController {\n  constructor(\n    private certificatesService: CertificationsService,\n    private notificationService: UserNotificationsService,\n    private studentsService: StudentsService,\n  ) {}\n\n  /**\n   * /certificate/abc - returns certificate in PDF format\n   * /certificate/abc.json - returns certificate metadata in JSON format\n   */\n  @Get('/:publicId')\n  @ApiOperation({ operationId: 'getCertificate' })\n  public async getCertificate(@Param('publicId') publicId: string, @Res() res: Response) {\n    const normalizedPublicId = publicId.endsWith('.json') ? publicId.slice(0, -5) : publicId;\n    const responseType = publicId.endsWith('.json') ? 'json' : 'pdf';\n\n    const certificate = await this.certificatesService.getByPublicId(normalizedPublicId);\n    if (!certificate) throw new NotFoundException();\n\n    try {\n      switch (responseType) {\n        case 'json': {\n          const metadata = await this.certificatesService.getCertificateMetadata(certificate);\n          res.json(metadata);\n          break;\n        }\n        case 'pdf': {\n          const stream = await this.certificatesService.getFileStream(certificate.s3Bucket, certificate.s3Key);\n          res.set('Content-Type', 'application/pdf');\n          stream.pipe(res);\n          break;\n        }\n      }\n    } catch {\n      throw new NotFoundException();\n    }\n  }\n\n  @Post('/')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'saveCertificate' })\n  public async saveCertificate(@Body() dto: SaveCertificateDto) {\n    const student = await this.studentsService.getById(dto.studentId);\n\n    const [notificationData] = await Promise.all([\n      this.certificatesService.buildNotificationData(student, dto),\n      this.certificatesService.saveCertificate(student.id, dto),\n    ]);\n\n    const { userId, notification } = notificationData;\n\n    await this.notificationService.sendEventNotification({\n      data: notification,\n      notificationId: 'courseCertificate',\n      userId: userId,\n    });\n  }\n\n  @Delete('/:studentId')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'removeCertificate' })\n  public async removeCertificate(@Param('studentId', ParseIntPipe) studentId: number) {\n    await this.certificatesService.removeCertificate(studentId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/certificates/certificates.module.ts",
    "content": "import { Certificate } from '@entities/certificate';\nimport { Course } from '@entities/course';\nimport { Student } from '@entities/student';\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { UsersNotificationsModule } from '../users-notifications';\nimport { ConfigModule } from '../config';\nimport { CoursesModule } from '../courses/courses.module';\nimport { CertificatesController } from './certificates.controller';\nimport { CertificationsService } from './certificates.service';\nimport { User } from '@entities/user';\nimport { HttpModule } from '@nestjs/axios';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([Certificate, Student, Course, User]),\n    ConfigModule,\n    UsersNotificationsModule,\n    CoursesModule,\n    HttpModule,\n  ],\n  controllers: [CertificatesController],\n  providers: [CertificationsService],\n})\nexport class CertificatesModule {}\n"
  },
  {
    "path": "nestjs/src/certificates/certificates.service.ts",
    "content": "import { Readable } from 'stream';\nimport { GetObjectCommand, S3 } from '@aws-sdk/client-s3';\nimport { Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { Certificate } from '@entities/certificate';\nimport { SaveCertificateDto } from './dto/save-certificate-dto';\n\nimport { ConfigService } from 'src/config';\nimport { Student } from '@entities/student';\nimport { Course } from '@entities/course';\nimport { User } from '@entities/user';\nimport { CertificateMetadataDto } from './dto/certificate-metadata.dto';\nimport { HttpService } from '@nestjs/axios';\nimport { lastValueFrom } from 'rxjs';\n\n@Injectable()\nexport class CertificationsService {\n  private s3: S3;\n\n  constructor(\n    @InjectRepository(Certificate)\n    private certificateRepository: Repository<Certificate>,\n    @InjectRepository(Course)\n    private courseRepository: Repository<Course>,\n    @InjectRepository(User)\n    private userRepository: Repository<User>,\n    private readonly configService: ConfigService,\n    private readonly httpService: HttpService,\n  ) {\n    this.s3 = new S3(this.configService.awsClient);\n  }\n\n  public async getByPublicId(publicId: string) {\n    return this.certificateRepository.findOne({\n      where: { publicId },\n      relations: ['student'],\n    });\n  }\n\n  public async getCertificateMetadata(certificate: Certificate): Promise<CertificateMetadataDto> {\n    const [user, course] = await Promise.all([\n      this.userRepository.findOneByOrFail({ id: certificate.student.userId }),\n      this.courseRepository.findOne({\n        where: { id: certificate.student.courseId },\n        relations: ['discipline'],\n      }),\n    ]);\n\n    if (!course) {\n      throw new NotFoundException('Course not found');\n    }\n\n    return new CertificateMetadataDto(certificate, course, user);\n  }\n\n  public async getFileStream(bucket: string, key: string) {\n    const { Body } = await this.s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));\n    return Body as Readable;\n  }\n\n  public async saveCertificate(studentId: number, data: SaveCertificateDto) {\n    let certificate = await this.getByPublicId(data.publicId);\n    if (certificate) {\n      await this.certificateRepository.update(certificate.id, data);\n      return;\n    }\n\n    certificate = await this.certificateRepository.findOne({ where: { studentId } });\n    if (certificate) {\n      await this.certificateRepository.update(certificate.id, data);\n      return;\n    }\n\n    await this.certificateRepository.save(data);\n  }\n\n  public async buildNotificationData(student: Student, data: SaveCertificateDto) {\n    const course = await this.courseRepository.findOneByOrFail({ id: student.courseId });\n    return {\n      userId: student.userId,\n      notification: {\n        course: course,\n        publicId: data.publicId,\n      },\n    };\n  }\n\n  public async removeCertificate(studentId: number) {\n    const certificate = await this.certificateRepository.findOneOrFail({\n      where: { studentId },\n    });\n\n    await Promise.all([\n      lastValueFrom(\n        this.httpService.delete(`${this.configService.awsServices.restApiUrl}/certificate/${certificate.s3Key}`),\n      ),\n      this.certificateRepository.remove(certificate),\n    ]);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/certificates/dto/certificate-metadata.dto.ts",
    "content": "import { Certificate } from '@entities/certificate';\nimport { Course } from '@entities/course';\nimport { User } from '@entities/user';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsDateString, IsString } from 'class-validator';\n\nexport enum CertificateCourseType {\n  Angular = 'Angular',\n  React = 'React',\n  AWS_Fundamentals = 'AWS Fundamentals',\n  AWS_Cloud_Developer = 'AWS Cloud Developer',\n  Javascript_Frontend = 'JavaScript / Front-end',\n  Javascript_Frontend_Fundamentals = 'JavaScript / Front-end Fundamentals',\n  Nodejs = 'Node.js',\n  MachineLearning = 'Machine Learning',\n  Go = 'Go',\n  iOS = 'iOS',\n  Android = 'Android',\n  Other = 'Other',\n}\n\nexport class CertificateMetadataDto {\n  constructor(certificate: Certificate, course: Course, user: User) {\n    this.id = certificate.publicId;\n    this.name = `${user.firstName} ${user.lastName}`;\n    this.issueDate = certificate.issueDate.toISOString().split('T')[0] as string;\n    this.issuer = 'RS School';\n    this.courseType = mapToCourseType(course);\n    this.courseFullName = course.fullName;\n    this.courseTrainers = course.certificateIssuer ?? 'Dzmitry Varabei';\n    this.courseDiscipline = course.discipline?.name ?? 'Other';\n  }\n\n  @ApiProperty()\n  @IsString()\n  public readonly id: string;\n\n  @ApiProperty()\n  @IsString()\n  public readonly name: string;\n\n  @ApiProperty()\n  @IsDateString()\n  public readonly issueDate: string;\n\n  @ApiProperty()\n  @IsString()\n  public readonly issuer: string;\n\n  @ApiProperty()\n  @IsString()\n  public readonly courseType: CertificateCourseType;\n\n  @ApiProperty()\n  @IsString()\n  public readonly courseTrainers: string;\n\n  @ApiProperty()\n  @IsString()\n  public readonly courseFullName: string;\n\n  @ApiProperty()\n  @IsString()\n  public readonly courseDiscipline: string;\n}\n\nfunction mapToCourseType(course: Course): CertificateCourseType {\n  const disciplineName = course.discipline?.name.toLowerCase();\n  switch (disciplineName) {\n    case 'react':\n      return CertificateCourseType.React;\n    case 'angular':\n      return CertificateCourseType.Angular;\n    case 'aws':\n      if (course.fullName.includes('Cloud Developer')) {\n        return CertificateCourseType.AWS_Cloud_Developer;\n      }\n      return CertificateCourseType.AWS_Fundamentals;\n    case 'nodejs':\n      return CertificateCourseType.Nodejs;\n    case 'machine learning':\n      return CertificateCourseType.MachineLearning;\n    case 'go':\n      return CertificateCourseType.Go;\n    case 'ios':\n      return CertificateCourseType.iOS;\n    case 'android':\n      return CertificateCourseType.Android;\n    case 'javascript':\n      if (course.fullName.includes('Pre-School')) {\n        return CertificateCourseType.Javascript_Frontend_Fundamentals;\n      }\n      return CertificateCourseType.Javascript_Frontend;\n    default:\n      return CertificateCourseType.Other;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/certificates/dto/save-certificate-dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsDateString, IsString } from 'class-validator';\n\nexport class SaveCertificateDto {\n  @ApiProperty()\n  @IsString()\n  public publicId: string;\n\n  @ApiProperty()\n  @IsNumber()\n  public studentId: number;\n\n  @ApiProperty()\n  @IsString()\n  s3Bucket: string;\n\n  @ApiProperty()\n  @IsString()\n  s3Key: string;\n\n  @ApiProperty()\n  @IsDateString()\n  issueDate: string;\n}\n"
  },
  {
    "path": "nestjs/src/cloud-api/cloud-api.module.ts",
    "content": "import { HttpModule } from '@nestjs/axios';\nimport { Module } from '@nestjs/common';\nimport { ConfigModule } from '../config';\nimport { CloudApiService } from './cloud-api.service';\n\n@Module({\n  imports: [HttpModule, ConfigModule],\n  controllers: [],\n  providers: [CloudApiService],\n  exports: [CloudApiService],\n})\nexport class CloudApiModule {}\n"
  },
  {
    "path": "nestjs/src/cloud-api/cloud-api.service.ts",
    "content": "import { HttpService } from '@nestjs/axios';\nimport { Injectable } from '@nestjs/common';\nimport { lastValueFrom } from 'rxjs';\nimport { ConfigService } from 'src/config';\n\n@Injectable()\nexport class CloudApiService {\n  private baseUrl: string;\n  private apiKey: string;\n\n  constructor(\n    private readonly httpService: HttpService,\n    readonly configService: ConfigService,\n  ) {\n    this.baseUrl = configService.awsServices.restApiUrl;\n    this.apiKey = configService.awsServices.restApiKey;\n  }\n\n  public async logErrors<T>(errors: T[]) {\n    return lastValueFrom(this.httpService.post(`${this.baseUrl}/errors`, errors, this.getHeaders()));\n  }\n\n  public async submitTask<T>(data: T[]) {\n    return lastValueFrom(this.httpService.post(`${this.baseUrl}/task`, data, this.getHeaders()));\n  }\n\n  private getHeaders() {\n    return { headers: { 'x-api-key': this.apiKey } };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/config/config.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule as NestConfigModule } from '@nestjs/config';\nimport { ConfigService } from './config.service';\n\n@Module({\n  imports: [NestConfigModule.forRoot({ isGlobal: true })],\n  controllers: [],\n  providers: [ConfigService],\n  exports: [ConfigService],\n})\nexport class ConfigModule {}\n"
  },
  {
    "path": "nestjs/src/config/config.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { ConfigService as NestConfigService } from '@nestjs/config';\nimport { AwsCredentialIdentity } from '@smithy/types';\n\ntype AuthConfig = {\n  github: {\n    clientId: string;\n    clientSecret: string;\n    callbackUrl: string;\n    scope: string[];\n    activityWebhookSecret: string;\n    integrationSiteToken: string;\n  };\n  dev: {\n    username: string;\n    admin: boolean;\n  };\n  jwt: {\n    secretKey: string;\n  };\n};\n\ntype UsersConfig = {\n  root: {\n    username: string;\n    password: string;\n  };\n  admins: string[];\n  hirers: string[];\n};\n\ntype AWSServices = {\n  restApiUrl: string;\n  restApiKey: string;\n};\n\ntype AwsClient = {\n  credentials: AwsCredentialIdentity;\n  region: string;\n};\n\ntype Secure = {\n  encryptKey: string;\n};\n\n@Injectable()\nexport class ConfigService {\n  public readonly auth: AuthConfig;\n  public readonly users: UsersConfig;\n  public readonly awsServices: AWSServices;\n  public readonly awsClient: AwsClient;\n  public readonly host: string;\n  public readonly isDev = process.env.NODE_ENV !== 'production';\n  public readonly secure: Secure;\n  public readonly openai: { apiKey: string };\n  public readonly env: 'prod' | 'staging' | 'local';\n  public readonly devToolsToggle = process.env.RSSCHOOL_DEV_TOOLS === 'true';\n\n  public readonly buckets: {\n    cdn: string;\n  };\n\n  constructor(conf: NestConfigService) {\n    this.auth = {\n      github: {\n        clientId: conf.get('RSSHCOOL_AUTH_GITHUB_CLIENT_ID') ?? '',\n        clientSecret: conf.get('RSSHCOOL_AUTH_GITHUB_CLIENT_SECRET') ?? '',\n        callbackUrl: conf.get('RSSHCOOL_AUTH_GITHUB_CALLBACK') ?? '',\n        scope: ['user:email'],\n        activityWebhookSecret: conf.get('RSSHCOOL_AUTH_GITHUB_WEBHOOK_ACTIVITY_SECRET', 'activity-webhook'),\n        // token for rolling-scopes/site integration\n        integrationSiteToken: conf.get('RSSHCOOL_AUTH_GITHUB_INTEGRATION_SITE_TOKEN', ''),\n      },\n      dev: {\n        username: conf.get('RSSCHOOL_AUTH_DEV_USERNAME') ?? '',\n        admin: conf.get<string>('RSSCHOOL_AUTH_DEV_ADMIN')?.toLowerCase() === 'true',\n      },\n      jwt: {\n        secretKey: conf.get('RSSHCOOL_AUTH_JWT_SECRET_KEY') ?? 'secret',\n      },\n    };\n\n    this.openai = {\n      apiKey: conf.get('RSSHCOOL_OPENAI_API_KEY') || '',\n    };\n\n    this.awsClient = {\n      region: conf.get('RSSHCOOL_AWS_REGION') ?? 'eu-central-1',\n      credentials: {\n        accessKeyId: conf.get('RSSHCOOL_AWS_ACCESS_KEY_ID') ?? '',\n        secretAccessKey: conf.get('RSSHCOOL_AWS_SECRET_ACCESS_KEY') || '',\n      },\n    };\n\n    this.awsServices = {\n      restApiUrl: conf.get('RSSHCOOL_AWS_REST_API_URL') || '',\n      restApiKey: conf.get('RSSHCOOL_AWS_REST_API_KEY') || '',\n    };\n\n    this.users = {\n      root: {\n        username: conf.get('RSSHCOOL_USERS_CLOUD_USERNAME') ?? '',\n        password: conf.get('RSSHCOOL_USERS_CLOUD_PASSWORD') ?? '',\n      },\n      hirers: conf.get('RSSHCOOL_USERS_HIRERS')?.split(',') ?? [],\n      admins: conf.get('RSSHCOOL_USERS_ADMINS')?.split(',') ?? [],\n    };\n\n    this.secure = {\n      encryptKey: conf.get('RSSHCOOL_SECURE_ENCRYPT_KEY') ?? 'secret',\n    };\n\n    this.host = conf.get('RSSHCOOL_HOST') ?? '';\n\n    this.buckets = { cdn: 'cdn.rs.school' };\n\n    this.env = conf.get('RS_ENV') === 'staging' ? 'staging' : this.isDev ? 'local' : 'prod';\n  }\n\n  authWithDevUser(username: string) {\n    if (!this.devToolsToggle) {\n      return;\n    }\n    this.auth.dev.username = username;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/config/index.ts",
    "content": "export * from './config.service';\nexport * from './config.module';\n"
  },
  {
    "path": "nestjs/src/constants.ts",
    "content": "// time in seconds\nexport const DEFAULT_CACHE_TTL = 60; // 1 minute\n\nexport const ONE_HOUR_CACHE_TTL = 60 * 60; // 1 hour\n"
  },
  {
    "path": "nestjs/src/contributors/contributors.controller.ts",
    "content": "import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { ContributorsService } from './contributors.service';\nimport { CreateContributorDto, ContributorDto, UpdateContributorDto } from './dto';\n\n@Controller('contributors')\n@ApiTags('contributors')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class ContributorsController {\n  constructor(private readonly service: ContributorsService) {}\n\n  @Post('/')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'createContributor' })\n  @ApiOkResponse({ type: ContributorDto })\n  public async create(@Body() dto: CreateContributorDto) {\n    const data = await this.service.create(dto);\n    return new ContributorDto(data);\n  }\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getContributors' })\n  @ApiOkResponse({ type: [ContributorDto] })\n  public async getAll() {\n    const items = await this.service.getAll();\n    return items.map(item => new ContributorDto(item));\n  }\n\n  @Get('/:id')\n  @ApiOperation({ operationId: 'getContributor' })\n  @ApiOkResponse({ type: ContributorDto })\n  public async getContributor(@Param('id', ParseIntPipe) id: number) {\n    const item = await this.service.getById(id);\n    return new ContributorDto(item);\n  }\n\n  @Delete('/:id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'deleteContributor' })\n  public async delete(@Param('id', ParseIntPipe) id: number) {\n    return this.service.delete(id);\n  }\n\n  @Patch('/:id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'updateContributor' })\n  @ApiOkResponse({ type: ContributorDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateContributorDto) {\n    const data = await this.service.update(id, dto);\n    return new ContributorDto(data);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/contributors/contributors.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { ContributorsController } from './contributors.controller';\nimport { ContributorsService } from './contributors.service';\nimport { Contributor } from '@entities/contributor';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Contributor])],\n  controllers: [ContributorsController],\n  providers: [ContributorsService],\n  exports: [ContributorsService],\n})\nexport class ContributorsModule {}\n"
  },
  {
    "path": "nestjs/src/contributors/contributors.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CreateContributorDto, UpdateContributorDto } from './dto';\nimport { Contributor } from '@entities/contributor';\n\n@Injectable()\nexport class ContributorsService {\n  constructor(\n    @InjectRepository(Contributor)\n    private repository: Repository<Contributor>,\n  ) {}\n\n  public async getAll() {\n    return this.repository.find({ relations: ['user'] });\n  }\n\n  public async getById(id: number) {\n    return this.repository.findOneOrFail({\n      where: { id },\n      relations: ['user'],\n    });\n  }\n\n  public async create(data: CreateContributorDto) {\n    const { id } = await this.repository.save(data);\n    return this.getById(id);\n  }\n\n  public async update(id: number, data: UpdateContributorDto) {\n    await this.repository.update(id, data);\n    return this.getById(id);\n  }\n\n  public async delete(id: number): Promise<void> {\n    await this.repository.softDelete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/contributors/dto/contributor.dto.ts",
    "content": "import { Contributor } from '@entities/contributor';\nimport { User } from '@entities/user';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class ContributorUserDto {\n  constructor(user: User) {\n    this.id = user.id;\n    this.githubId = user.githubId;\n    this.firstName = user.firstName;\n    this.lastName = user.lastName;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  githubId: string;\n\n  @ApiProperty()\n  firstName: string;\n\n  @ApiProperty()\n  lastName: string;\n}\n\nexport class ContributorDto {\n  constructor(contributor: Contributor) {\n    this.id = contributor.id;\n    this.description = contributor.description;\n    this.createdDate = contributor.createdDate;\n    this.updatedDate = contributor.updatedDate;\n\n    this.user = new ContributorUserDto(contributor.user);\n  }\n\n  @ApiProperty()\n  description: string;\n\n  @ApiProperty({ type: () => ContributorUserDto })\n  user: ContributorUserDto;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public createdDate: string;\n\n  @ApiProperty()\n  public updatedDate: string;\n}\n"
  },
  {
    "path": "nestjs/src/contributors/dto/create-contributor.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nexport class CreateContributorDto {\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  description: string;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  userId: number;\n}\n"
  },
  {
    "path": "nestjs/src/contributors/dto/index.ts",
    "content": "export * from './create-contributor.dto';\nexport * from './update-contributor.dto';\nexport * from './contributor.dto';\n"
  },
  {
    "path": "nestjs/src/contributors/dto/update-contributor.dto.ts",
    "content": "import { ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nexport class UpdateContributorDto {\n  @IsNotEmpty()\n  @IsString()\n  @ApiPropertyOptional()\n  description?: string;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiPropertyOptional()\n  userId?: number;\n}\n"
  },
  {
    "path": "nestjs/src/contributors/index.ts",
    "content": "export * from './contributors.module';\n"
  },
  {
    "path": "nestjs/src/core/core.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ConfigModule } from '../config';\nimport { JwtService } from './jwt/jwt.service';\n@Module({\n  imports: [ConfigModule],\n  controllers: [],\n  providers: [JwtService],\n  exports: [JwtService],\n})\nexport class CoreModule {}\n"
  },
  {
    "path": "nestjs/src/core/decorators/index.ts",
    "content": "export * from './student-id.decorator';\n"
  },
  {
    "path": "nestjs/src/core/decorators/student-id.decorator.ts",
    "content": "import { createParamDecorator, ExecutionContext } from '@nestjs/common';\n\nexport const StudentId = createParamDecorator((_: unknown, ctx: ExecutionContext): number => {\n  const request = ctx.switchToHttp().getRequest();\n  const courseId = request.params.courseId;\n  return request.user.courses[courseId]?.studentId;\n});\n"
  },
  {
    "path": "nestjs/src/core/dto/id-name.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'class-validator';\n\nexport class IdNameDto {\n  constructor(obj: { name: string; id: number }) {\n    this.id = obj.id;\n    this.name = obj.name;\n  }\n\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  id: number;\n}\n"
  },
  {
    "path": "nestjs/src/core/dto/index.ts",
    "content": "export * from './person.dto';\nexport * from './pagination.dto';\nexport * from './id-name.dto';\n"
  },
  {
    "path": "nestjs/src/core/dto/pagination.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class PaginationDto {\n  constructor(pageSize: number, current: number, total: number, totalPages: number) {\n    this.pageSize = pageSize;\n    this.current = current;\n    this.total = total;\n    this.totalPages = totalPages;\n  }\n\n  @ApiProperty()\n  public pageSize: number;\n\n  @ApiProperty()\n  public current: number;\n\n  @ApiProperty()\n  public total: number;\n\n  @ApiProperty()\n  public totalPages: number;\n}\n"
  },
  {
    "path": "nestjs/src/core/dto/person.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nexport class PersonDto {\n  constructor(person: { firstName?: string; lastName?: string; githubId?: string; id: number }) {\n    this.id = person?.id;\n    this.name = PersonDto.getName(person);\n    this.githubId = person?.githubId || '';\n  }\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  githubId: string;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  id: number;\n\n  public static getQueryFields(base = '') {\n    const prefix = base ? `${base}.` : '';\n    return ['id', 'firstName', 'lastName', 'githubId'].map(i => `${prefix}${i}`);\n  }\n\n  public static getName(person: { firstName?: string; lastName?: string }) {\n    return [person?.firstName || '', person?.lastName || ''].join(' ').trim() || '(Empty)';\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/filters/entity-not-found.filter.ts",
    "content": "import { ArgumentsHost, Catch, ExceptionFilter, NotFoundException } from '@nestjs/common';\nimport { EntityNotFoundError } from 'typeorm';\n\nconst exception = new NotFoundException();\n\n@Catch(EntityNotFoundError)\nexport class EntityNotFoundFilter implements ExceptionFilter {\n  catch(_: EntityNotFoundError, host: ArgumentsHost) {\n    const context = host.switchToHttp();\n    const response = context.getResponse();\n    return response.status(404).json(exception.getResponse());\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/filters/index.ts",
    "content": "export * from './entity-not-found.filter';\nexport * from './sentry.filter';\n"
  },
  {
    "path": "nestjs/src/core/filters/sentry.filter.ts",
    "content": "import { ArgumentsHost, Catch } from '@nestjs/common';\nimport { BaseExceptionFilter } from '@nestjs/core';\nimport * as Sentry from '@sentry/node';\n\n@Catch()\nexport class SentryFilter extends BaseExceptionFilter {\n  catch(exception: unknown, host: ArgumentsHost) {\n    Sentry.captureException(exception);\n    super.catch(exception, host);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/jwt/jwt.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { sign, verify } from 'jsonwebtoken';\nimport { ConfigService } from '../../config';\nimport { JWT_TOKEN_EXPIRATION } from '../../auth/constants';\nimport { AuthUser, JwtToken } from '../../auth/auth-user.model';\n\n@Injectable()\nexport class JwtService {\n  private readonly secretKey: string = '';\n\n  constructor(readonly configService: ConfigService) {\n    this.secretKey = configService.auth.jwt.secretKey;\n  }\n\n  public createToken(payload: AuthUser) {\n    const tokenPayload: JwtToken = {\n      id: payload.id,\n      githubId: payload.githubId,\n      isAdmin: payload.isAdmin,\n      isHirer: payload.isHirer,\n    };\n    const jwt: string = sign(tokenPayload, this.secretKey, {\n      expiresIn: JWT_TOKEN_EXPIRATION,\n    });\n    return jwt;\n  }\n\n  public createPublicCalendarToken<T>(payload: T) {\n    const jwt: string = sign(JSON.parse(JSON.stringify(payload)) as object, this.secretKey, { expiresIn: '365d' });\n    return jwt;\n  }\n\n  public validateToken<T>(token: string): T {\n    return verify(token, this.secretKey) as T;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/middlewares/index.ts",
    "content": "export * from './logger.middleware';\nexport * from './no-cache.middleware';\n"
  },
  {
    "path": "nestjs/src/core/middlewares/logger.middleware.ts",
    "content": "import { Injectable, Logger, NestMiddleware } from '@nestjs/common';\nimport { NextFunction, Request, Response } from 'express';\n\nconst NS_PER_SEC = 1e9;\nconst NS_TO_MS = 1e6;\n\n@Injectable()\nexport class LoggingMiddleware implements NestMiddleware {\n  private logger = new Logger(LoggingMiddleware.name);\n\n  public use(req: Request, res: Response, next: NextFunction) {\n    const start = process.hrtime();\n\n    res.on('finish', () => {\n      this.logger.log({\n        msg: 'Processed request',\n        url: req.url,\n        query: req.query,\n        method: req.method,\n        status: res.statusCode,\n        duration: this.getDurationInMilliseconds(start),\n        userId: (req.user as { id: number })?.id ?? null,\n      });\n    });\n\n    next();\n  }\n\n  private getDurationInMilliseconds(start: [number, number]) {\n    const [diff0, diff1] = process.hrtime(start);\n    return (diff0 * NS_PER_SEC + diff1) / NS_TO_MS;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/middlewares/no-cache.middleware.ts",
    "content": "import { Injectable, NestMiddleware } from '@nestjs/common';\nimport { NextFunction, Request, Response } from 'express';\n\n@Injectable()\nexport class NoCacheMiddleware implements NestMiddleware {\n  public use(_: Request, res: Response, next: NextFunction) {\n    res.setHeader('Cache-Control', 'no-cache');\n    next();\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/paginate/dto/Paginate.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { PaginationMeta } from '../index';\n\nexport class PaginationMetaDto implements PaginationMeta {\n  constructor(paginationMeta: PaginationMeta) {\n    this.itemCount = paginationMeta.itemCount;\n    this.total = paginationMeta.total;\n    this.current = paginationMeta.current;\n    this.pageSize = paginationMeta.pageSize;\n    this.totalPages = paginationMeta.totalPages;\n  }\n\n  @ApiProperty({ type: Number })\n  itemCount: number;\n\n  @ApiProperty({ type: Number })\n  total: number;\n\n  @ApiProperty({ type: Number })\n  current: number;\n\n  @ApiProperty({ type: Number })\n  pageSize: number;\n\n  @ApiProperty({ type: Number })\n  totalPages: number;\n}\n"
  },
  {
    "path": "nestjs/src/core/paginate/index.test.ts",
    "content": "import { paginate } from './index';\nimport { ObjectLiteral, SelectQueryBuilder } from 'typeorm';\n\nconst createMockQueryBuilder = (items: ObjectLiteral[], total: number) => {\n  const mockQueryBuilder = {\n    take: vi.fn().mockReturnThis(),\n    skip: vi.fn().mockReturnThis(),\n    getManyAndCount: vi.fn().mockResolvedValue([items, total]),\n  } as unknown as SelectQueryBuilder<ObjectLiteral>;\n\n  return mockQueryBuilder;\n};\n\ndescribe('paginate', () => {\n  describe('basic functionality', () => {\n    it('should return paginated results with correct meta data', async () => {\n      const mockItems = [{ id: 1 }, { id: 2 }, { id: 3 }];\n      const mockTotal = 10;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 3 });\n\n      expect(result.items).toEqual(mockItems);\n      expect(result.meta).toEqual({\n        itemCount: 3,\n        total: 10,\n        current: 1,\n        pageSize: 3,\n        totalPages: 4,\n      });\n\n      expect(queryBuilder.take).toHaveBeenCalledWith(3);\n      expect(queryBuilder.skip).toHaveBeenCalledWith(0);\n    });\n\n    it('should calculate correct skip value for page 2', async () => {\n      const mockItems = [{ id: 4 }, { id: 5 }];\n      const mockTotal = 10;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      await paginate(queryBuilder, { page: 2, limit: 3 });\n\n      expect(queryBuilder.skip).toHaveBeenCalledWith(3);\n    });\n\n    it('should calculate correct skip value for page 3', async () => {\n      const mockItems = [{ id: 7 }];\n      const mockTotal = 10;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      await paginate(queryBuilder, { page: 3, limit: 3 });\n\n      expect(queryBuilder.skip).toHaveBeenCalledWith(6);\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle page 0 by treating it as page 1', async () => {\n      const mockItems = [{ id: 1 }];\n      const mockTotal = 5;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 0, limit: 2 });\n\n      expect(queryBuilder.skip).toHaveBeenCalledWith(0);\n      expect(result.meta.current).toBe(0);\n    });\n\n    it('should handle negative page numbers', async () => {\n      const mockItems = [{ id: 1 }];\n      const mockTotal = 5;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      await paginate(queryBuilder, { page: -1, limit: 2 });\n\n      expect(queryBuilder.skip).toHaveBeenCalledWith(0);\n    });\n\n    it('should handle empty results', async () => {\n      const mockItems: ObjectLiteral[] = [];\n      const mockTotal = 0;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 10 });\n\n      expect(result.items).toEqual([]);\n      expect(result.meta).toEqual({\n        itemCount: 0,\n        total: 0,\n        current: 1,\n        pageSize: 10,\n        totalPages: 0,\n      });\n    });\n\n    it('should handle limit of 1', async () => {\n      const mockItems = [{ id: 1 }];\n      const mockTotal = 5;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 1 });\n\n      expect(result.meta.totalPages).toBe(5);\n      expect(result.meta.pageSize).toBe(1);\n    });\n\n    it('should handle large limit that exceeds total items', async () => {\n      const mockItems = [{ id: 1 }, { id: 2 }];\n      const mockTotal = 2;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 100 });\n\n      expect(result.meta.totalPages).toBe(1);\n      expect(result.meta.itemCount).toBe(2);\n      expect(result.meta.pageSize).toBe(100);\n    });\n  });\n\n  describe('meta data calculations', () => {\n    it('should calculate totalPages correctly when total is divisible by limit', async () => {\n      const mockItems = [{ id: 1 }, { id: 2 }];\n      const mockTotal = 10;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 5 });\n\n      expect(result.meta.totalPages).toBe(2);\n    });\n\n    it('should calculate totalPages correctly when total is not divisible by limit', async () => {\n      const mockItems = [{ id: 1 }, { id: 2 }];\n      const mockTotal = 11;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 5 });\n\n      expect(result.meta.totalPages).toBe(3);\n    });\n\n    it('should have itemCount match actual returned items length', async () => {\n      const mockItems = [{ id: 1 }, { id: 2 }];\n      const mockTotal = 100;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 10 });\n\n      expect(result.meta.itemCount).toBe(2);\n      expect(result.items.length).toBe(2);\n    });\n\n    it('should preserve current page in meta', async () => {\n      const mockItems = [{ id: 4 }];\n      const mockTotal = 10;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 5, limit: 2 });\n\n      expect(result.meta.current).toBe(5);\n    });\n  });\n\n  describe('different data types', () => {\n    it('should work with complex objects', async () => {\n      const mockItems = [\n        { id: 1, name: 'John', email: 'john@example.com' },\n        { id: 2, name: 'Jane', email: 'jane@example.com' },\n      ];\n      const mockTotal = 20;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 1, limit: 2 });\n\n      expect(result.items).toEqual(mockItems);\n      expect(result.meta.itemCount).toBe(2);\n      expect(result.meta.total).toBe(20);\n    });\n\n    it('should work with different object structures', async () => {\n      const mockItems = [\n        { value: 'item1', type: 'A' },\n        { value: 'item2', type: 'B' },\n        { value: 'item3', type: 'C' },\n      ];\n      const mockTotal = 15;\n      const queryBuilder = createMockQueryBuilder(mockItems, mockTotal);\n\n      const result = await paginate(queryBuilder, { page: 2, limit: 5 });\n\n      expect(result.items).toEqual(mockItems);\n      expect(result.meta.current).toBe(2);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/core/paginate/index.ts",
    "content": "import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';\n\nexport interface PaginationMeta {\n  /**\n   * the amount of items on this specific page\n   */\n  itemCount: number;\n  /**\n   * the total amount of items\n   */\n  total: number;\n  /**\n   * the amount of items that were requested per page\n   */\n  pageSize: number;\n  /**\n   * the total amount of pages in this paginator\n   */\n  totalPages: number;\n  /**\n   * the current page this paginator \"points\" to\n   */\n  current: number;\n}\n\nexport async function paginate<T extends ObjectLiteral>(\n  queryBuilder: SelectQueryBuilder<T>,\n  { page, limit }: { page: number; limit: number },\n): Promise<{ items: T[]; meta: PaginationMeta }> {\n  const queryBuilderWithLimit = queryBuilder.take(limit).skip(Math.max((page - 1) * limit, 0));\n\n  const [items, total] = await queryBuilderWithLimit.getManyAndCount();\n  const totalPages = Math.ceil(total / limit);\n\n  return {\n    items,\n    meta: { itemCount: items.length, total: total, current: page, pageSize: limit, totalPages },\n  };\n}\n"
  },
  {
    "path": "nestjs/src/core/pino.ts",
    "content": "import { Params } from 'nestjs-pino';\nimport cloudwatchStream from '@apalchys/pino-cloudwatch';\n\nconst devMode = process.env.NODE_ENV !== 'production' && !process.env.AWS_LAMBDA;\n\nconst awsAccessKeyId = process.env.RSSHCOOL_AWS_ACCESS_KEY_ID;\nconst awsSecretAccessKey = process.env.RSSHCOOL_AWS_SECRET_ACCESS_KEY;\nconst awsRegion = process.env.RSSHCOOL_AWS_REGION;\n\nexport function getPinoHttp(): Params['pinoHttp'] {\n  const pinoOptions = {\n    base: {},\n    autoLogging: false,\n    quietReqLogger: true,\n  };\n\n  if (!devMode && awsAccessKeyId && awsSecretAccessKey) {\n    return [\n      pinoOptions,\n      cloudwatchStream({\n        interval: 2000,\n        aws_access_key_id: awsAccessKeyId,\n        aws_secret_access_key: awsSecretAccessKey,\n        aws_region: awsRegion,\n        group: '/app/rsschool-api',\n        stream: `${new Date().toISOString().split('T')[0]}-nestjs`,\n      }),\n    ];\n  }\n\n  return pinoOptions;\n}\n"
  },
  {
    "path": "nestjs/src/core/subscribers/base-subscriber.ts",
    "content": "import { EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';\nimport { History } from '@entities/history';\n\nexport class BaseSubscriber implements EntitySubscriberInterface {\n  async afterInsert?(event: InsertEvent<{ id: number }>): Promise<void> {\n    await event.manager.save(\n      History,\n      {\n        event: event.metadata.tableName,\n        entityId: event.entity.id,\n        update: event.entity,\n        operation: 'insert',\n      },\n      {\n        listeners: false,\n      },\n    );\n  }\n\n  async beforeUpdate?(event: UpdateEvent<{ id: number }>): Promise<void> {\n    if (!event.entity || !event.entity.id) {\n      console.warn('subscriber missing entity id');\n      return;\n    }\n\n    const old = await event.manager.findOneBy(event.metadata.target, { id: event.entity.id });\n    if (!old) {\n      return;\n    }\n    await event.manager.save(\n      History,\n      {\n        event: event.metadata.tableName,\n        update: event.entity,\n        entityId: old.id,\n        previous: old,\n        operation: 'update',\n      },\n      {\n        listeners: false,\n      },\n    );\n  }\n\n  async beforeRemove?(event: RemoveEvent<{ id: number }>): Promise<void> {\n    if (!event.entity) return;\n\n    const entityId = event.entity.id ?? event.entityId;\n    if (!entityId) {\n      console.warn('subscriber missing entity id');\n      return;\n    }\n\n    const old = await event.manager.findOneBy<{ id: number }>(event.metadata.target, { id: entityId });\n\n    await event.manager.save(\n      History,\n      {\n        event: event.metadata.tableName,\n        operation: 'remove',\n        entityId: old?.id,\n        update: event.entity,\n        previous: old,\n      },\n      {\n        listeners: false,\n      },\n    );\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/subscribers/course-event.subscriber.ts",
    "content": "import { CourseEvent } from '@entities/courseEvent';\nimport { EventSubscriber } from 'typeorm';\nimport { BaseSubscriber } from './base-subscriber';\n\n@EventSubscriber()\nexport class CourseEventSubscriber extends BaseSubscriber {\n  listenTo() {\n    return CourseEvent;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/subscribers/course-task.subscriber.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { EventSubscriber } from 'typeorm';\nimport { BaseSubscriber } from './base-subscriber';\n\n@EventSubscriber()\nexport class CourseTaskSubscriber extends BaseSubscriber {\n  listenTo() {\n    return CourseTask;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/templates/index.ts",
    "content": "import handlebars from 'handlebars';\nimport { formatInTimeZone } from 'date-fns-tz';\n\nhandlebars.registerHelper('truncate', (string: string, options: { maxLength: number }) => {\n  const { maxLength = 50 } = options || {};\n  if (string.length > maxLength) {\n    return `${string.slice(0, maxLength)}...`;\n  }\n  return string;\n});\n\nhandlebars.registerHelper('capitalize', (string: string) => {\n  return string.charAt(0).toUpperCase() + string.slice(1);\n});\n\nhandlebars.registerHelper('ifEquals', function (arg1, arg2, options) {\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n  // @ts-ignore\n  return arg1 == arg2 ? options.fn(this) : options.inverse(this);\n});\n\nhandlebars.registerHelper('formatDateTime', (dateTime: string) => {\n  if (!dateTime) return '';\n  return formatInTimeZone(dateTime, 'UTC', 'yyyy-MM-dd HH:mm');\n});\n"
  },
  {
    "path": "nestjs/src/core/validation/index.ts",
    "content": "export * from './validation.exception';\nexport * from './validation.filter';\n"
  },
  {
    "path": "nestjs/src/core/validation/validation.exception.ts",
    "content": "import { BadRequestException } from '@nestjs/common';\n\nexport class ValidationException extends BadRequestException {\n  constructor(public validationErrors: string[]) {\n    super();\n  }\n}\n"
  },
  {
    "path": "nestjs/src/core/validation/validation.filter.ts",
    "content": "import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common';\nimport { ValidationException } from './validation.exception';\n\n@Catch(ValidationException)\nexport class ValidationFilter implements ExceptionFilter {\n  private logger = new Logger(ValidationFilter.name);\n\n  catch(exception: ValidationException, host: ArgumentsHost) {\n    const context = host.switchToHttp();\n    const response = context.getResponse();\n    this.logger.warn(exception.validationErrors.join('\\n'));\n    return response.status(400).json({\n      statusCode: 400,\n      errors: exception.validationErrors,\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-access.service.ts",
    "content": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Between, FindOptionsWhere, In, Repository } from 'typeorm';\nimport { AuthUser, CourseRole, Role } from '../auth';\nimport { ExpelledStatsService } from './expelled-stats.service';\nimport { LeaveCourseRequestDto } from './dto';\nimport { Course, Student } from '../../../server/src/models';\n\n// use this as a mark for identifying self-expelled students.\nconst SELF_EXPELLED_MARK = 'Self expelled from the course';\n\n@Injectable()\nexport class CourseAccessService {\n  constructor(\n    @InjectRepository(Student)\n    readonly studentRepository: Repository<Student>,\n    @InjectRepository(Course)\n    readonly courseRepository: Repository<Course>,\n    private readonly expelledStatsService: ExpelledStatsService,\n  ) {}\n\n  public async canAccessCourse(user: AuthUser, courseId: number): Promise<boolean> {\n    if (user.appRoles?.includes(Role.Admin)) {\n      return true;\n    }\n\n    return !!user.courses[courseId];\n  }\n\n  public async getUserAllowedCourseIds(user: AuthUser, ids: number[] = [], year: number): Promise<number[]> {\n    const isAdmin = user.appRoles?.includes(Role.Admin);\n    const userCourses: number[] = isAdmin ? ids : Object.keys(user?.courses).map(Number);\n    const coursesIntersection = ids.filter(id => userCourses.includes(id));\n\n    if (year) {\n      // if the year is provided, but the course list\n      // is empty, return all courses for the given year\n      // for admins and the courses that the user is enrolled\n      // in for students.\n      const startDate = new Date(year.toString());\n      const endDate = new Date((year + 1).toString());\n\n      const condition: FindOptionsWhere<Course> = { startDate: Between(startDate, endDate) };\n\n      if (!isAdmin) {\n        condition.id = In(userCourses);\n      }\n\n      const courses = await this.courseRepository.find({\n        where: condition,\n        select: ['id'],\n      });\n\n      return courses.map(({ id }) => id);\n    }\n\n    return coursesIntersection;\n  }\n\n  public canAccessCourseAsManager(user: AuthUser, courseId: number): boolean {\n    return user.courses[courseId]?.roles.includes(CourseRole.Manager) || user.isAdmin;\n  }\n\n  public async leaveAsStudent(\n    courseId: number,\n    studentId: number,\n    leaveCourseDto: LeaveCourseRequestDto,\n  ): Promise<void> {\n    const [student, course] = await Promise.all([\n      this.studentRepository.findOneByOrFail({ id: studentId }),\n      this.courseRepository.findOneByOrFail({ id: courseId }),\n    ]);\n\n    if (course.completed) throw new BadRequestException('Course is already completed');\n    if (student.isExpelled) throw new BadRequestException('Student is not active');\n\n    await this.studentRepository.update(student.id, {\n      mentorId: null,\n      isExpelled: true,\n      expellingReason: `${SELF_EXPELLED_MARK}. ${leaveCourseDto.otherComment || ''}`,\n      endDate: new Date(),\n    });\n\n    await this.expelledStatsService.submitLeaveSurvey(\n      student.userId,\n      courseId,\n      leaveCourseDto.reasonForLeaving,\n      leaveCourseDto.otherComment,\n    );\n  }\n\n  public async rejoinAsStudent(courseId: number, studentId: number): Promise<void> {\n    const [student, course] = await Promise.all([\n      this.studentRepository.findOneByOrFail({ id: studentId }),\n      this.courseRepository.findOneByOrFail({ id: courseId }),\n    ]);\n\n    if (course.completed) throw new BadRequestException('Course is already completed');\n    if (!student.isExpelled) throw new BadRequestException('Student is active');\n\n    if (!student.expellingReason?.startsWith(SELF_EXPELLED_MARK)) {\n      throw new BadRequestException('Student is not expelled by self');\n    }\n\n    await this.studentRepository.update(student.id, {\n      isExpelled: false,\n      expellingReason: 'Re-joined course',\n      endDate: null,\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-events/course-events.controller.ts",
    "content": "import { Body, Controller, Delete, Param, ParseIntPipe, Post, Put, UseGuards } from '@nestjs/common';\nimport {\n  ApiBadRequestResponse,\n  ApiForbiddenResponse,\n  ApiOkResponse,\n  ApiOperation,\n  ApiParam,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { CourseGuard, CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { CourseEventsService } from './course-events.service';\nimport { CourseEventDto } from './dto/course-event.dto';\nimport { CreateCourseEventDto } from './dto/create-course-event.dto';\nimport { UpdateCourseEventDto } from './dto/update-course-event.dto';\n\n@Controller('courses/:courseId/events')\n@ApiTags('courses events')\n@UseGuards(DefaultGuard, CourseGuard, RoleGuard)\nexport class CourseEventsController {\n  constructor(private courseEventsService: CourseEventsService) {}\n\n  @Post('/')\n  @ApiOkResponse({ type: [CourseEventDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'createCourseEvent' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager], true)\n  public async createCourseTask(@Param('courseId', ParseIntPipe) courseId: number, @Body() dto: CreateCourseEventDto) {\n    const result = await this.courseEventsService.createCourseEvent({\n      courseId,\n      ...dto,\n    });\n\n    return new CourseEventDto(result);\n  }\n\n  @Put('/:courseEventId')\n  @ApiOkResponse()\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'updateCourseEvent' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager], true)\n  public async updateCourseTask(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('courseEventId', ParseIntPipe) courseEventId: number,\n    @Body() dto: UpdateCourseEventDto,\n  ) {\n    await this.courseEventsService.updateCourseEvent(courseEventId, {\n      courseId,\n      id: courseEventId,\n      ...dto,\n    });\n  }\n\n  @Delete('/:courseEventId')\n  @ApiOkResponse()\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiParam({ name: 'courseId' })\n  @ApiOperation({ operationId: 'deleteCourseEvent' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager], true)\n  public async deleteCourseEvent(@Param('courseEventId', ParseIntPipe) courseEventId: number) {\n    await this.courseEventsService.deleteCourseEvent(courseEventId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-events/course-events.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CourseEvent } from '@entities/courseEvent';\n\nexport enum Status {\n  Started = 'started',\n  InProgress = 'inprogress',\n  Finished = 'finished',\n}\n\n@Injectable()\nexport class CourseEventsService {\n  constructor(\n    @InjectRepository(CourseEvent)\n    readonly courseEventRepository: Repository<CourseEvent>,\n  ) {}\n\n  public async createCourseEvent(courseEvent: Partial<Omit<CourseEvent, 'organizer'> & { organizer: { id: number } }>) {\n    const { id } = await this.courseEventRepository.save(courseEvent);\n    return this.courseEventRepository.findOneOrFail({ where: { id }, relations: ['organizer', 'event'] });\n  }\n\n  public async updateCourseEvent(\n    id: number,\n    courseEvent: Partial<Omit<CourseEvent, 'organizer'> & { organizer: { id: number } }>,\n  ) {\n    await this.courseEventRepository.update(id, courseEvent);\n    return this.courseEventRepository.findOneByOrFail({ id });\n  }\n\n  public async deleteCourseEvent(id: number) {\n    const entity = await this.courseEventRepository.findOneByOrFail({ id });\n    return this.courseEventRepository.remove(entity);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-events/dto/course-event.dto.ts",
    "content": "import { CourseEvent } from '@entities/courseEvent';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { PersonDto } from 'src/core/dto';\n\nexport enum EventType {\n  Online = 'lecture_online',\n  Offline = 'lecture_offline',\n  Mixed = 'lecture_mixed',\n  SelfStudy = 'lecture_self_study',\n  Warmup = 'warmup',\n  Info = 'info',\n  Workshop = 'workshop',\n  Meetup = 'meetup',\n  CrossCheckdeadline = 'cross_check_deadline',\n  Webinar = 'webinar',\n  Special = 'special',\n}\n\nexport class CourseEventDto {\n  constructor(courseEvent: CourseEvent) {\n    this.id = courseEvent.id;\n    this.name = courseEvent.event.name;\n    this.type = courseEvent.event.type as EventType;\n    this.description = courseEvent.event.description;\n    this.descriptionUrl = courseEvent.event.descriptionUrl;\n    this.dateTime = (courseEvent.dateTime as Date).toISOString();\n    this.organizer = new PersonDto(courseEvent.organizer);\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty({ enum: EventType })\n  type: EventType;\n\n  @ApiProperty()\n  description: string;\n\n  @ApiProperty()\n  descriptionUrl: string;\n\n  @ApiProperty()\n  dateTime: string;\n\n  @ApiProperty()\n  endTime: string;\n\n  @ApiProperty()\n  organizer: PersonDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-events/dto/create-course-event.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class Organizer {\n  @ApiProperty()\n  @IsNumber()\n  id: number;\n}\n\nexport class CreateCourseEventDto {\n  @ApiProperty()\n  @IsNumber()\n  eventId: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  special?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  dateTime?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  endTime?: string;\n\n  @ApiProperty({ required: false })\n  @IsNumber()\n  @IsOptional()\n  duration?: number;\n\n  @ApiProperty({ required: false })\n  @IsString()\n  @IsOptional()\n  place?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  organizer?: Organizer;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  organizerId?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  broadcastUrl?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  coordinator?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  comment?: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-events/dto/update-course-event.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsOptional, IsString } from 'class-validator';\nimport { Organizer } from './create-course-event.dto';\n\nexport class UpdateCourseEventDto {\n  @ApiProperty({ required: false })\n  @IsOptional()\n  special?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  dateTime?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  endTime?: string;\n\n  @ApiProperty({ required: false })\n  @IsNumber()\n  @IsOptional()\n  duration?: number;\n\n  @ApiProperty({ required: false })\n  @IsString()\n  @IsOptional()\n  place?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  organizer?: Organizer;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  organizerId?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  broadcastUrl?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  coordinator?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  comment?: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-events/index.ts",
    "content": "export * from './course-events.service';\nexport * from './course-events.controller';\n"
  },
  {
    "path": "nestjs/src/courses/course-mentors/course-mentors.controller.ts",
    "content": "import { CacheTTL } from '@nestjs/cache-manager';\nimport { Controller, Get, Param, ParseIntPipe, Res, UseGuards } from '@nestjs/common';\nimport { ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { Response } from 'express';\nimport { parseAsync, transforms } from 'json2csv';\nimport { DEFAULT_CACHE_TTL } from 'src/constants';\nimport { CourseGuard, CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { CourseMentorsService } from './course-mentors.service';\nimport { MentorDetailsDto } from './dto/mentor-details.dto';\nimport { SearchMentorDto } from './dto/search-mentor.dto';\n\n@Controller('course/:courseId/mentors')\n@ApiTags('course mentors')\nexport class CourseMentorsController {\n  constructor(private readonly courseMentorsService: CourseMentorsService) {}\n\n  @Get('details')\n  @ApiOperation({ operationId: 'getMentorsDetails' })\n  @ApiOkResponse({ type: [MentorDetailsDto] })\n  @ApiForbiddenResponse()\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor], true)\n  public async getMentorsDetails(@Param('courseId', ParseIntPipe) courseId: number) {\n    const mentors = await this.courseMentorsService.getMentorsWithStats(courseId);\n    return mentors.map(mentor => new MentorDetailsDto(mentor));\n  }\n\n  @Get('details/csv')\n  @ApiOperation({ operationId: 'getMentorsDetailsCsv' })\n  @ApiForbiddenResponse()\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor], true)\n  public async getMentorsDetailsCsv(@Param('courseId', ParseIntPipe) courseId: number, @Res() res: Response) {\n    const results = await this.courseMentorsService.getMentorsWithStats(courseId);\n    const parsedData = await parseAsync(results, { transforms: [transforms.flatten()] });\n\n    res.setHeader('Content-Type', 'text/csv');\n    res.setHeader('Content-disposition', `filename=mentors.csv`);\n\n    res.end(parsedData);\n  }\n\n  @Get('search/:searchText')\n  @ApiOperation({ operationId: 'searchMentors' })\n  @ApiOkResponse({ type: [SearchMentorDto] })\n  @ApiForbiddenResponse()\n  @CacheTTL(DEFAULT_CACHE_TTL)\n  @UseGuards(DefaultGuard, CourseGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  public async searchMentors(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('searchText') searchText: string,\n  ) {\n    const mentors = await this.courseMentorsService.searchMentors(courseId, `${searchText}%`);\n    return mentors.map(mentor => new SearchMentorDto(mentor));\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-mentors/course-mentors.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { DataSource, Repository } from 'typeorm';\nimport { CourseTask, Mentor, TaskResult } from '@entities/index';\nimport { MentorDetails } from '@common/models';\nimport { UsersService } from '../../users/users.service';\nimport { MentorsService } from '../mentors';\nimport { PersonDto } from '../../core/dto';\n\n@Injectable()\nexport class CourseMentorsService {\n  constructor(\n    @InjectRepository(Mentor)\n    readonly mentorsRepository: Repository<Mentor>,\n    @InjectRepository(CourseTask)\n    readonly courseTaskRepository: Repository<CourseTask>,\n    @InjectRepository(TaskResult)\n    readonly taskResultRepository: Repository<TaskResult>,\n    private dataSource: DataSource,\n  ) {}\n\n  private async getMentorCheckerCourseTaskIds(courseId: number): Promise<[number[], number]> {\n    const [courseTasks, count] = await this.courseTaskRepository\n      .createQueryBuilder('c')\n      .select('c.id')\n      .leftJoin('c.task', 't')\n      .where({ checker: 'mentor', courseId, disabled: false })\n      .andWhere('c.studentEndDate < NOW()')\n      .andWhere(\"c.type <> 'interview'\")\n      .getManyAndCount();\n\n    return [courseTasks.map(task => task.id), count];\n  }\n\n  public async getMentorsWithStats(courseId: number): Promise<MentorDetails[]> {\n    const [courseTasksIds, count] = await this.getMentorCheckerCourseTaskIds(courseId);\n\n    const [records, checkedCountByMentor, lastCheckedDateByMentor] = await Promise.all([\n      this.getMentorsRecordsWithDetails(courseId),\n      this.getCheckedTasksCount(courseId, courseTasksIds),\n      this.getLastCheckedDates(courseId, courseTasksIds),\n    ]);\n\n    return this.constructMentorsWithStats(records, checkedCountByMentor, lastCheckedDateByMentor, count);\n  }\n\n  public async searchMentors(\n    courseId: number,\n    searchText: string,\n  ): Promise<{ id: number; githubId: string; name: string }[]> {\n    const entities = await this.mentorsRepository\n      .createQueryBuilder('mentor')\n      .innerJoin('mentor.user', 'user')\n      .addSelect(['user.id', 'user.firstName', 'user.lastName', 'user.githubId'])\n      .where('\"mentor\".\"courseId\" = :courseId', { courseId })\n      .andWhere('\"mentor\".\"isExpelled\" = false')\n      .andWhere(\n        `\n        (\n          \"user\".\"githubId\" ILIKE :searchText OR\n          \"user\".\"firstName\" ILIKE :searchText OR\n          \"user\".\"lastName\" ILIKE :searchText OR\n          CONCAT(\"user\".\"firstName\", ' ', \"user\".\"lastName\") ILIKE :searchText\n        )\n      `,\n        { courseId, searchText },\n      )\n      .limit(20)\n      .getMany();\n\n    const result = entities.map(entity => ({\n      id: entity.id,\n      githubId: entity.user.githubId,\n      name: PersonDto.getName(entity.user),\n    }));\n\n    return result;\n  }\n\n  private async getMentorsRecordsWithDetails(courseId: number) {\n    return await this.mentorsRepository\n      .createQueryBuilder('mentor')\n      .innerJoin('mentor.user', 'user')\n      .addSelect(UsersService.getPrimaryUserFields())\n      .addSelect(['user.contactsEpamEmail', 'user.contactsEmail'])\n      .leftJoin('mentor.students', 's')\n      .leftJoin('mentor.stageInterviews', 'si')\n      .leftJoin('mentor.taskChecker', 'tc')\n      .leftJoin('mentor.interviewResults', 'ir')\n      .leftJoin('tc.courseTask', 't', 't.type = :type', { type: 'interview' })\n      .addSelect(['s.id', 's.isExpelled', 's.isFailed'])\n      .addSelect(['si.id', 'si.isCompleted'])\n      .addSelect(['tc.id', 'tc.courseTaskId'])\n      .addSelect(['t.id', 't.type'])\n      .addSelect(['ir.id'])\n      .where(`\"mentor\".\"courseId\" = :courseId`, { courseId })\n      .orderBy('mentor.createdDate')\n      .getMany();\n  }\n\n  private buildTaskResultSubQuery(courseTasksIds: number[]) {\n    // add 0 to preserve empty array\n    return this.taskResultRepository\n      .createQueryBuilder('t')\n      .select('t.lastCheckerId', 'lastCheckerId')\n      .where('t.courseTaskId IN (:...ids)', { ids: courseTasksIds.concat([0]) });\n  }\n\n  private async getLastCheckedDates(courseId: number, courseTasksIds: number[]): Promise<Record<string, Date | null>> {\n    const subQuery = this.buildTaskResultSubQuery(courseTasksIds)\n      .addSelect('MAX(t.\"updatedDate\")', 'updatedDate')\n      .groupBy('t.lastCheckerId');\n\n    const query = this.dataSource\n      .createQueryBuilder()\n      .select('m.id', 'id')\n      .addSelect('tr.\"updatedDate\"', 'value')\n      .from(Mentor, 'm')\n      .leftJoin('user', 'u', 'u.\"id\" = m.\"userId\"')\n      .leftJoin(`(${subQuery.getQuery()})`, 'tr', 'tr.\"lastCheckerId\" = u.id')\n      .where('m.\"courseId\" = :courseId', { courseId })\n      .setParameters(subQuery.getParameters());\n\n    const result: { id: number; value: Date | null }[] = await query.getRawMany();\n\n    return Object.fromEntries(result.map(({ id, value }) => [id, value]));\n  }\n\n  private async getCheckedTasksCount(courseId: number, courseTasksIds: number[]): Promise<Record<string, number>> {\n    const subQuery = this.buildTaskResultSubQuery(courseTasksIds).addSelect('t.\"updatedDate\"', 'updatedDate');\n\n    const query = this.dataSource\n      .createQueryBuilder()\n      .select('m.id', 'id')\n      .addSelect('COUNT(tr.\"lastCheckerId\")', 'value')\n      .from(Mentor, 'm')\n      .leftJoin('user', 'u', 'u.\"id\" = m.\"userId\"')\n      .leftJoin(`(${subQuery.getQuery()})`, 'tr', 'tr.\"lastCheckerId\" = u.id')\n      .where('m.\"courseId\" = :courseId', { courseId })\n      .groupBy('m.id')\n      .setParameters(subQuery.getParameters());\n\n    const result: { id: number; value: string }[] = await query.getRawMany();\n\n    return Object.fromEntries(result.map(({ id, value }) => [id, Number(value)]));\n  }\n\n  private constructMentorsWithStats(\n    mentorsRecords: Mentor[],\n    checkedTasksCountByMentor: Record<string, number>,\n    lastCheckedDateByMentor: Record<string, Date | null>,\n    tasksCount: number,\n  ) {\n    const mentors = [];\n    for (const mentor of mentorsRecords) {\n      const mentorDetails = MentorsService.convertMentorToMentorDetails(mentor);\n\n      const { user } = mentor;\n      const activeStudents = mentor.students?.filter(s => !s.isExpelled && !s.isFailed) ?? [];\n      const totalToCheck = activeStudents.length * tasksCount;\n\n      const lastCheckedDate = lastCheckedDateByMentor[mentor.id];\n      const checkedCount = checkedTasksCountByMentor[mentor.id] ?? 0;\n\n      mentors.push({\n        ...mentorDetails,\n        contactsEmail: user.contactsEmail ?? '',\n        contactsEpamEmail: user.contactsEpamEmail ?? '',\n        studentsCount: activeStudents.length,\n        screenings: {\n          total: mentor.stageInterviews?.length ?? 0,\n          completed: mentor.stageInterviews?.filter(s => s.isCompleted).length ?? 0,\n        },\n        interviews: {\n          total: mentor.taskChecker?.filter(tc => tc.courseTask?.type === 'interview').length,\n          completed: mentor.interviewResults?.length ?? 0,\n        },\n        taskResultsStats: {\n          lastUpdatedDate: lastCheckedDate ? new Date(lastCheckedDate) : null,\n          total: totalToCheck,\n          checked: checkedCount,\n        },\n      });\n    }\n    return mentors;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-mentors/dto/mentor-details.dto.ts",
    "content": "import { InterviewStatistics, MentorDetails } from '@common/models';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { UserDto } from '../../../users/dto';\nimport { PreferredStudentsLocation } from '@entities/mentorRegistry';\n\nclass StudentId {\n  @ApiProperty()\n  id: number;\n}\n\nexport class MentorDetailsDto extends UserDto {\n  constructor(mentor: MentorDetails) {\n    super(mentor);\n\n    this.isActive = mentor.isActive;\n    this.cityName = mentor.cityName;\n    this.countryName = mentor.countryName;\n    this.students = mentor.students;\n    this.interviews = mentor.interviews;\n    this.screenings = mentor.screenings;\n    this.taskResultsStats = mentor.taskResultsStats;\n    this.maxStudentsLimit = mentor.maxStudentsLimit;\n    this.studentsPreference = mentor.studentsPreference as PreferredStudentsLocation;\n    this.studentsCount = mentor.studentsCount;\n  }\n\n  @ApiProperty()\n  public isActive: boolean;\n\n  @ApiProperty()\n  public cityName: string;\n\n  @ApiProperty()\n  public countryName: string;\n\n  @ApiProperty()\n  public maxStudentsLimit: number;\n\n  @ApiProperty({ type: [StudentId] })\n  public students: { id: number }[];\n\n  @ApiPropertyOptional()\n  public interviews?: InterviewStatistics;\n\n  @ApiPropertyOptional()\n  public screenings?: InterviewStatistics;\n\n  @ApiPropertyOptional()\n  public taskResultsStats?: {\n    lastUpdatedDate?: Date | string | null;\n    total: number;\n    checked: number;\n  };\n\n  @ApiProperty({ enum: PreferredStudentsLocation })\n  public studentsPreference: PreferredStudentsLocation;\n\n  @ApiPropertyOptional()\n  public studentsCount?: number;\n\n  @ApiProperty()\n  public contactsEpamEmail: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-mentors/dto/search-mentor.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class SearchMentorDto {\n  constructor({ id, githubId, name }: { id: number; githubId: string; name: string }) {\n    this.id = id;\n    this.githubId = githubId;\n    this.name = name;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  githubId: string;\n\n  @ApiProperty()\n  name: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-mentors/index.ts",
    "content": "export * from './course-mentors.service';\nexport * from './course-mentors.controller';\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/course-icalendar.controller.ts",
    "content": "import { Controller, Get, Header, Param, ParseIntPipe, Query, Req, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { JwtService } from 'src/core/jwt/jwt.service';\nimport { CourseGuard, CurrentRequest, DefaultGuard } from '../../auth';\nimport { CoursesService } from '../courses.service';\nimport { CourseICalendarService } from './course-icalendar.service';\nimport { CourseScheduleService } from './course-schedule.service';\nimport { CourseScheduleTokenDto } from './dto';\n\n@Controller('courses/:courseId/icalendar')\n@ApiTags('courses schedule ical')\nexport class CourseICalendarController {\n  constructor(\n    private readonly courseScheduleService: CourseScheduleService,\n    private readonly coursesService: CoursesService,\n    private readonly courseICalendarService: CourseICalendarService,\n    private readonly jwtService: JwtService,\n  ) {}\n\n  @Get('/token')\n  @Header('Cache-Control', 'max-age=86400')\n  @ApiOkResponse({ type: CourseScheduleTokenDto })\n  @ApiOperation({ operationId: 'getScheduleICalendarToken' })\n  @UseGuards(DefaultGuard, CourseGuard)\n  public async getScheduleICalendarToken(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ): Promise<CourseScheduleTokenDto> {\n    return {\n      token: this.jwtService.createPublicCalendarToken({ courseId, githubId: req.user.githubId }),\n    };\n  }\n\n  @Get('/:token')\n  @Header('Content-Type', 'text/calendar')\n  @ApiOkResponse({ type: String })\n  @ApiOperation({ operationId: 'getScheduleICalendar' })\n  public async getScheduleICalendar(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('token') token: string,\n    @Query('timezone') timezone: string,\n  ): Promise<string> {\n    const payload = this.jwtService.validateToken<{ githubId: string; courseId: number }>(token);\n    await this.courseICalendarService.validateUserCourse(courseId, payload);\n    const [data, course] = await Promise.all([\n      this.courseScheduleService.getAll(courseId),\n      this.coursesService.getById(courseId),\n    ]);\n    const result = await this.courseICalendarService.getICalendar(data, course.name, timezone || 'Europe/Minsk');\n    return result;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/course-icalendar.service.ts",
    "content": "import { User } from '@entities/user';\nimport { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport ical from 'ical-generator';\nimport { formatInTimeZone } from 'date-fns-tz';\nimport { CourseScheduleItem, CourseScheduleItemTag } from './course-schedule.service';\n\n@Injectable()\nexport class CourseICalendarService {\n  constructor(\n    @InjectRepository(User)\n    readonly courseRepository: Repository<User>,\n  ) {}\n\n  public async validateUserCourse(courseId: number, payload: { githubId: string; courseId: number }): Promise<boolean> {\n    if (Number(payload.courseId) !== courseId) {\n      throw new BadRequestException('Invalid Course Id');\n    }\n    await this.courseRepository.findOneByOrFail({ githubId: payload.githubId });\n    return true;\n  }\n\n  public async getICalendar(data: CourseScheduleItem[], courseName: string, timezone: string): Promise<string> {\n    const icalData = ical({\n      name: courseName,\n      timezone: timezone,\n    });\n\n    for (const item of data) {\n      // CrossCheck has two events: submit and review and they have the same task id. iCal requires unique id for each event.\n      const id = item.tag === CourseScheduleItemTag.CrossCheckReview ? `${item.id}-1` : item.id;\n      const endDate = item.endDate || new Date(item.startDate.getTime() + 1000 * 60 * 60);\n      icalData.createEvent({\n        start: formatInTimeZone(item.startDate, timezone, \"yyyy-MM-dd'T'HH:mm\"),\n        end: formatInTimeZone(endDate, timezone, \"yyyy-MM-dd'T'HH:mm\"),\n        summary: item.name,\n        description: this.buildDescription(item),\n        id,\n        alarms: [],\n        organizer: item.organizer ? { name: item.organizer?.name ?? '', email: 'user@example.com' } : undefined,\n        url: item.descriptionUrl ?? undefined,\n      });\n    }\n    return icalData.toString();\n  }\n\n  private buildDescription(item: CourseScheduleItem): string {\n    const result = [];\n\n    if (item.organizer) {\n      result.push(`🎙 Organizer: ${item.organizer.name} (@${item.organizer.githubId})`);\n    }\n    if (item.maxScore) {\n      result.push(`🏅 Max Score: ${item.maxScore}`);\n    }\n    if (item.scoreWeight) {\n      result.push(`🔼 Score Weight: ${item.scoreWeight}`);\n    }\n    return result.join('\\n\\n');\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/course-schedule.controller.ts",
    "content": "import { Body, Controller, Get, Param, ParseIntPipe, Post, Req, UseGuards } from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { CourseScheduleService } from './course-schedule.service';\nimport { CourseScheduleItemDto } from './dto';\nimport { CourseCopyFromDto } from './dto/course-copy-from.dto';\n\n@Controller('courses/:courseId/schedule')\n@ApiTags('courses schedule')\nexport class CourseScheduleController {\n  constructor(private courseScheduleService: CourseScheduleService) {}\n\n  @Get()\n  @ApiOkResponse({ type: [CourseScheduleItemDto] })\n  @ApiOperation({ operationId: 'getSchedule' })\n  @UseGuards(DefaultGuard, CourseGuard)\n  public async getAll(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ): Promise<CourseScheduleItemDto[]> {\n    const studentId = req.user.courses[courseId]?.studentId ?? undefined;\n    const data = await this.courseScheduleService.getAll(courseId, studentId);\n    return data.map(item => new CourseScheduleItemDto(item));\n  }\n\n  @Post('/copy')\n  @ApiOkResponse({})\n  @ApiOperation({ operationId: 'copySchedule' })\n  @ApiBody({ type: CourseCopyFromDto, required: true })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Manager, Role.Admin])\n  public async copyFrom(\n    @Param('courseId', ParseIntPipe) copyToCourseId: number,\n    @Body() body: CourseCopyFromDto,\n  ): Promise<void> {\n    await this.courseScheduleService.copyFromTo(body.copyFromCourseId, copyToCourseId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/course-schedule.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n  CourseScheduleDataSource,\n  CourseScheduleItemStatus,\n  CourseScheduleItemTag,\n  CourseScheduleService,\n} from './course-schedule.service';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { Course } from '@entities/course';\nimport {\n  CourseEvent,\n  CourseTask,\n  StageInterview,\n  TaskChecker,\n  TaskInterviewResult,\n  TaskResult,\n  TaskSolution,\n  TeamDistribution,\n  TeamDistributionStudent,\n} from '@entities/index';\n\nconst MOCK_CURRENT_TIME = new Date('2022-03-22T00:00:00.000Z');\n\nenum MockDate {\n  DateBefore = '2022-03-21T00:00:00.000Z',\n  DateAfter = '2022-03-23T00:00:00.000Z',\n}\n\nconst mockCrossCheckCourseTask = {\n  id: 111,\n  createdDate: '2022-10-29T15:28:29.054Z',\n  updatedDate: '2022-11-01T14:33:47.015Z',\n  taskId: 222,\n  courseId: 333,\n  studentStartDate: '2022-10-27T17:27:00.000Z',\n  studentEndDate: '2022-10-31T17:28:00.000Z',\n  crossCheckEndDate: '2022-11-05T23:59:00.000Z',\n  mentorStartDate: null,\n  mentorEndDate: null,\n  maxScore: 100,\n  scoreWeight: 1,\n  checker: 'crossCheck',\n  taskOwnerId: 999,\n  pairsCount: 3,\n  type: 'jstask',\n  disabled: false,\n  crossCheckStatus: 'initial',\n  submitText: 'Some text',\n  validations: { githubIdInUrl: false },\n  task: {\n    id: 222,\n    createdDate: ' 2020-02-21T10:24:38.588Z',\n    updatedDate: '2020-02-21T10:24:38.588Z',\n    name: 'Singolo',\n    descriptionUrl: 'https://github.com/rolling-scopes-school/tasks/tree/master/tasks/markups/level-2/singolo',\n    description: null,\n    githubPrRequired: false,\n    verification: 'manual',\n    githubRepoName: null,\n    sourceGithubRepoUrl: null,\n    type: 'htmltask',\n    useJury: false,\n    allowStudentArtefacts: false,\n    tags: ['stage1', 'html'],\n    skills: [],\n    disciplineId: null,\n    attributes: {},\n  },\n  taskOwner: {\n    id: 98765,\n    firstName: 'John',\n    lastName: 'Doe',\n    githubId: 'john-doe',\n  },\n} as unknown as CourseTask;\n\ndescribe('CourseScheduleService', () => {\n  let service: CourseScheduleService;\n\n  beforeAll(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CourseScheduleService,\n        {\n          provide: getRepositoryToken(Course),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(CourseTask),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(CourseEvent),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(TaskResult),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(TaskInterviewResult),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(StageInterview),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(TaskSolution),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(TaskChecker),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(TeamDistributionStudent),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(TeamDistribution),\n          useValue: {},\n        },\n      ],\n    }).compile();\n\n    service = module.get<CourseScheduleService>(CourseScheduleService);\n  });\n  it('should be defined', () => {\n    expect(service).toBeDefined();\n  });\n\n  it('should have getCrossCheckItemStatus method', () => {\n    expect(service).toHaveProperty('getCrossCheckItemStatus');\n  });\n\n  it('should have transformCrossCheckTask method', () => {\n    expect(service).toHaveProperty('transformCrossCheckTask');\n  });\n\n  describe('transformCrossCheckTask', () => {\n    afterAll(() => {\n      vi.restoreAllMocks();\n    });\n    it('should create 2 items from task', () => {\n      const mockStatus = CourseScheduleItemStatus.Available;\n      vi.spyOn(service, 'getCrossCheckItemStatus').mockReturnValue(mockStatus);\n      const [submitItem, reviewItem] = service.transformCrossCheckTask(mockCrossCheckCourseTask, true, null, 12345);\n\n      expect(submitItem).toEqual({\n        id: mockCrossCheckCourseTask.id,\n        name: mockCrossCheckCourseTask.task.name,\n        courseId: mockCrossCheckCourseTask.courseId,\n        startDate: mockCrossCheckCourseTask.studentStartDate,\n        endDate: mockCrossCheckCourseTask.studentEndDate,\n        status: mockStatus,\n        tag: CourseScheduleItemTag.CrossCheckSubmit,\n        descriptionUrl: mockCrossCheckCourseTask.task.descriptionUrl,\n        organizer: {\n          githubId: mockCrossCheckCourseTask.taskOwner?.githubId,\n          id: mockCrossCheckCourseTask.taskOwner?.id,\n          name: `${mockCrossCheckCourseTask.taskOwner?.firstName} ${mockCrossCheckCourseTask.taskOwner?.lastName}`,\n        },\n        scoreWeight: mockCrossCheckCourseTask.scoreWeight,\n        score: null,\n        maxScore: mockCrossCheckCourseTask.maxScore,\n        type: CourseScheduleDataSource.CourseTask,\n      });\n\n      expect(reviewItem).toEqual({\n        id: mockCrossCheckCourseTask.id,\n        name: mockCrossCheckCourseTask.task.name,\n        courseId: mockCrossCheckCourseTask.courseId,\n        startDate: mockCrossCheckCourseTask.studentEndDate,\n        endDate: mockCrossCheckCourseTask.crossCheckEndDate,\n        status: mockStatus,\n        tag: CourseScheduleItemTag.CrossCheckReview,\n        descriptionUrl: mockCrossCheckCourseTask.task.descriptionUrl,\n        organizer: {\n          githubId: mockCrossCheckCourseTask.taskOwner?.githubId,\n          id: mockCrossCheckCourseTask.taskOwner?.id,\n          name: `${mockCrossCheckCourseTask.taskOwner?.firstName} ${mockCrossCheckCourseTask.taskOwner?.lastName}`,\n        },\n        scoreWeight: mockCrossCheckCourseTask.scoreWeight,\n        score: null,\n        maxScore: mockCrossCheckCourseTask.maxScore,\n      });\n    });\n  });\n\n  describe('getCrossCheckItemStatus', () => {\n    beforeAll(() => {\n      vi.useFakeTimers().setSystemTime(MOCK_CURRENT_TIME);\n    });\n\n    afterAll(() => {\n      vi.useRealTimers();\n    });\n\n    const testCases = [\n      {\n        description: 'should define item as Archived if no startTime is provided',\n        input: [\n          {\n            startTime: null,\n            endTime: MockDate.DateAfter,\n          },\n          CourseScheduleItemTag.CrossCheckSubmit,\n          undefined,\n        ],\n        expectedStatus: CourseScheduleItemStatus.Archived,\n      },\n      {\n        description: 'should define item as Archived if no endTime is provided',\n        input: [\n          {\n            startTime: MockDate.DateAfter,\n            endTime: null,\n          },\n          CourseScheduleItemTag.CrossCheckSubmit,\n          undefined,\n        ],\n        expectedStatus: CourseScheduleItemStatus.Archived,\n      },\n      {\n        description: 'should define item as Future',\n        input: [\n          {\n            startTime: MockDate.DateAfter,\n            endTime: MockDate.DateAfter,\n          },\n          CourseScheduleItemTag.CrossCheckSubmit,\n          undefined,\n        ],\n        expectedStatus: CourseScheduleItemStatus.Future,\n      },\n      {\n        description: 'should define review item as Done if score is present',\n        input: [\n          {\n            startTime: MockDate.DateBefore,\n            endTime: MockDate.DateBefore,\n          },\n          CourseScheduleItemTag.CrossCheckReview,\n          { currentScore: 100 },\n        ],\n        expectedStatus: CourseScheduleItemStatus.Done,\n      },\n      {\n        description: 'should define submit item as Done if score is present',\n        input: [\n          {\n            startTime: MockDate.DateBefore,\n            endTime: MockDate.DateBefore,\n          },\n          CourseScheduleItemTag.CrossCheckSubmit,\n          { currentScore: 100 },\n        ],\n        expectedStatus: CourseScheduleItemStatus.Done,\n      },\n      {\n        description: 'should define submit item as Done if score is not present but task was submitted',\n        input: [\n          {\n            startTime: MockDate.DateBefore,\n            endTime: MockDate.DateBefore,\n          },\n          CourseScheduleItemTag.CrossCheckSubmit,\n          { currentScore: null, submitted: true },\n        ],\n        expectedStatus: CourseScheduleItemStatus.Done,\n      },\n      {\n        description: 'should define item as Available if task is in progress',\n        input: [\n          {\n            startTime: MockDate.DateBefore,\n            endTime: MockDate.DateAfter,\n          },\n          CourseScheduleItemTag.CrossCheckSubmit,\n          { currentScore: null, submitted: true },\n        ],\n        expectedStatus: CourseScheduleItemStatus.Available,\n      },\n      {\n        description: 'should define review item as Missed if there is no score after submit',\n        input: [\n          {\n            startTime: MockDate.DateBefore,\n            endTime: MockDate.DateBefore,\n          },\n          CourseScheduleItemTag.CrossCheckReview,\n          { currentScore: null, submitted: true },\n        ],\n        expectedStatus: CourseScheduleItemStatus.Missed,\n      },\n      {\n        description: 'should define submit item as Missed if there is no score after submit and there was no submit',\n        input: [\n          {\n            startTime: MockDate.DateBefore,\n            endTime: MockDate.DateBefore,\n          },\n          CourseScheduleItemTag.CrossCheckSubmit,\n          { currentScore: null, submitted: false },\n        ],\n        expectedStatus: CourseScheduleItemStatus.Missed,\n      },\n    ] as {\n      description: string;\n      input: [\n        { startTime: string | Date | null; endTime: string | Date | null },\n        CourseScheduleItemTag,\n        { currentScore: number | null; submitted: boolean } | undefined,\n      ];\n      expectedStatus: CourseScheduleItemStatus;\n    }[];\n\n    test.each(testCases)('$description', ({ input: [range, tag, studentData], expectedStatus }) => {\n      expect(service.getCrossCheckItemStatus(range, tag, studentData)).toBe(expectedStatus);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/course-schedule.service.ts",
    "content": "import { TaskChecker } from '@entities/taskChecker';\nimport { CourseEvent } from '@entities/courseEvent';\nimport { Checker, CourseTask } from '@entities/courseTask';\nimport { StageInterview } from '@entities/stageInterview';\nimport { TaskInterviewResult } from '@entities/taskInterviewResult';\nimport { TaskResult } from '@entities/taskResult';\nimport { TaskSolution } from '@entities/taskSolution';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { PersonDto } from '../../core/dto';\nimport { Repository } from 'typeorm';\nimport { EventType } from '../course-events/dto/course-event.dto';\nimport { Course } from '@entities/course';\nimport { differenceInMinutes } from 'date-fns';\nimport { TeamDistribution } from '@entities/teamDistribution';\nimport { TeamDistributionStudent } from '@entities/teamDistributionStudent';\n\nexport type CourseScheduleItem = Pick<CourseTask, 'id' | 'courseId'> &\n  Partial<Pick<CourseTask, 'maxScore' | 'scoreWeight'>> & {\n    startDate: Date;\n    endDate: Date;\n    name: string;\n    organizer?: PersonDto;\n    score?: number;\n    status: CourseScheduleItemStatus;\n    tag: CourseScheduleItemTag;\n    descriptionUrl?: string;\n    type: CourseScheduleDataSource;\n  };\n\nexport enum CourseScheduleDataSource {\n  CourseTask = 'courseTask',\n  CourseEvent = 'courseEvent',\n  CourseTeamDistribution = 'courseTeamDistribution',\n}\n\nexport enum CourseScheduleItemTag {\n  Lecture = 'lecture',\n  Coding = 'coding',\n  SelfStudy = 'self-study',\n  Interview = 'interview',\n  CrossCheckSubmit = 'cross-check-submit',\n  CrossCheckReview = 'cross-check-review',\n  Test = 'test',\n  TeamDistribution = 'team-distribution',\n}\n\nexport enum CourseScheduleItemStatus {\n  Done = 'done',\n  Available = 'available',\n  Archived = 'archived',\n  Future = 'future',\n  Missed = 'missed',\n  Review = 'review',\n  Registered = 'registered',\n  Unavailable = 'unavailable',\n}\n\n@Injectable()\nexport class CourseScheduleService {\n  constructor(\n    @InjectRepository(Course)\n    readonly courseRepository: Repository<Course>,\n    @InjectRepository(CourseTask)\n    readonly courseTaskRepository: Repository<CourseTask>,\n    @InjectRepository(CourseEvent)\n    readonly courseEventRepository: Repository<CourseEvent>,\n    @InjectRepository(TeamDistribution)\n    readonly teamDistribution: Repository<TeamDistribution>,\n    @InjectRepository(TaskResult)\n    readonly taskResultRepository: Repository<TaskResult>,\n    @InjectRepository(TaskInterviewResult)\n    readonly taskInterviewResultRepository: Repository<TaskInterviewResult>,\n    @InjectRepository(StageInterview)\n    readonly stageInterviewRepository: Repository<StageInterview>,\n    @InjectRepository(TaskSolution)\n    readonly taskSolutionRepository: Repository<TaskSolution>,\n    @InjectRepository(TaskChecker)\n    readonly taskCheckerRepository: Repository<TaskChecker>,\n    @InjectRepository(TeamDistribution)\n    readonly courseTeamDistributionRepository: Repository<TeamDistribution>,\n    @InjectRepository(TeamDistributionStudent)\n    private teamDistributionStudentRepository: Repository<TeamDistributionStudent>,\n  ) {}\n\n  private scheduleSort(a: CourseScheduleItem, b: CourseScheduleItem) {\n    const tagPriority: { [key in CourseScheduleItemTag]?: number } = {\n      'self-study': 1,\n      test: 2,\n      coding: 3,\n    };\n\n    const timeDifference = differenceInMinutes(a.startDate, b.startDate);\n    if (timeDifference !== 0) {\n      return timeDifference;\n    }\n\n    const aTagPriority = tagPriority[a.tag] || Infinity;\n    const bTagPriority = tagPriority[b.tag] || Infinity;\n\n    return aTagPriority - bTagPriority;\n  }\n\n  private async getCourseTeamDistributions(courseId: number, studentId?: number) {\n    return this.courseTeamDistributionRepository.find({\n      where: { courseId },\n      cache: studentId ? 90 * 1000 : undefined,\n    });\n  }\n\n  private async getTeamDistributionStudents(courseId: number, studentId?: number) {\n    if (!studentId) {\n      return [];\n    }\n\n    const teamDistributionStudents = await this.teamDistributionStudentRepository.find({\n      where: { courseId, studentId },\n      relations: { student: true },\n    });\n    return teamDistributionStudents;\n  }\n\n  private getTeamDistributionStatus(\n    teamDistribution: TeamDistribution,\n    teamDistributionStudents: TeamDistributionStudent[],\n  ) {\n    const currTimestampUTC = new Date();\n    const distributionStartDate = new Date(teamDistribution.startDate);\n    const teamDistributionStudent = teamDistributionStudents.find(el => el.teamDistributionId === teamDistribution.id);\n\n    if (currTimestampUTC < distributionStartDate) {\n      return CourseScheduleItemStatus.Future;\n    }\n\n    if (teamDistributionStudent?.distributed) {\n      return CourseScheduleItemStatus.Done;\n    }\n\n    if (teamDistributionStudent?.active) {\n      return CourseScheduleItemStatus.Registered;\n    }\n\n    if (\n      teamDistributionStudent?.student == null ||\n      teamDistributionStudent.student.isExpelled ||\n      teamDistribution.minTotalScore > teamDistributionStudent.student.totalScore\n    ) {\n      return CourseScheduleItemStatus.Unavailable;\n    }\n\n    const distributionEndDate = new Date(teamDistribution.endDate);\n    if (currTimestampUTC <= distributionEndDate && currTimestampUTC >= distributionStartDate) {\n      return CourseScheduleItemStatus.Available;\n    }\n\n    if (currTimestampUTC > distributionEndDate) {\n      return CourseScheduleItemStatus.Missed;\n    }\n\n    return CourseScheduleItemStatus.Unavailable;\n  }\n\n  public async getAll(courseId: number, studentId?: number): Promise<CourseScheduleItem[]> {\n    const [courseTasks, courseEvents, teamDistribution] = await Promise.all([\n      this.getActiveCourseTasks(courseId, studentId),\n      this.getCourseEvents(courseId, studentId),\n      this.getCourseTeamDistributions(courseId, studentId),\n    ]);\n\n    const [\n      taskResults,\n      interviewResults,\n      technicalScreeningResults,\n      taskSolutions,\n      taskCheckers,\n      teamDistributionStudents,\n    ] = await Promise.all([\n      this.getTaskResults(studentId),\n      this.getInterviewResults(studentId),\n      this.getPrescreeningResults(studentId),\n      this.getTaskSolutions(studentId),\n      this.getTaskCheckers(studentId),\n      this.getTeamDistributionStudents(courseId, studentId),\n    ]);\n\n    const schedule = courseTasks\n      .reduce<CourseScheduleItem[]>((acc, courseTask) => {\n        const { id, courseId, studentStartDate, studentEndDate, maxScore, scoreWeight, crossCheckEndDate } = courseTask;\n        const { name } = courseTask.task;\n\n        const isCrossCheckTask = courseTask.checker === Checker.CrossCheck;\n\n        const currentScore = this.getCurrentTaskScore(id, taskResults, interviewResults, technicalScreeningResults);\n        const submitted = this.getCourseTaskSubmitted(courseTask.id, taskSolutions, taskCheckers);\n        const status = this.getCourseTaskStatus(\n          { startTime: courseTask.studentStartDate, endTime: courseTask.studentEndDate },\n          { maxScore: courseTask.maxScore, checker: courseTask.checker },\n          studentId ? { currentScore, submitted } : undefined,\n        );\n        const tag = this.getCourseTaskTag(courseTask);\n\n        if (isCrossCheckTask) {\n          const scheduleItems = this.transformCrossCheckTask(courseTask, submitted, currentScore, studentId);\n          acc.push(...scheduleItems);\n        } else {\n          const scheduleItem = {\n            id,\n            name,\n            courseId,\n            startDate: studentStartDate,\n            endDate: studentEndDate,\n            crossCheckEndDate,\n            maxScore,\n            scoreWeight,\n            score: currentScore,\n            status,\n            tag,\n            descriptionUrl: courseTask.task.descriptionUrl,\n            organizer: courseTask.taskOwner ? new PersonDto(courseTask.taskOwner) : null,\n            type: CourseScheduleDataSource.CourseTask,\n          } as CourseScheduleItem;\n\n          acc.push(scheduleItem);\n        }\n\n        return acc;\n      }, [])\n      .concat(\n        courseEvents.map(courseEvent => {\n          const { courseId, dateTime, endTime, id } = courseEvent;\n          const { name } = courseEvent.event;\n          const tag = this.getCourseEventTag(courseEvent);\n          return {\n            id,\n            name,\n            courseId,\n            startDate: dateTime,\n            endDate: endTime,\n            status: this.getEventStatus(courseEvent),\n            tag,\n            descriptionUrl: courseEvent.event.descriptionUrl,\n            organizer: new PersonDto(courseEvent.organizer),\n            type: CourseScheduleDataSource.CourseEvent,\n          } as CourseScheduleItem;\n        }),\n      )\n      .concat(\n        teamDistribution.map(teamDistribution => {\n          return {\n            id: teamDistribution.id,\n            name: teamDistribution.name,\n            courseId,\n            startDate: teamDistribution.startDate,\n            endDate: teamDistribution.endDate,\n            status: this.getTeamDistributionStatus(teamDistribution, teamDistributionStudents),\n            type: CourseScheduleDataSource.CourseTeamDistribution,\n            tag: CourseScheduleItemTag.TeamDistribution,\n            descriptionUrl: teamDistribution.descriptionUrl,\n          };\n        }),\n      )\n      .sort(this.scheduleSort);\n\n    return schedule;\n  }\n\n  public getCourseTaskSubmitted(courseTaskId: number, taskSolutions: TaskSolution[], taskCheckers: TaskChecker[]) {\n    return (\n      taskSolutions.some(solution => solution.courseTaskId === courseTaskId) ||\n      taskCheckers.some(checker => checker.courseTaskId === courseTaskId)\n    );\n  }\n\n  public transformCrossCheckTask(\n    crossCheckTask: CourseTask,\n    submitted: boolean,\n    currentScore: number | null,\n    studentId?: number,\n  ): CourseScheduleItem[] {\n    const submitRange = { startTime: crossCheckTask.studentStartDate, endTime: crossCheckTask.studentEndDate };\n    const reviewRange = { startTime: crossCheckTask.studentEndDate, endTime: crossCheckTask.crossCheckEndDate };\n\n    const submitItem = {\n      id: crossCheckTask.id,\n      name: crossCheckTask.task.name,\n      courseId: crossCheckTask.courseId,\n      startDate: crossCheckTask.studentStartDate,\n      endDate: crossCheckTask.studentEndDate,\n      status: this.getCrossCheckItemStatus(\n        submitRange,\n        CourseScheduleItemTag.CrossCheckSubmit,\n        studentId ? { currentScore, submitted } : undefined,\n      ),\n      tag: CourseScheduleItemTag.CrossCheckSubmit,\n      descriptionUrl: crossCheckTask.task.descriptionUrl,\n      organizer: crossCheckTask.taskOwner ? new PersonDto(crossCheckTask.taskOwner) : null,\n      scoreWeight: crossCheckTask.scoreWeight,\n      score: currentScore,\n      maxScore: crossCheckTask.maxScore,\n      type: CourseScheduleDataSource.CourseTask,\n    };\n\n    const reviewItem = {\n      id: crossCheckTask.id,\n      name: crossCheckTask.task.name,\n      courseId: crossCheckTask.courseId,\n      startDate: crossCheckTask.studentEndDate,\n      endDate: crossCheckTask.crossCheckEndDate,\n      status: this.getCrossCheckItemStatus(\n        reviewRange,\n        CourseScheduleItemTag.CrossCheckReview,\n        studentId ? { currentScore, submitted } : undefined,\n      ),\n      tag: CourseScheduleItemTag.CrossCheckReview,\n      descriptionUrl: crossCheckTask.task.descriptionUrl,\n      organizer: crossCheckTask.taskOwner ? new PersonDto(crossCheckTask.taskOwner) : null,\n      scoreWeight: crossCheckTask.scoreWeight,\n      score: currentScore,\n      maxScore: crossCheckTask.maxScore,\n    };\n\n    return [submitItem, reviewItem] as CourseScheduleItem[];\n  }\n\n  public getCrossCheckItemStatus(\n    range: { startTime: string | Date | null; endTime: string | Date | null },\n    tag: CourseScheduleItemTag,\n    studentData?: { currentScore: number | null; submitted: boolean },\n  ) {\n    if (!range.startTime || !range.endTime) {\n      return CourseScheduleItemStatus.Archived;\n    }\n\n    const startTime = new Date(range.startTime).getTime();\n    const endTime = new Date(range.endTime).getTime();\n    const { currentScore = null, submitted = false } = studentData ?? {};\n    const now = Date.now();\n\n    if (startTime > now) {\n      return CourseScheduleItemStatus.Future;\n    }\n\n    if (currentScore != null || (endTime < now && tag === CourseScheduleItemTag.CrossCheckSubmit && submitted)) {\n      return CourseScheduleItemStatus.Done;\n    }\n\n    if (startTime <= now && endTime >= now) {\n      return CourseScheduleItemStatus.Available;\n    }\n\n    return studentData ? CourseScheduleItemStatus.Missed : CourseScheduleItemStatus.Archived;\n  }\n\n  public async copyFromTo(fromCourseId: number, toCourseId: number) {\n    const [fromCourse, toCourse] = await Promise.all([\n      this.courseRepository.findOneByOrFail({ id: fromCourseId }),\n      this.courseRepository.findOneByOrFail({ id: toCourseId }),\n    ]);\n\n    const [courseTasks, courseEvents, courseTeamDistributions] = await Promise.all([\n      this.courseTaskRepository.find({ where: { courseId: fromCourseId } }),\n      this.courseEventRepository.find({ where: { courseId: fromCourseId } }),\n      this.teamDistribution.find({ where: { courseId: fromCourseId } }),\n    ]);\n\n    const timeDiff = toCourse.startDate.getTime() - fromCourse.startDate.getTime();\n\n    for (const courseTask of courseTasks) {\n      const { id, createdDate, updatedDate, crossCheckStatus, ...newCourseTask } = courseTask;\n      newCourseTask.courseId = toCourseId;\n      newCourseTask.crossCheckEndDate = this.adjustDate(newCourseTask.crossCheckEndDate, timeDiff);\n      newCourseTask.studentStartDate = this.adjustDate(newCourseTask.studentStartDate, timeDiff);\n      newCourseTask.studentEndDate = this.adjustDate(newCourseTask.studentEndDate, timeDiff);\n      newCourseTask.mentorStartDate = this.adjustDate(newCourseTask.mentorStartDate, timeDiff);\n      newCourseTask.mentorEndDate = this.adjustDate(newCourseTask.mentorEndDate, timeDiff);\n      newCourseTask.studentRegistrationStartDate = this.adjustDate(\n        newCourseTask.studentRegistrationStartDate,\n        timeDiff,\n      );\n      await this.courseTaskRepository.save(newCourseTask);\n    }\n\n    for (const courseEvent of courseEvents) {\n      const { id, createdDate, updatedDate, ...newCourseEvent } = courseEvent;\n      newCourseEvent.courseId = toCourseId;\n      newCourseEvent.dateTime = this.adjustDate(newCourseEvent.dateTime, timeDiff);\n      newCourseEvent.endTime = this.adjustDate(newCourseEvent.endTime, timeDiff);\n      newCourseEvent.date = null;\n      newCourseEvent.time = null;\n      await this.courseEventRepository.save(newCourseEvent);\n    }\n\n    for (const teamDistribution of courseTeamDistributions) {\n      const { id, createdDate, updatedDate, ...newTeamDistribution } = teamDistribution;\n      newTeamDistribution.courseId = toCourseId;\n      newTeamDistribution.startDate =\n        this.adjustDate(newTeamDistribution.startDate, timeDiff) ?? newTeamDistribution.startDate;\n      newTeamDistribution.endDate =\n        this.adjustDate(newTeamDistribution.endDate, timeDiff) ?? newTeamDistribution.endDate;\n      await this.teamDistribution.save(newTeamDistribution);\n    }\n  }\n\n  private adjustDate(date: string | Date | null, timeDiff: number): Date | null {\n    const fixedDate = typeof date === 'string' ? new Date(date) : date;\n    return fixedDate ? new Date((fixedDate as Date).getTime() + timeDiff) : fixedDate;\n  }\n\n  private getCurrentTaskScore(\n    courseTaskId: number,\n    taskResults: TaskResult[],\n    interviewResults: TaskInterviewResult[],\n    technicalScreeningResults: StageInterview[],\n  ) {\n    const scoreRaw =\n      taskResults.find(task => task.courseTaskId === courseTaskId)?.score ??\n      interviewResults.find(task => task.courseTaskId === courseTaskId)?.score ??\n      Math.max(\n        ...(technicalScreeningResults\n          .find(task => task.courseTaskId === courseTaskId)\n          ?.stageInterviewFeedbacks.map(feedback => JSON.parse(feedback.json))\n          .map((json: any) => {\n            const resumeScore = json?.resume?.score;\n            const decisionScore = json?.steps?.decision?.values?.finalScore;\n            return resumeScore ?? decisionScore ?? 0;\n          }) ?? []),\n      );\n    const currentScore = isFinite(scoreRaw) ? scoreRaw : null;\n    return currentScore;\n  }\n\n  private async getTaskSolutions(studentId: number | undefined): Promise<TaskSolution[]> {\n    if (!studentId) {\n      return [];\n    }\n    return this.taskSolutionRepository.find({\n      where: { studentId },\n      select: ['id', 'url', 'courseTaskId', 'studentId'],\n    });\n  }\n\n  private async getPrescreeningResults(studentId: number | undefined): Promise<StageInterview[]> {\n    if (!studentId) {\n      return [];\n    }\n    return this.stageInterviewRepository.find({\n      where: { studentId, isCompleted: true },\n      relations: ['stageInterviewFeedbacks'],\n    });\n  }\n\n  private async getInterviewResults(studentId: number | undefined): Promise<TaskInterviewResult[]> {\n    if (!studentId) {\n      return [];\n    }\n    return this.taskInterviewResultRepository.find({\n      where: { studentId },\n      select: ['id', 'score', 'studentId', 'courseTaskId'],\n    });\n  }\n\n  private async getTaskResults(studentId: number | undefined): Promise<TaskResult[]> {\n    if (!studentId) {\n      return [];\n    }\n    return this.taskResultRepository.find({\n      where: { studentId },\n      select: ['id', 'score', 'studentId', 'courseTaskId'],\n    });\n  }\n\n  private async getTaskCheckers(studentId: number | undefined): Promise<TaskChecker[]> {\n    if (!studentId) {\n      return [];\n    }\n    return this.taskCheckerRepository.find({\n      where: { studentId },\n      select: ['id', 'studentId', 'courseTaskId'],\n    });\n  }\n\n  private async getActiveCourseTasks(courseId: number, studentId?: number): Promise<CourseTask[]> {\n    return this.courseTaskRepository.find({\n      where: { courseId, disabled: false },\n      relations: ['task', 'taskOwner'],\n      cache: studentId ? 90 * 1000 : undefined,\n    });\n  }\n\n  private async getCourseEvents(courseId: number, studentId?: number): Promise<CourseEvent[]> {\n    return this.courseEventRepository.find({\n      where: { courseId },\n      relations: ['event', 'organizer'],\n      cache: studentId ? 90 * 1000 : undefined,\n    });\n  }\n\n  private getEventStatus(courseEvent: CourseEvent) {\n    const startTime = (courseEvent.dateTime as Date).getTime();\n    const endTime = Number(courseEvent.endTime) || startTime + (courseEvent.duration ?? 60) * 1000 * 60;\n    if (endTime && endTime < Date.now()) {\n      return CourseScheduleItemStatus.Archived;\n    }\n    if (startTime < Date.now()) {\n      return CourseScheduleItemStatus.Available;\n    }\n    return CourseScheduleItemStatus.Future;\n  }\n\n  private getCourseTaskStatus(\n    range: { startTime: string | Date | null; endTime: string | Date | null },\n    task: Pick<CourseTask, 'maxScore' | 'checker'>,\n    studentData?: { currentScore: number | null; submitted: boolean },\n  ) {\n    if (!range.startTime || !range.endTime) {\n      return CourseScheduleItemStatus.Archived;\n    }\n    const startTime = new Date(range.startTime).getTime();\n    const endTime = new Date(range.endTime).getTime();\n    const { currentScore = null, submitted = false } = studentData ?? {};\n    const now = Date.now();\n    const isAvailablePeriod = startTime <= now && endTime >= now;\n    const isAvailableAutoTest = isAvailablePeriod && task.checker === Checker.AutoTest;\n    if (startTime > now) {\n      return CourseScheduleItemStatus.Future;\n    }\n    if (isAvailableAutoTest && Number(currentScore) < task.maxScore) {\n      return CourseScheduleItemStatus.Available;\n    }\n    if (currentScore != null) {\n      return CourseScheduleItemStatus.Done;\n    }\n    if (submitted) {\n      return CourseScheduleItemStatus.Review;\n    }\n    if (isAvailablePeriod) {\n      return CourseScheduleItemStatus.Available;\n    }\n    return studentData ? CourseScheduleItemStatus.Missed : CourseScheduleItemStatus.Archived;\n  }\n\n  private getCourseTaskTag(courseTask: CourseTask): CourseScheduleItemTag {\n    const taskType = courseTask.type || courseTask.task.type;\n\n    if (taskType === 'selfeducation' || taskType === 'test') {\n      return CourseScheduleItemTag.Test;\n    }\n    if (taskType === 'interview' || taskType === 'stage-interview') {\n      return CourseScheduleItemTag.Interview;\n    }\n    return CourseScheduleItemTag.Coding;\n  }\n\n  private getCourseEventTag(courseEvent: CourseEvent): CourseScheduleItemTag {\n    const type = courseEvent.event.type as EventType;\n    switch (type) {\n      case EventType.SelfStudy:\n        return CourseScheduleItemTag.SelfStudy;\n      default:\n        return CourseScheduleItemTag.Lecture;\n    }\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/dto/course-copy-from.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber } from 'class-validator';\n\nexport class CourseCopyFromDto {\n  @ApiProperty()\n  @IsNumber()\n  copyFromCourseId: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/dto/course-schedule-hash.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'class-validator';\n\nexport class CourseScheduleTokenDto {\n  @ApiProperty()\n  @IsString()\n  public token: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/dto/course-schedule-item.dto.ts",
    "content": "import { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { PersonDto } from 'src/core/dto';\nimport {\n  CourseScheduleDataSource,\n  CourseScheduleItem,\n  CourseScheduleItemStatus,\n  CourseScheduleItemTag,\n} from '../course-schedule.service';\n\n@ApiResponse({})\nexport class CourseScheduleItemDto {\n  constructor(item: CourseScheduleItem) {\n    this.id = item.id;\n    this.name = item.name;\n    this.startDate = (item.startDate as Date)?.toISOString();\n    this.endDate = (item.endDate as Date)?.toISOString();\n    this.maxScore = item.maxScore ?? null;\n    this.scoreWeight = item.scoreWeight ?? null;\n    this.organizer = item.organizer ?? null;\n    this.status = item.status;\n    this.score = item.score ?? null;\n    this.tag = item.tag ?? null;\n    this.descriptionUrl = item.descriptionUrl ?? null;\n    this.type = item.type;\n  }\n\n  @ApiProperty({ type: Number, nullable: true })\n  score: number | null;\n\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty({ enum: CourseScheduleItemStatus })\n  status: CourseScheduleItemStatus;\n\n  @ApiProperty()\n  startDate: string;\n\n  @ApiProperty()\n  endDate: string;\n\n  @ApiProperty()\n  crossCheckEndDate: string;\n\n  @ApiProperty({ nullable: true, type: PersonDto })\n  organizer: PersonDto | null;\n\n  @ApiProperty({ nullable: true, type: Number })\n  maxScore: number | null;\n\n  @ApiProperty({ nullable: true, type: Number })\n  scoreWeight: number | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  descriptionUrl: string | null;\n\n  @ApiProperty({ enum: CourseScheduleItemTag })\n  tag: CourseScheduleItemTag;\n\n  @ApiProperty({ enum: CourseScheduleDataSource })\n  type: CourseScheduleDataSource;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/dto/index.ts",
    "content": "export * from './course-schedule-item.dto';\nexport * from './course-schedule-hash.dto';\n"
  },
  {
    "path": "nestjs/src/courses/course-schedule/index.ts",
    "content": "export * from './course-schedule.service';\nexport * from './course-schedule.controller';\nexport * from './course-icalendar.service';\nexport * from './course-icalendar.controller';\n"
  },
  {
    "path": "nestjs/src/courses/course-students/course-students.controller.ts",
    "content": "import { Body, Controller, Get, NotFoundException, Param, Post, UseGuards } from '@nestjs/common';\nimport { ApiBadRequestResponse, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { StudentSummaryDto } from './dto/student-summary.dto';\nimport { CourseStudentsService } from './course-students.service';\nimport { ExpelStatusDto } from './dto/student-status.dto';\n\n@Controller('courses/:courseId/students')\n@ApiTags('students')\n@UseGuards(DefaultGuard)\nexport class CourseStudentsController {\n  constructor(private courseStudentService: CourseStudentsService) {}\n\n  @Get(':githubId/summary')\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOkResponse({\n    type: StudentSummaryDto,\n  })\n  @ApiOperation({ operationId: 'getStudentSummary' })\n  public async getStudentSummary(@Param('courseId') courseId: number, @Param('githubId') githubId: string) {\n    const student = await this.courseStudentService.getStudentByGithubId(courseId, githubId);\n\n    if (student === null) {\n      throw new NotFoundException(`Student with GitHub id ${githubId} not found`);\n    }\n    const [score, mentor] = await Promise.all([\n      this.courseStudentService.getStudentScore(student?.id),\n      student?.mentorId ? await this.courseStudentService.getMentorWithContacts(student.mentorId) : null,\n    ]);\n\n    return new StudentSummaryDto({\n      totalScore: score?.totalScore,\n      results: score?.results,\n      rank: score?.rank,\n      isActive: !student?.isExpelled && !student?.isFailed,\n      mentor,\n      repository: student?.repository ?? null,\n    });\n  }\n\n  @Post('expel')\n  @ApiOperation({ operationId: 'expelStudents' })\n  @UseGuards(RoleGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  public async expelStudents(@Param('courseId') courseId: number, @Body() expelStatusDto: ExpelStatusDto) {\n    return this.courseStudentService.expelStudents({\n      courseId,\n      expelStatusDto,\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-students/course-students.service.ts",
    "content": "import { User } from '@entities/user';\nimport { StageInterview, StageInterviewFeedback, Mentor, Student } from '@entities/index';\n\nimport { TaskInterviewResult } from '@entities/taskInterviewResult';\nimport { TaskResult } from '@entities/taskResult';\nimport { Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { Mentor as MentorWithContacts } from './dto/mentor-student-summary.dto';\nimport { MentorBasic, StageInterviewFeedbackJson } from '@common/models';\nimport { ExpelStatusDto } from './dto/student-status.dto';\n\n@Injectable()\nexport class CourseStudentsService {\n  constructor(\n    @InjectRepository(Student)\n    readonly studentRepository: Repository<Student>,\n\n    @InjectRepository(Mentor)\n    readonly mentorRepository: Repository<Mentor>,\n  ) {}\n\n  async getStudentByGithubId(courseId: number, githubId: string): Promise<Student | null> {\n    const record = await this.studentRepository.findOne({\n      where: {\n        courseId,\n        user: { githubId },\n      },\n      relations: ['user'],\n    });\n\n    if (record == null) {\n      return null;\n    }\n    return record;\n  }\n\n  public async getStudentScore(studentId: number) {\n    const student = await this.studentRepository\n      .createQueryBuilder('student')\n      .leftJoinAndSelect('student.taskResults', 'taskResults')\n      .leftJoin('taskResults.courseTask', 'courseTask')\n      .addSelect(['courseTask.disabled', 'courseTask.id'])\n      .leftJoinAndSelect('student.taskInterviewResults', 'taskInterviewResults')\n      .leftJoin('student.stageInterviews', 'si')\n      .leftJoin('si.stageInterviewFeedbacks', 'sif')\n      .addSelect([\n        'sif.stageInterviewId',\n        'sif.json',\n        'si.isCompleted',\n        'si.id',\n        'si.courseTaskId',\n        'si.score',\n        'sif.version',\n      ])\n      .where('student.id = :studentId', { studentId })\n      .getOne();\n\n    if (!student) return null;\n\n    const { taskResults, taskInterviewResults, stageInterviews } = student;\n\n    const toTaskScore = ({ courseTaskId, score = 0 }: TaskResult | TaskInterviewResult) => ({ courseTaskId, score });\n\n    const results = [];\n\n    if (taskResults?.length) {\n      results.push(...(taskResults.filter(taskResult => !taskResult.courseTask.disabled).map(toTaskScore) ?? []));\n    }\n\n    if (taskInterviewResults?.length) {\n      results.push(...taskInterviewResults.map(toTaskScore));\n    }\n\n    // we have a case when technical screening score are set as task result.\n    if (stageInterviews?.length && !results.find(tr => tr.courseTaskId === stageInterviews[0]?.courseTaskId)) {\n      const feedbackVersion = stageInterviews[0]?.stageInterviewFeedbacks[0]?.version;\n      const score = !feedbackVersion\n        ? Math.floor(getStageInterviewRating(stageInterviews) ?? 0)\n        : stageInterviews[0]?.score;\n\n      results.push({\n        score,\n        courseTaskId: stageInterviews[0]?.courseTaskId,\n      });\n    }\n\n    return {\n      totalScore: student.totalScore,\n      rank: student.rank ?? 999999,\n      results,\n    };\n  }\n\n  async getMentorWithContacts(mentorId: number): Promise<MentorWithContacts> {\n    const record = await this.mentorRepository.findOne({\n      relations: ['user'],\n      where: {\n        id: mentorId,\n      },\n    });\n\n    if (!record) {\n      throw new NotFoundException(`Mentor not found ${mentorId}`);\n    }\n\n    const mentor = convertToMentorBasic(record);\n    const user = record.user as User;\n    const mentorWithContacts: MentorWithContacts = {\n      ...mentor,\n      contactsEmail: user.contactsEmail,\n      contactsSkype: user.contactsSkype,\n      contactsWhatsApp: user.contactsWhatsApp,\n      contactsTelegram: user.contactsTelegram,\n      contactsNotes: user.contactsNotes,\n      contactsPhone: null,\n    };\n    return mentorWithContacts;\n  }\n\n  public async expelStudents({ courseId, expelStatusDto }: { courseId: number; expelStatusDto: ExpelStatusDto }) {\n    const { criteria, options, expellingReason } = expelStatusDto;\n\n    // duplicate updateStatuses query from the /server/src/routes/course/students.ts\n    let query = this.studentRepository.createQueryBuilder('student').select(['student.id', 'student.mentorId']);\n\n    if (criteria.courseTaskIds && criteria.courseTaskIds.length > 0) {\n      query = query.leftJoin(\n        'student.taskResults',\n        'tr',\n        'tr.studentId = student.id AND tr.score > 0 AND tr.courseTaskId IN (:...requiredCourseTaskIds)',\n        {\n          requiredCourseTaskIds: criteria.courseTaskIds,\n        },\n      );\n    }\n\n    query = query.where('student.courseId = :courseId', { courseId }).andWhere('student.isExpelled = false');\n\n    if (options.keepWithMentor) {\n      query = query.andWhere('student.mentorId IS NULL');\n    }\n\n    if (criteria.minScore != null) {\n      query = query.andWhere('student.totalScore < :minScore', { minScore: criteria.minScore });\n    }\n\n    if (criteria.courseTaskIds && criteria.courseTaskIds.length > 0) {\n      query = query.andWhere('tr.id IS NULL');\n    }\n\n    const students = await query.getMany();\n\n    await this.studentRepository.save(\n      students.map(({ id, mentorId }) => ({\n        id,\n        isExpelled: true,\n        endDate: new Date(),\n        expellingReason,\n        // key difference with the original query - remove mentor by default\n        mentorId: options.saveAssigningToMentor ? mentorId : null,\n      })),\n    );\n\n    return students;\n  }\n}\n\nconst getStageInterviewRating = (stageInterviews: StageInterview[]) => {\n  const [lastInterview] = stageInterviews\n    .filter((interview: StageInterview) => interview.isCompleted)\n    .map(({ stageInterviewFeedbacks, score }: StageInterview) =>\n      stageInterviewFeedbacks.map((feedback: StageInterviewFeedback) => ({\n        date: feedback.updatedDate,\n        // interviews in new template should have precalculated score\n        rating: score ?? getInterviewRatings(JSON.parse(feedback.json) as StageInterviewFeedbackJson).rating,\n      })),\n    )\n    .reduce((acc, cur) => acc.concat(cur), [])\n    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());\n\n  return lastInterview?.rating ?? null;\n};\n\nexport function getInterviewRatings({ skills, programmingTask, resume }: StageInterviewFeedbackJson) {\n  const commonSkills = Object.values(skills?.common ?? {}).filter(Boolean) as number[];\n  const dataStructuresSkills = Object.values(skills?.dataStructures ?? {}).filter(Boolean) as number[];\n\n  const htmlCss = skills?.htmlCss.level;\n  const common = commonSkills.reduce((acc, cur) => acc + cur, 0) / commonSkills.length;\n  const dataStructures = dataStructuresSkills.reduce((acc, cur) => acc + cur, 0) / dataStructuresSkills.length;\n\n  if (resume?.score !== undefined) {\n    const rating = resume.score;\n    return { rating, htmlCss, common, dataStructures };\n  }\n\n  const ratingsCount = 4;\n  const ratings = [htmlCss, common, dataStructures, programmingTask.codeWritingLevel].filter(Boolean) as number[];\n  const rating = (ratings.length === ratingsCount ? ratings.reduce((sum, num) => sum + num) / ratingsCount : 0) * 10;\n\n  return { rating, htmlCss, common, dataStructures };\n}\n\nexport function convertToMentorBasic(mentor: Mentor): MentorBasic {\n  const { user, isExpelled, id, students } = mentor;\n  return {\n    isActive: !isExpelled,\n    name: createName(user),\n    id: id,\n    githubId: user.githubId,\n    students: students ? students.filter(s => !s.isExpelled && !s.isFailed).map(s => ({ id: s.id })) : [],\n    cityName: user.cityName ?? '',\n    countryName: user.countryName ?? '',\n  };\n}\n\nexport function createName({ firstName, lastName }: { firstName: string; lastName: string }) {\n  const result = [];\n  if (firstName) {\n    result.push(firstName.trim());\n  }\n  if (lastName) {\n    result.push(lastName.trim());\n  }\n  return result.join(' ');\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-students/dto/mentor-student-summary.dto.ts",
    "content": "import { MentorBasic, StudentBasic } from '@common/models';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport type Mentor = MentorBasic & {\n  contactsEmail: string | null;\n  contactsPhone: string | null;\n  contactsSkype: string | null;\n  contactsTelegram: string | null;\n  contactsNotes: string | null;\n  contactsWhatsApp: string | null;\n};\n\nexport class MentorStudentSummaryDto {\n  constructor(mentor: Mentor) {\n    this.id = mentor.id;\n    this.githubId = mentor.githubId;\n    this.name = mentor.name;\n    this.isActive = mentor.isActive;\n    this.cityName = mentor.cityName;\n    this.countryName = mentor.countryName;\n    this.students = mentor.students;\n    this.contactsEmail = mentor.contactsEmail;\n    this.contactsPhone = mentor.contactsPhone;\n    this.contactsSkype = mentor.contactsSkype;\n    this.contactsTelegram = mentor.contactsTelegram;\n    this.contactsNotes = mentor.contactsNotes;\n    this.contactsWhatsApp = mentor.contactsWhatsApp;\n  }\n\n  @ApiProperty({ type: Number })\n  id: number;\n\n  @ApiProperty({ type: String })\n  githubId: string;\n\n  @ApiProperty({ type: String })\n  name: string;\n\n  @ApiProperty({ type: Boolean })\n  isActive: boolean;\n\n  @ApiProperty({ type: String })\n  cityName: string;\n\n  @ApiProperty({ type: String })\n  countryName: string;\n\n  @ApiProperty({ type: Array<StudentBasic | { id: number }> })\n  students: (StudentBasic | { id: number })[];\n\n  @ApiProperty({ type: String, nullable: true })\n  contactsEmail: string | null;\n\n  @ApiProperty({ type: String, nullable: true })\n  contactsPhone: string | null;\n\n  @ApiProperty({ type: String, nullable: true })\n  contactsSkype: string | null;\n\n  @ApiProperty({ type: String, nullable: true })\n  contactsTelegram: string | null;\n\n  @ApiProperty({ type: String, nullable: true })\n  contactsNotes: string | null;\n\n  @ApiProperty({ type: String, nullable: true })\n  contactsWhatsApp: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-students/dto/result.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class ResultDto {\n  @ApiProperty({ type: Number, required: false })\n  score?: number;\n\n  @ApiProperty({ type: Number, required: false })\n  courseTaskId?: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-students/dto/student-status.dto.ts",
    "content": "import { IsArray, IsBoolean, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\n\nclass ExpelCriteriaDto {\n  @ApiProperty({\n    example: [123, 456, 789],\n    description: 'Array of course task IDs',\n    required: false,\n    type: [Number],\n  })\n  @IsOptional()\n  @IsArray()\n  @IsNumber({}, { each: true })\n  courseTaskIds?: number[];\n\n  @ApiProperty({\n    example: 100,\n    description: 'Minimum score threshold',\n    required: false,\n  })\n  @IsOptional()\n  @IsNumber()\n  minScore?: number;\n}\n\nclass ExpelOptionsDto {\n  @ApiProperty({\n    example: true,\n    description: 'Whether to keep the student with their mentor',\n    required: false,\n  })\n  @IsOptional()\n  @IsBoolean()\n  keepWithMentor?: boolean;\n\n  @ApiProperty({\n    example: true,\n    description: 'Save assigning to the mentor (default: false)',\n    required: false,\n  })\n  @IsOptional()\n  @IsBoolean()\n  saveAssigningToMentor?: boolean;\n}\n\nexport class ExpelStatusDto {\n  @ApiProperty({\n    type: ExpelCriteriaDto,\n    description: 'Criteria for expelling students',\n  })\n  @ValidateNested()\n  @Type(() => ExpelCriteriaDto)\n  criteria: ExpelCriteriaDto;\n\n  @ApiProperty({\n    type: ExpelOptionsDto,\n    description: 'Additional options for expelling',\n  })\n  @ValidateNested()\n  @Type(() => ExpelOptionsDto)\n  options: ExpelOptionsDto;\n\n  @ApiProperty({\n    example: 'Cheating',\n    description: 'Reason for expelling the student',\n  })\n  @IsString()\n  expellingReason: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-students/dto/student-summary.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Mentor, MentorStudentSummaryDto } from './mentor-student-summary.dto';\nimport { ResultDto } from './result.dto';\n\nexport interface StudentSummary {\n  totalScore?: number;\n  results?: Array<ResultDto>;\n  isActive: boolean;\n  mentor: Mentor | null;\n  rank?: number;\n  repository: string | null;\n}\n\nexport class StudentSummaryDto {\n  constructor(studentSummary: StudentSummary) {\n    this.totalScore = studentSummary.totalScore ?? 0;\n    this.results = studentSummary.results ?? [];\n    this.isActive = studentSummary.isActive;\n    this.mentor = studentSummary.mentor;\n    this.rank = studentSummary.rank ?? 999999;\n    this.repository = studentSummary.repository;\n  }\n\n  @ApiProperty()\n  totalScore: number;\n\n  @ApiProperty({ type: ResultDto, isArray: true })\n  results: Array<ResultDto>;\n\n  @ApiProperty()\n  isActive: boolean;\n\n  @ApiProperty({ type: MentorStudentSummaryDto, nullable: true })\n  mentor: MentorStudentSummaryDto | null;\n\n  @ApiProperty({ type: Number })\n  rank: number;\n\n  @ApiProperty({ type: String, nullable: true })\n  repository: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/course-tasks.controller.ts",
    "content": "import { Checker, CourseTask } from '@entities/courseTask';\nimport {\n  BadRequestException,\n  Body,\n  Controller,\n  Delete,\n  Get,\n  Param,\n  ParseIntPipe,\n  Post,\n  Put,\n  Query,\n  Req,\n  UseGuards,\n} from '@nestjs/common';\nimport {\n  ApiBadRequestResponse,\n  ApiForbiddenResponse,\n  ApiOkResponse,\n  ApiOperation,\n  ApiQuery,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { CourseTasksService, Status } from './course-tasks.service';\nimport { CourseTaskDetailedDto, CourseTaskDto } from './dto';\nimport { CreateCourseTaskDto } from './dto/create-course-task.dto';\nimport { UpdateCourseTaskDto } from './dto/update-course-task.dto';\n\n@Controller('courses/:courseId/tasks')\n@ApiTags('courses tasks')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class CourseTasksController {\n  constructor(private courseTasksService: CourseTasksService) {}\n\n  @Get()\n  @ApiOkResponse({ type: [CourseTaskDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'getCourseTasks' })\n  @ApiQuery({ name: 'status', enum: ['started', 'inprogress', 'finished'], required: false })\n  @ApiQuery({ name: 'checker', enum: Checker, required: false })\n  @UseGuards(CourseGuard)\n  public async getAll(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Query('status') status?: Status,\n    @Query('checker') checker?: Checker,\n  ): Promise<CourseTaskDto[]> {\n    const isStudent = !!req.user.courses[courseId]?.studentId;\n    const data = await this.courseTasksService.getAll(courseId, status, isStudent, checker);\n    return data.map(item => new CourseTaskDto(item));\n  }\n\n  @Get('/solutions')\n  @ApiOkResponse({ type: [CourseTaskDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'getCourseTasksWithStudentSolution' })\n  @ApiQuery({ name: 'status', enum: ['started', 'inprogress', 'finished'], required: false })\n  @UseGuards(CourseGuard)\n  public async getAllWithStudentSolution(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Query('status') status?: Status,\n  ): Promise<CourseTaskDto[]> {\n    const isStudent = !!req.user.courses[courseId]?.studentId;\n    const studentId = req.user.courses[courseId]?.studentId;\n    if (!studentId) {\n      throw new BadRequestException('You are not a student in this course');\n    }\n    const data = await this.courseTasksService.getAllWithStudentSolution(courseId, studentId, status, isStudent);\n    return data.map(item => new CourseTaskDto(item));\n  }\n\n  @Get('/detailed')\n  @ApiOkResponse({ type: [CourseTaskDetailedDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'getCourseTasksDetailed' })\n  @UseGuards(CourseGuard)\n  public async getAllExtended(\n    @Req() _: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ): Promise<CourseTaskDto[]> {\n    const data = await this.courseTasksService.getAllDetailed(courseId);\n    return data.map(item => new CourseTaskDetailedDto(item));\n  }\n\n  @Get('/:courseTaskId')\n  @ApiOkResponse({ type: CourseTaskDetailedDto })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @UseGuards(CourseGuard)\n  @ApiOperation({ operationId: 'getCourseTask' })\n  public async getById(\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n  ): Promise<CourseTaskDetailedDto> {\n    const data = await this.courseTasksService.getById(courseTaskId);\n    return new CourseTaskDetailedDto(data);\n  }\n\n  @Post('/')\n  @ApiOkResponse({ type: CourseTaskDetailedDto })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'createCourseTask' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  public async createCourseTask(@Param('courseId', ParseIntPipe) courseId: number, @Body() dto: CreateCourseTaskDto) {\n    await this.courseTasksService.createCourseTask({\n      ...(dto as CourseTask),\n      courseId,\n    });\n  }\n\n  @Put('/:courseTaskId')\n  @ApiOkResponse({ type: CourseTaskDetailedDto })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'updateCourseTask' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager], true)\n  public async updateCourseTask(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n    @Body() dto: UpdateCourseTaskDto,\n  ) {\n    await this.courseTasksService.updateCourseTask(courseTaskId, {\n      ...dto,\n      courseId,\n      id: courseTaskId,\n    } as Partial<CourseTask>);\n  }\n\n  @Delete('/:courseTaskId')\n  @ApiOkResponse()\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'deleteCourseTask' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager], true)\n  public async deleteCourseTask(\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n  ) {\n    await this.courseTasksService.disable(courseTaskId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/course-tasks.service.ts",
    "content": "import { Checker, CourseTask, CrossCheckStatus } from '@entities/courseTask';\nimport { User } from '@entities/user';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport {\n  Between,\n  LessThan,\n  LessThanOrEqual,\n  MoreThan,\n  MoreThanOrEqual,\n  Repository,\n  FindOptionsWhere,\n  In,\n} from 'typeorm';\nimport { addHours, subHours } from 'date-fns';\nimport { TaskResult } from '@entities/taskResult';\nimport { TaskInterviewResult } from '@entities/taskInterviewResult';\nimport { TaskSolution } from '@entities/taskSolution';\nimport { StageInterview } from '@entities/index';\n\nexport enum Status {\n  Started = 'started',\n  InProgress = 'inprogress',\n  Finished = 'finished',\n}\n\n@Injectable()\nexport class CourseTasksService {\n  constructor(\n    @InjectRepository(CourseTask)\n    readonly courseTaskRepository: Repository<CourseTask>,\n    @InjectRepository(TaskSolution)\n    readonly taskSolutionRepository: Repository<TaskSolution>,\n  ) {}\n\n  public getAll(courseId: number, status?: 'started' | 'inprogress' | 'finished', useCache = false, checker?: Checker) {\n    return this.courseTaskRepository.find({\n      where: { courseId, disabled: false, ...this.getFindConditionForStatus(status), checker: checker },\n      relations: ['task', 'taskOwner'],\n      order: {\n        studentEndDate: 'ASC',\n        studentStartDate: 'ASC',\n      },\n      cache: useCache ? 60 * 1000 : undefined,\n    });\n  }\n\n  public async getAllWithStudentSolution(\n    courseId: number,\n    studentId: number,\n    status?: 'started' | 'inprogress' | 'finished',\n    useCache = false,\n  ) {\n    const courseTasks = await this.getAll(courseId, status, useCache);\n    const taskSolutions = await this.taskSolutionRepository.findBy({\n      courseTaskId: In(courseTasks.map(courseTask => courseTask.id)),\n      studentId,\n    });\n\n    const studentCourseTaskSolutions = courseTasks.map(courseTask => {\n      const taskSolution = taskSolutions.find(({ courseTaskId }) => courseTaskId === courseTask.id);\n      return { ...courseTask, ...(taskSolution && { taskSolutions: [taskSolution] }) };\n    });\n\n    return studentCourseTaskSolutions;\n  }\n\n  public async getAllDetailed(courseId: number) {\n    const [courseTasks, courseTaskResults] = await Promise.all([\n      this.getAll(courseId),\n      // get info about task results for each task\n      this.courseTaskRepository\n        .createQueryBuilder('ct')\n        .select('ct.id', 'id')\n        .addSelect('COUNT(r.id)', 'resultsCount')\n        .addSelect('COUNT(i.id)', 'interviewResultsCount')\n        .addSelect('COUNT(CASE WHEN si.score > 0 THEN 1 END)', 'stageInterviewResultsCount')\n        .leftJoin(TaskResult, 'r', 'r.courseTaskId = ct.id')\n        .leftJoin(TaskInterviewResult, 'i', 'i.courseTaskId = ct.id')\n        .leftJoin(StageInterview, 'si', 'si.courseTaskId = ct.id')\n        .where('ct.courseId = :courseId', { courseId })\n        .groupBy('ct.id')\n        .getRawMany<{\n          id: number;\n          resultsCount: number;\n          interviewResultsCount: number;\n          stageInterviewResultsCount: number;\n        }>(),\n    ]);\n    return courseTasks.map(courseTask => {\n      const result = courseTaskResults.find(({ id }) => id === courseTask.id);\n      return {\n        ...courseTask,\n        resultsCount: Number(result?.resultsCount || 0),\n        interviewResultsCount: Number(result?.interviewResultsCount || 0),\n        stageInterviewResultsCount: Number(result?.stageInterviewResultsCount ?? 0),\n      };\n    });\n  }\n\n  public getById(courseTaskId: number) {\n    return this.courseTaskRepository.findOneOrFail({\n      where: { id: courseTaskId },\n      relations: ['task'],\n    });\n  }\n\n  public getByOwner(username: string) {\n    return this.courseTaskRepository\n      .createQueryBuilder('t')\n      .leftJoin(User, 'u', 'u.id = t.taskOwnerId')\n      .where(`t.checker = :checker`, { checker: Checker.TaskOwner })\n      .andWhere('u.githubId = :username', { username })\n      .getMany();\n  }\n\n  private getFindConditionForStatus(status?: 'started' | 'inprogress' | 'finished'): FindOptionsWhere<CourseTask> {\n    const now = new Date().toISOString();\n    let where: FindOptionsWhere<CourseTask> = {};\n\n    switch (status) {\n      case 'started':\n        where = { ...where, studentStartDate: LessThanOrEqual(now) };\n        break;\n      case 'inprogress':\n        where = { ...where, studentStartDate: LessThanOrEqual(now), studentEndDate: MoreThan(now) };\n        break;\n      case 'finished':\n        where = { ...where, studentEndDate: LessThan(now) };\n        break;\n    }\n    return where;\n  }\n\n  public getUpdatedTasks(courseId: number, lastHours: number) {\n    const date = subHours(new Date(), lastHours);\n\n    return this.courseTaskRepository.find({\n      where: { courseId, updatedDate: MoreThanOrEqual(date.toISOString()) },\n      relations: ['task'],\n    });\n  }\n\n  public getTasksPendingDeadline(\n    courseId: number,\n    { deadlineWithinHours = 24, safeBuffer = 1 }: { deadlineWithinHours?: number; safeBuffer?: number } = {},\n  ) {\n    const now = new Date();\n    const endDate = addHours(now, deadlineWithinHours).toISOString();\n\n    const where: FindOptionsWhere<CourseTask> = {\n      courseId,\n      disabled: false,\n      studentStartDate: LessThanOrEqual(now.toISOString()),\n      studentEndDate: Between(addHours(now, safeBuffer).toISOString(), endDate),\n    };\n\n    return this.courseTaskRepository.find({\n      where,\n      relations: ['task', 'taskSolutions'],\n      order: {\n        studentEndDate: 'ASC',\n      },\n    });\n  }\n\n  public createCourseTask(courseEvent: Partial<CourseTask>) {\n    return this.courseTaskRepository.insert(courseEvent);\n  }\n\n  public updateCourseTask(id: number, courseEvent: Partial<CourseTask>) {\n    return this.courseTaskRepository.update(id, courseEvent);\n  }\n\n  public disable(id: number) {\n    return this.courseTaskRepository.update(id, {\n      id, // required to get right update in subscription\n      disabled: true,\n    });\n  }\n\n  public changeCourseTaskProcessing(id: number, isProcessing: boolean) {\n    return this.courseTaskRepository.update(id, {\n      isCreatingInterviewPairs: isProcessing,\n    });\n  }\n\n  public getAvailableCrossChecks(courseId: number) {\n    return this.courseTaskRepository.find({\n      where: { courseId, checker: Checker.CrossCheck, crossCheckStatus: CrossCheckStatus.Distributed, disabled: false },\n      relations: { task: true },\n      select: {\n        id: true,\n        task: {\n          name: true,\n        },\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/dto/course-task-detailed.dto.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { CourseTaskDto } from './course-task.dto';\n\n@ApiResponse({})\nexport class CourseTaskDetailedDto extends CourseTaskDto {\n  constructor(\n    courseTask: CourseTask & {\n      resultsCount?: number;\n      interviewResultsCount?: number;\n      stageInterviewResultsCount?: number;\n    },\n  ) {\n    super(courseTask);\n    this.publicAttributes = courseTask.task?.attributes?.['public'] ?? {};\n    this.githubRepoName = courseTask.task.githubRepoName;\n    this.sourceGithubRepoUrl = courseTask.task.sourceGithubRepoUrl;\n    this.resultsCount =\n      courseTask.resultsCount || courseTask.interviewResultsCount || courseTask.stageInterviewResultsCount || 0;\n  }\n\n  @ApiProperty()\n  publicAttributes: unknown;\n\n  @ApiProperty()\n  githubRepoName: string;\n\n  @ApiProperty()\n  sourceGithubRepoUrl: string;\n\n  @ApiProperty()\n  resultsCount: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/dto/course-task.dto.ts",
    "content": "import { CourseTask, Checker, CrossCheckStatus, CourseTaskValidation } from '@entities/courseTask';\nimport { TaskType } from '@entities/task';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsOptional } from 'class-validator';\nimport { PersonDto } from 'src/core/dto';\nimport { TaskSolutionDto } from 'src/courses/task-solutions/dto';\n\nclass Validations {\n  @ApiProperty()\n  [CourseTaskValidation.githubIdInUrl]: boolean;\n  @ApiProperty()\n  [CourseTaskValidation.githubPrInUrl]: boolean;\n}\n\n@ApiResponse({})\nexport class CourseTaskDto {\n  constructor(courseTask: CourseTask) {\n    this.id = courseTask.id;\n    this.taskId = courseTask.taskId;\n    this.type = courseTask.type;\n    this.name = courseTask.task.name;\n    this.studentStartDate = (courseTask.studentStartDate as Date)?.toISOString();\n    this.studentEndDate = (courseTask.studentEndDate as Date)?.toISOString();\n    this.maxScore = courseTask.maxScore;\n    this.scoreWeight = courseTask.scoreWeight;\n    this.descriptionUrl = courseTask.task.descriptionUrl;\n    this.checker = courseTask.checker;\n    this.crossCheckStatus = courseTask.crossCheckStatus;\n    this.crossCheckEndDate = (courseTask.crossCheckEndDate as Date)?.toISOString() ?? null;\n    this.pairsCount = courseTask.pairsCount;\n    this.submitText = courseTask.submitText;\n    this.taskOwner = courseTask.taskOwner ? new PersonDto(courseTask.taskOwner) : null;\n    this.validations = courseTask.validations;\n    this.taskSolutions = courseTask.taskSolutions?.map(taskSolution => new TaskSolutionDto(taskSolution)) ?? null;\n    this.studentRegistrationStartDate = courseTask.studentRegistrationStartDate;\n  }\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  id: number;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  taskId: number;\n\n  @IsNotEmpty()\n  @ApiProperty({ enum: TaskType })\n  type: string;\n\n  @IsNotEmpty()\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty({ enum: Checker })\n  checker: Checker;\n\n  @ApiProperty()\n  studentStartDate: string;\n\n  @ApiProperty()\n  studentEndDate: string;\n\n  @ApiProperty({ nullable: true, type: String })\n  studentRegistrationStartDate?: string | null | Date;\n\n  @ApiProperty({ nullable: true, type: String })\n  crossCheckEndDate: string | null;\n\n  @ApiProperty()\n  descriptionUrl: string;\n\n  @ApiProperty({ nullable: true, type: PersonDto })\n  taskOwner: PersonDto | null;\n\n  @ApiProperty({ nullable: true })\n  taskSolutions: TaskSolutionDto[] | null;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  maxScore: number;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  scoreWeight: number;\n\n  @IsNumber()\n  @IsOptional()\n  @ApiProperty({ type: Number, nullable: true })\n  pairsCount: number | null;\n\n  @IsNotEmpty()\n  @ApiProperty({ enum: CrossCheckStatus, enumName: 'CrossCheckStatusEnum' })\n  crossCheckStatus: CrossCheckStatus;\n\n  @ApiProperty({ nullable: true, type: String })\n  submitText: string | null;\n\n  @ApiProperty({ nullable: true, type: Validations })\n  validations: Record<CourseTaskValidation, boolean> | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/dto/create-course-task.dto.ts",
    "content": "import { Checker, CourseTaskValidation } from '@entities/courseTask';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsOptional } from 'class-validator';\nimport { TaskType } from '@entities/task';\n\nexport class CreateCourseTaskDto {\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  taskId: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  maxScore?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  scoreWeight?: number;\n\n  @ApiProperty({ type: Checker, enum: Checker, enumName: 'CheckerEnum' })\n  @IsNotEmpty()\n  checker: Checker;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  studentStartDate: string;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  studentEndDate: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  studentRegistrationStartDate?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  crossCheckEndDate?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  taskOwnerId?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  pairsCount?: number;\n\n  @IsNotEmpty()\n  @IsOptional()\n  @ApiProperty({ enum: TaskType })\n  type?: string;\n\n  @IsOptional()\n  @ApiProperty({ type: String })\n  submitText?: string;\n\n  @IsOptional()\n  @ApiProperty()\n  validations?: Record<CourseTaskValidation, boolean> | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/dto/index.ts",
    "content": "export * from './course-task.dto';\nexport * from './course-task-detailed.dto';\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/dto/update-course-task.dto.ts",
    "content": "import { CourseTaskValidation } from '@entities/courseTask';\nimport { TaskType } from '@entities/task';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from 'class-validator';\n\nexport class UpdateCourseTaskDto {\n  @IsOptional()\n  @ApiProperty({ enum: TaskType, required: false })\n  type?: string;\n\n  @IsOptional()\n  @ApiProperty({ required: false })\n  name?: string;\n\n  @IsOptional()\n  @ApiProperty({ required: false })\n  checker?: string;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  studentStartDate: string;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  studentEndDate: string;\n\n  @IsOptional()\n  @ApiProperty({ required: false })\n  descriptionUrl: string;\n\n  @ApiProperty({ required: false })\n  @IsNumber()\n  @IsOptional()\n  taskOwnerId?: number;\n\n  @IsOptional()\n  @IsNumber()\n  @ApiProperty({ required: false })\n  maxScore?: number;\n\n  @IsOptional()\n  @IsNumber()\n  @ApiProperty({ required: false })\n  scoreWeight?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  pairsCount?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  taskId?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  crossCheckEndDate?: string;\n\n  @IsOptional()\n  @IsString()\n  @ApiProperty({ type: String })\n  submitText?: string;\n\n  @IsOptional()\n  @IsObject()\n  @ApiProperty()\n  validations?: Record<CourseTaskValidation, boolean> | null;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  studentRegistrationStartDate?: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-tasks/index.ts",
    "content": "export * from './course-tasks.service';\nexport * from './course-tasks.controller';\n"
  },
  {
    "path": "nestjs/src/courses/course-users/course-users.controller.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { CourseUsersController } from './course-users.controller';\nimport { CourseUsersService } from './course-users.service';\nimport { BadRequestException } from '@nestjs/common';\nimport { ExtendedCourseUser } from './types';\nimport { omit } from 'lodash';\nimport { UsersService } from 'src/users/users.service';\n\nconst mockUserId = 111;\nconst mockCourseId = 333;\nconst mockGithubId = 'john-doe';\n\nconst mockCourseUser = {\n  id: mockUserId,\n  courseId: mockCourseId,\n  userId: mockUserId,\n  isManager: false,\n  isSupervisor: true,\n  isJuryActivist: false,\n  isDementor: true,\n  isActivist: false,\n  githubId: mockGithubId,\n  name: 'Foo Bar',\n} as ExtendedCourseUser;\n\nconst mockGetByUserId = vi.fn();\nconst mockGetCourseUsersByCourseId = vi.fn();\nconst mockGetUsersToUpdateAndToInsert = vi.fn();\nconst mockSaveCourseUsers = vi.fn();\nconst mockUpdateCourseUser = vi.fn();\nconst mockUpdateCourseUsersRoles = vi.fn();\n\nconst mockCourseUsersServiceFactory = vi.fn(() => ({\n  getByUserId: mockGetByUserId,\n  getCourseUsersByCourseId: mockGetCourseUsersByCourseId,\n  getUsersToUpdateAndToInsert: mockGetUsersToUpdateAndToInsert,\n  saveCourseUsers: mockSaveCourseUsers,\n  updateCourseUser: mockUpdateCourseUser,\n  updateCourseUsersRoles: mockUpdateCourseUsersRoles,\n}));\n\nconst mockGetByGithubId = vi.fn();\n\nconst mockUsersServiceFactory = vi.fn(() => ({\n  getByGithubId: mockGetByGithubId,\n}));\n\ndescribe('CourseUsersController', () => {\n  let controller: CourseUsersController;\n  let mockCourseUsersService: CourseUsersService;\n  let mockUsersService: UsersService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      controllers: [CourseUsersController],\n      providers: [\n        { provide: CourseUsersService, useFactory: mockCourseUsersServiceFactory },\n        { provide: UsersService, useFactory: mockUsersServiceFactory },\n      ],\n    }).compile();\n\n    controller = module.get<CourseUsersController>(CourseUsersController);\n    mockCourseUsersService = module.get<CourseUsersService>(CourseUsersService);\n    mockUsersService = module.get<UsersService>(UsersService);\n  });\n\n  describe('getUsers', () => {\n    it('should get users from service and map it to dto', async () => {\n      mockGetCourseUsersByCourseId.mockResolvedValueOnce([mockCourseUser]);\n      const expected = omit(mockCourseUser, ['userId']);\n\n      const result = await controller.getUsers(mockCourseId);\n\n      expect(mockCourseUsersService.getCourseUsersByCourseId).toHaveBeenCalledWith(mockCourseId);\n      expect(result).toMatchObject([expected]);\n    });\n  });\n\n  describe('putUser', () => {\n    it('should update user roles and update course user if user exists', async () => {\n      mockGetByGithubId.mockResolvedValueOnce({ id: mockUserId });\n      mockGetByUserId.mockResolvedValueOnce({ id: mockUserId });\n\n      await controller.putUser(mockCourseId, mockGithubId, mockCourseUser);\n\n      const { isManager, isDementor, isSupervisor, isActivist } = mockCourseUser;\n\n      expect(mockUsersService.getByGithubId).toHaveBeenCalledWith(mockGithubId);\n      expect(mockCourseUsersService.getByUserId).toHaveBeenCalledWith(mockUserId, mockCourseId);\n      expect(mockCourseUsersService.updateCourseUser).toHaveBeenCalledWith(mockUserId, {\n        isDementor,\n        isManager,\n        isSupervisor,\n        isActivist,\n      });\n      expect(mockCourseUsersService.saveCourseUsers).not.toHaveBeenCalled();\n    });\n\n    it('should create course user if user does not exist', async () => {\n      mockGetByGithubId.mockResolvedValueOnce({ id: mockUserId });\n      mockGetByUserId.mockResolvedValueOnce(null);\n\n      await controller.putUser(mockCourseId, mockGithubId, mockCourseUser);\n\n      const { isManager, isDementor, isSupervisor, isActivist } = mockCourseUser;\n\n      expect(mockUsersService.getByGithubId).toHaveBeenCalledWith(mockGithubId);\n      expect(mockCourseUsersService.getByUserId).toHaveBeenCalledWith(mockUserId, mockCourseId);\n      expect(mockCourseUsersService.saveCourseUsers).toHaveBeenCalledWith({\n        courseId: mockCourseId,\n        userId: mockUserId,\n        isDementor,\n        isManager,\n        isSupervisor,\n        isActivist,\n      });\n      expect(mockCourseUsersService.updateCourseUser).not.toHaveBeenCalled();\n    });\n\n    it('should throw BadRequestException if user does not exist', async () => {\n      mockGetByGithubId.mockResolvedValueOnce(null);\n\n      await expect(() => controller.putUser(mockCourseId, mockGithubId, mockCourseUser)).rejects.toThrow(\n        new BadRequestException(`User with githubid ${mockGithubId} is not found`),\n      );\n\n      expect(mockUsersService.getByGithubId).toHaveBeenCalledWith(mockGithubId);\n      expect(mockCourseUsersService.getByUserId).not.toHaveBeenCalled();\n      expect(mockCourseUsersService.saveCourseUsers).not.toHaveBeenCalled();\n      expect(mockCourseUsersService.updateCourseUser).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('putUsers', () => {\n    it('should process users and put them to update and insert', async () => {\n      mockGetUsersToUpdateAndToInsert.mockResolvedValueOnce({\n        usersToInsert: [mockCourseUser],\n        usersToUpdate: [mockCourseUser],\n      });\n\n      await controller.putUsers(mockCourseId, [mockCourseUser, mockCourseUser]);\n\n      expect(mockCourseUsersService.getUsersToUpdateAndToInsert).toHaveBeenCalledWith(\n        [mockCourseUser, mockCourseUser],\n        mockCourseId,\n      );\n      expect(mockCourseUsersService.saveCourseUsers).toHaveBeenCalledWith([mockCourseUser]);\n      expect(mockCourseUsersService.updateCourseUsersRoles).toHaveBeenCalledWith([mockCourseUser]);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/course-users/course-users.controller.ts",
    "content": "import { Controller, Get, Put, UseGuards, Param, ParseIntPipe, Body, BadRequestException } from '@nestjs/common';\nimport {\n  ApiBadRequestResponse,\n  ApiBody,\n  ApiNotFoundResponse,\n  ApiOkResponse,\n  ApiOperation,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { UsersService } from 'src/users/users.service';\n\nimport { CourseUsersService } from './course-users.service';\nimport { CourseUserDto } from './dto/course-user.dto';\nimport { UpdateCourseUserDto } from './dto/update-user.dto';\nimport { CourseRolesDto } from './dto/course-roles.dto';\n\n@Controller('courses/:courseId/users')\n@ApiTags('course users')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class CourseUsersController {\n  constructor(\n    private readonly courseUserService: CourseUsersService,\n    private readonly usersService: UsersService,\n  ) {}\n\n  @Get()\n  @ApiOperation({ operationId: 'getCourseUsers' })\n  @ApiNotFoundResponse()\n  @ApiOkResponse({ type: [CourseUserDto] })\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  async getUsers(@Param('courseId', ParseIntPipe) courseId: number) {\n    const users = await this.courseUserService.getCourseUsersByCourseId(courseId);\n    return users.map(user => new CourseUserDto(user));\n  }\n\n  @Put()\n  @ApiOperation({ operationId: 'putCourseUsers' })\n  @ApiBody({ type: [UpdateCourseUserDto] })\n  @ApiOkResponse()\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  async putUsers(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Body() courseUsersWithRoles: UpdateCourseUserDto[],\n  ) {\n    const { usersToInsert, usersToUpdate } = await this.courseUserService.getUsersToUpdateAndToInsert(\n      courseUsersWithRoles,\n      courseId,\n    );\n\n    if (usersToInsert.length) {\n      await this.courseUserService.saveCourseUsers(usersToInsert);\n    }\n\n    if (usersToUpdate.length) {\n      await this.courseUserService.updateCourseUsersRoles(usersToUpdate);\n    }\n  }\n\n  @Put('/:githubId')\n  @ApiOperation({ operationId: 'putCourseUser' })\n  @ApiOkResponse()\n  @ApiBadRequestResponse()\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  async putUser(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('githubId') githubId: string,\n    @Body() roles: CourseRolesDto,\n  ) {\n    const user = await this.usersService.getByGithubId(githubId);\n\n    if (!user) {\n      throw new BadRequestException(`User with githubid ${githubId} is not found`);\n    }\n\n    const { isManager = false, isSupervisor = false, isDementor = false, isActivist = false } = roles;\n    const courseUser = await this.courseUserService.getByUserId(user.id, courseId);\n    if (isActivist) {\n      this.usersService.updateUser(user.id, { activist: isActivist });\n    }\n\n    if (!courseUser) {\n      await this.courseUserService.saveCourseUsers({\n        courseId,\n        userId: user.id,\n        isManager,\n        isSupervisor,\n        isDementor,\n        isActivist,\n      });\n    } else {\n      await this.courseUserService.updateCourseUser(courseUser.id, { isManager, isSupervisor, isDementor, isActivist });\n    }\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-users/course-users.service.spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { CourseUsersService } from './course-users.service';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { CourseUser } from '@entities/courseUser';\nimport { In } from 'typeorm';\n\nconst mockUserId = 111;\nconst mockUserId2 = 112;\nconst mockUserId3 = 113;\nconst mockCourseUserId = 222;\nconst mockCourseId = 333;\nconst mockGithubId = 'john-doe';\nconst mockFullName = 'Foo Bar';\n\nconst mockCourseUser = {\n  id: mockCourseUserId,\n  courseId: mockCourseId,\n  userId: mockUserId,\n  isManager: false,\n  isSupervisor: true,\n  isJuryActivist: false,\n  isDementor: true,\n  isActivist: true,\n  user: {\n    id: mockUserId,\n    firstName: ' Foo ',\n    lastName: ' Bar ',\n    githubId: mockGithubId,\n  },\n  course: {\n    id: mockCourseId,\n  },\n} as CourseUser;\n\nconst mockUsersToInsert = [\n  {\n    userId: mockUserId3,\n    isDementor: true,\n    isManager: true,\n    isSupervisor: true,\n    isActivist: false,\n  },\n];\nconst mockUsersToUpdate = [\n  {\n    userId: mockUserId,\n    isDementor: true,\n    isManager: false,\n    isSupervisor: true,\n    isActivist: true,\n  },\n  {\n    userId: mockUserId2,\n    isDementor: true,\n    isManager: true,\n    isSupervisor: true,\n    isActivist: false,\n  },\n];\n\nconst mockCourseUsersWithRoles = [...mockUsersToInsert, ...mockUsersToUpdate];\n\nconst mockFindResponse = [mockCourseUser, { ...mockCourseUser, userId: mockUserId2 }];\nconst mockFindOneResponse = mockCourseUser;\nconst mockInsertResponse = { a: 1 };\nconst mockUpdateResponse = { b: 2 };\n\nconst mockFind = vi.fn(() => Promise.resolve(mockFindResponse));\nconst mockFindOne = vi.fn(() => Promise.resolve(mockFindOneResponse));\nconst mockInsert = vi.fn(() => Promise.resolve(mockInsertResponse));\nconst mockUpdate = vi.fn(() => Promise.resolve(mockUpdateResponse));\n\nconst mockCourseUserRepositoryFactory = vi.fn(() => ({\n  find: mockFind,\n  findOne: mockFindOne,\n  insert: mockInsert,\n  update: mockUpdate,\n}));\n\ndescribe('CourseUsersService', () => {\n  let service: CourseUsersService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CourseUsersService,\n        {\n          provide: getRepositoryToken(CourseUser),\n          useFactory: mockCourseUserRepositoryFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<CourseUsersService>(CourseUsersService);\n  });\n\n  describe('getByUserId', () => {\n    it('should find one user by userId and return it', async () => {\n      const response = await service.getByUserId(mockUserId, mockCourseId);\n      expect(mockFindOne).toHaveBeenCalledWith({\n        where: { userId: mockUserId, courseId: mockCourseId },\n      });\n      expect(response).toBe(mockFindOneResponse);\n    });\n  });\n\n  describe('getByGithubId', () => {\n    it('should find one user by githubId and return it', async () => {\n      const response = await service.getByGithubId(mockGithubId, mockCourseId);\n      expect(mockFindOne).toHaveBeenCalledWith({\n        where: { user: { githubId: mockGithubId }, courseId: mockCourseId },\n      });\n      expect(response).toBe(mockFindOneResponse);\n    });\n  });\n\n  describe('getCourseUsersByCourseId', () => {\n    it('should find users by course id and return it', async () => {\n      const expectedResponse = mockFindResponse.map(({ user, course: _, ...rest }) => ({\n        ...rest,\n        name: mockFullName,\n        githubId: user.githubId,\n      }));\n\n      const response = await service.getCourseUsersByCourseId(mockCourseId);\n\n      expect(mockFind).toHaveBeenCalledWith({\n        where: { courseId: mockCourseId },\n        relations: ['user', 'course'],\n      });\n      expect(response).toStrictEqual(expectedResponse);\n    });\n  });\n\n  describe('getUsersToUpdateAndToInsert', () => {\n    it('should check existance of incoming users and correctly split them between upsert and update', async () => {\n      const { usersToInsert, usersToUpdate } = await service.getUsersToUpdateAndToInsert(\n        mockCourseUsersWithRoles,\n        mockCourseId,\n      );\n      const mockUsersIds = mockCourseUsersWithRoles.map(({ userId }) => userId);\n\n      expect(mockFind).toHaveBeenCalledWith({\n        where: {\n          courseId: mockCourseId,\n          userId: In(mockUsersIds),\n        },\n      });\n\n      expect(usersToInsert).toStrictEqual(mockUsersToInsert.map(user => ({ ...user, courseId: mockCourseId })));\n      expect(usersToUpdate).toStrictEqual(\n        mockFindResponse.map((user, index) => ({ ...user, ...mockUsersToUpdate[index] })),\n      );\n    });\n  });\n\n  describe('saveCourseUsers', () => {\n    it('should save users data and return response', async () => {\n      const response = await service.saveCourseUsers(mockCourseUser);\n      expect(mockInsert).toHaveBeenCalledWith(mockCourseUser);\n      expect(response).toBe(mockInsertResponse);\n    });\n  });\n\n  describe('updateCourseUser', () => {\n    it('should update user and return response', async () => {\n      const response = await service.updateCourseUser(mockCourseUserId, mockCourseUser);\n      expect(mockUpdate).toHaveBeenCalledWith(mockCourseUserId, mockCourseUser);\n      expect(response).toBe(mockUpdateResponse);\n    });\n  });\n\n  describe('updateCourseUsersRoles', () => {\n    it('should update users roles', async () => {\n      await service.updateCourseUsersRoles(mockFindResponse);\n      expect(mockUpdate).toHaveBeenCalledTimes(mockFindResponse.length);\n      mockFindResponse.forEach(({ userId, isManager, isSupervisor, isDementor }, idx) => {\n        expect(mockUpdate).toHaveBeenNthCalledWith(idx + 1, { userId }, { isManager, isSupervisor, isDementor });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/course-users/course-users.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { CourseUser } from '@entities/courseUser';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository, In } from 'typeorm';\nimport { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';\nimport { ExtendedCourseUser, PartialCourseUserData, SplitCourseUsers } from './types';\nimport { UpdateCourseUserDto } from './dto/update-user.dto';\n\n@Injectable()\nexport class CourseUsersService {\n  constructor(\n    @InjectRepository(CourseUser)\n    readonly courseUserRepository: Repository<CourseUser>,\n  ) {}\n\n  public getByUserId(userId: number, courseId: number): Promise<CourseUser | null> {\n    return this.courseUserRepository.findOne({\n      where: { userId, courseId },\n    });\n  }\n\n  public getByGithubId(githubId: string, courseId: number): Promise<CourseUser | null> {\n    return this.courseUserRepository.findOne({\n      where: { courseId, user: { githubId } },\n    });\n  }\n\n  public async getCourseUsersByCourseId(courseId: number): Promise<ExtendedCourseUser[]> {\n    const rawCourseUsers = await this.courseUserRepository.find({\n      where: { courseId },\n      relations: ['user', 'course'],\n    });\n\n    const courseUsers = rawCourseUsers.map(({ user, course, ...rest }) => ({\n      ...rest,\n      name: this.createFullName(user.firstName, user.lastName),\n      githubId: user.githubId,\n    }));\n\n    return courseUsers;\n  }\n\n  public async getUsersToUpdateAndToInsert(\n    courseUserWithRoles: UpdateCourseUserDto[],\n    courseId: number,\n  ): Promise<SplitCourseUsers> {\n    const courseUsersWithCourseId = courseUserWithRoles.map(user => ({ ...user, courseId }));\n    const usersIds = courseUserWithRoles.map(({ userId }) => userId);\n\n    const courseUsers = await this.courseUserRepository.find({\n      where: {\n        courseId,\n        userId: In(usersIds),\n      },\n    });\n\n    return courseUsersWithCourseId.reduce<SplitCourseUsers>(\n      (acc, update) => {\n        const foundUser = courseUsers.find(({ userId }) => update.userId === userId);\n        if (foundUser) {\n          acc.usersToUpdate.push({ ...foundUser, ...update });\n        } else {\n          acc.usersToInsert.push(update);\n        }\n        return acc;\n      },\n      { usersToInsert: [], usersToUpdate: [] },\n    );\n  }\n\n  public saveCourseUsers(data: PartialCourseUserData) {\n    return this.courseUserRepository.insert(data);\n  }\n\n  public updateCourseUser(courseUserId: number, data: QueryDeepPartialEntity<CourseUser>) {\n    return this.courseUserRepository.update(courseUserId, data);\n  }\n\n  public async updateCourseUsersRoles(courseUsers: CourseUser[]) {\n    await Promise.all(\n      courseUsers.map(({ userId, isManager, isSupervisor, isDementor }) =>\n        this.courseUserRepository.update({ userId }, { isManager, isSupervisor, isDementor }),\n      ),\n    );\n  }\n\n  private createFullName(firstName: string | null, lastName: string | null) {\n    const names = [firstName?.trim(), lastName?.trim()].filter(Boolean);\n    return names.length ? names.join(' ') : '';\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-users/dto/course-roles.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsOptional } from 'class-validator';\n\nexport class CourseRolesDto {\n  @IsOptional()\n  @IsBoolean()\n  @ApiProperty()\n  isManager: boolean;\n\n  @IsOptional()\n  @IsBoolean()\n  @ApiProperty()\n  isSupervisor: boolean;\n\n  @IsOptional()\n  @IsBoolean()\n  @ApiProperty()\n  isDementor: boolean;\n\n  @IsOptional()\n  @IsBoolean()\n  @ApiProperty()\n  isActivist: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-users/dto/course-user.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator';\nimport { ExtendedCourseUser } from '../types';\n\nexport class CourseUserDto {\n  constructor(courseUser: ExtendedCourseUser) {\n    this.id = courseUser.userId;\n    this.courseId = courseUser.courseId;\n    this.name = courseUser.name;\n    this.githubId = courseUser.githubId;\n    this.isManager = courseUser.isManager;\n    this.isSupervisor = courseUser.isSupervisor;\n    this.isJuryActivist = courseUser.isJuryActivist;\n    this.isDementor = courseUser.isDementor;\n    this.isActivist = courseUser.isActivist;\n  }\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  id: number;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  courseId: number;\n\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @IsString()\n  @ApiProperty()\n  githubId: string;\n\n  @IsBoolean()\n  @ApiProperty()\n  isManager: boolean;\n\n  @IsBoolean()\n  @ApiProperty()\n  isSupervisor: boolean;\n\n  @IsBoolean()\n  @ApiProperty()\n  isJuryActivist: boolean;\n\n  @IsBoolean()\n  @ApiProperty()\n  isDementor: boolean;\n\n  @IsBoolean()\n  @ApiProperty()\n  isActivist: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-users/dto/update-user.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber } from 'class-validator';\nimport { CourseRolesDto } from './course-roles.dto';\n\nexport class UpdateCourseUserDto extends CourseRolesDto {\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  userId: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/course-users/types.ts",
    "content": "import { CourseUser } from '@entities/courseUser';\nimport { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';\nimport { UpdateCourseUserDto } from './dto/update-user.dto';\n\nexport type PartialCourseUserData = QueryDeepPartialEntity<CourseUser> | QueryDeepPartialEntity<CourseUser>[];\n\nexport type ExtendedCourseUser = Omit<CourseUser, 'course' | 'user'> & {\n  name: string;\n  githubId: string;\n};\n\nexport type SplitCourseUsers = {\n  usersToInsert: UpdateCourseUserDto[];\n  usersToUpdate: CourseUser[];\n};\n"
  },
  {
    "path": "nestjs/src/courses/courses.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  ForbiddenException,\n  Get,\n  Param,\n  ParseIntPipe,\n  Post,\n  Put,\n  Req,\n  UseGuards,\n} from '@nestjs/common';\nimport { ApiBody, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { CourseGuard } from '../auth/course.guard';\nimport { CourseAccessService } from './course-access.service';\nimport { CoursesService } from './courses.service';\nimport { CourseDto, LeaveCourseRequestDto, UpdateCourseDto } from './dto';\nimport { CourseScheduleService } from './course-schedule/course-schedule.service';\nimport { CreateCourseDto } from './dto/create-course.dto';\n\n@Controller('courses')\n@ApiTags('courses')\nexport class CoursesController {\n  constructor(\n    private courseService: CoursesService,\n    private courseAccessService: CourseAccessService,\n    private courseScheduleService: CourseScheduleService,\n  ) {}\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getCourses' })\n  @ApiOkResponse({ type: [CourseDto] })\n  @UseGuards(DefaultGuard)\n  public async getCourses() {\n    const data = await this.courseService.getAll();\n    return data.map(it => new CourseDto(it));\n  }\n\n  @Post('/')\n  @ApiOperation({ operationId: 'createCourse' })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiBody({ type: CreateCourseDto })\n  @ApiOkResponse({ type: CourseDto })\n  public async createCourse(@Body() dto: CreateCourseDto) {\n    const created = await this.courseService.create(dto);\n    return new CourseDto(created);\n  }\n\n  @Get('/:courseId')\n  @ApiOperation({ operationId: 'getCourse' })\n  @ApiForbiddenResponse()\n  @ApiOkResponse({ type: CourseDto })\n  @UseGuards(DefaultGuard, CourseGuard)\n  public async getCourse(@Req() _: CurrentRequest, @Param('courseId', ParseIntPipe) courseId: number) {\n    const data = await this.courseService.getById(courseId);\n    return new CourseDto(data);\n  }\n\n  @Put('/:courseId')\n  @ApiOperation({ operationId: 'updateCourse' })\n  @ApiForbiddenResponse()\n  @ApiOkResponse({ type: CourseDto })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Manager, Role.Admin])\n  public async updateCourse(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Body() update: UpdateCourseDto,\n  ) {\n    if (!this.courseAccessService.canAccessCourseAsManager(req.user, courseId)) {\n      throw new ForbiddenException('No access to edit course');\n    }\n    const data = await this.courseService.update(courseId, update);\n    return new CourseDto(data);\n  }\n\n  @Post('/:courseId/leave')\n  @ApiOperation({ operationId: 'leaveCourse' })\n  @ApiBody({ type: LeaveCourseRequestDto, required: false })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Student])\n  public async leaveCourse(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Body() leaveCourseDto: LeaveCourseRequestDto,\n  ) {\n    const studentId = req.user.courses[courseId]?.studentId;\n    if (studentId) {\n      await this.courseAccessService.leaveAsStudent(courseId, studentId, leaveCourseDto);\n    }\n  }\n\n  @Post('/:courseId/rejoin')\n  @ApiOperation({ operationId: 'rejoinCourse' })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Student])\n  public async rejoinCourse(@Req() req: CurrentRequest, @Param('courseId', ParseIntPipe) courseId: number) {\n    const studentId = req.user.courses[courseId]?.studentId;\n    if (studentId) {\n      await this.courseAccessService.rejoinAsStudent(courseId, studentId);\n    }\n  }\n\n  @Post('/:courseId/copy')\n  @ApiOkResponse({ type: CourseDto })\n  @ApiOperation({ operationId: 'copyCourse' })\n  @ApiBody({ type: CreateCourseDto, required: true })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Manager, Role.Admin])\n  public async copyCourse(@Param('courseId') courseId: number, @Body() body: CreateCourseDto) {\n    const created = await this.courseService.create(body);\n    if (created.id) {\n      await this.courseScheduleService.copyFromTo(courseId, created.id);\n    }\n    const course = await this.courseService.getById(created.id);\n    return new CourseDto(course);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/courses.module.ts",
    "content": "import { CacheModule } from '@nestjs/cache-manager';\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\n\nimport { Course } from '@entities/course';\nimport { CourseTask } from '@entities/courseTask';\nimport { CourseUser } from '@entities/courseUser';\nimport { Task } from '@entities/task';\nimport {\n  Student,\n  Mentor,\n  TaskChecker,\n  StudentFeedback,\n  StageInterview,\n  StageInterviewFeedback,\n  TaskSolutionChecker,\n  TaskSolutionResult,\n  CourseEvent,\n  TaskSolution,\n  TaskResult,\n  TaskInterviewResult,\n  User,\n  TaskInterviewStudent,\n  TeamDistribution,\n  Team,\n  TaskVerification,\n  TeamDistributionStudent,\n  StageInterviewStudent,\n} from '@entities/index';\n\nimport { UsersModule } from 'src/users';\nimport { UsersNotificationsModule } from 'src/users-notifications/users-notifications.module';\n\nimport { FeedbacksService, FeedbacksController } from './students/feedbacks';\nimport { CoursesController } from './courses.controller';\nimport { CoursesService } from './courses.service';\nimport { StudentsService, StudentsController } from './students';\nimport { MentorsService, MentorsController } from './mentors';\nimport { CourseAccessService } from './course-access.service';\nimport { CourseTasksController, CourseTasksService } from './course-tasks';\nimport { InterviewsController, InterviewsService, InterviewFeedbackService } from './interviews';\nimport { CrossMentorDistributionService } from './interviews/cross-mentor-distribution.service';\nimport { TasksController } from './tasks/tasks.controller';\nimport { TasksService } from './tasks/tasks.service';\nimport { CourseStatsController, CourseStatsService } from './stats';\nimport { CourseCrossCheckController, CourseCrossCheckService } from './cross-checks';\nimport { CourseEventsController, CourseEventsService } from './course-events';\nimport { ScoreController, ScoreService, WriteScoreService } from './score';\nimport { TaskSolutionsController, TaskSolutionsService } from './task-solutions';\nimport {\n  CourseScheduleService,\n  CourseScheduleController,\n  CourseICalendarService,\n  CourseICalendarController,\n} from './course-schedule';\nimport { CoreModule } from '../core/core.module';\nimport { TeamDistributionController } from './team-distribution/team-distribution.controller';\nimport { TeamDistributionService } from './team-distribution/team-distribution.service';\nimport { TeamService } from './team-distribution/team.service';\nimport { TeamController } from './team-distribution/team.controller';\nimport { TaskVerificationsController } from './task-verifications/task-verifications.controller';\nimport { TaskVerificationsService } from './task-verifications/task-verifications.service';\nimport { TeamDistributionStudentService } from './team-distribution/team-distribution-student.service';\nimport { DistributeStudentsService } from './team-distribution/distribute-students.service';\nimport { CourseUsersController } from './course-users/course-users.controller';\nimport { CourseUsersService } from './course-users/course-users.service';\nimport { CloudApiModule } from 'src/cloud-api/cloud-api.module';\nimport { SelfEducationService } from './task-verifications/self-education.service';\nimport { CourseMentorsController, CourseMentorsService } from './course-mentors';\nimport { CourseStudentsController } from './course-students/course-students.controller';\nimport { CourseStudentsService } from './course-students/course-students.service';\nimport { MentorReviewsController, MentorReviewsService } from './mentor-reviews';\nimport { ConfigModule } from '../config';\nimport { ExpelledStatsService } from './expelled-stats.service';\nimport { CourseLeaveSurveyResponse } from '@entities/index';\n\n@Module({\n  imports: [\n    CacheModule.register(),\n    TypeOrmModule.forFeature([\n      Course,\n      CourseEvent,\n      CourseTask,\n      CourseUser,\n      Mentor,\n      StageInterview,\n      StageInterviewFeedback,\n      StageInterviewStudent,\n      Student,\n      StudentFeedback,\n      Task,\n      TaskChecker,\n      TaskInterviewResult,\n      TaskInterviewStudent,\n      TaskResult,\n      TaskSolution,\n      TaskSolutionChecker,\n      TaskSolutionResult,\n      TaskVerification,\n      Team,\n      TeamDistribution,\n      TeamDistributionStudent,\n      User,\n      CourseLeaveSurveyResponse,\n    ]),\n    CoreModule,\n    UsersModule,\n    UsersNotificationsModule,\n    CloudApiModule,\n    ConfigModule,\n  ],\n  controllers: [\n    FeedbacksController,\n    CoursesController,\n    StudentsController,\n    MentorsController,\n    CourseTasksController,\n    CourseEventsController,\n    InterviewsController,\n    TasksController,\n    CourseStatsController,\n    CourseCrossCheckController,\n    ScoreController,\n    TaskSolutionsController,\n    CourseScheduleController,\n    CourseICalendarController,\n    TeamDistributionController,\n    TeamController,\n    TaskVerificationsController,\n    CourseUsersController,\n    CourseMentorsController,\n    CourseStudentsController,\n    MentorReviewsController,\n  ],\n  providers: [\n    CourseTasksService,\n    CourseEventsService,\n    CourseUsersService,\n    FeedbacksService,\n    CoursesService,\n    StudentsService,\n    MentorsService,\n    CourseAccessService,\n    InterviewsService,\n    InterviewFeedbackService,\n    CrossMentorDistributionService,\n    TasksService,\n    CourseStatsService,\n    CourseCrossCheckService,\n    ScoreService,\n    WriteScoreService,\n    TaskSolutionsService,\n    CourseScheduleService,\n    CourseICalendarService,\n    TeamDistributionService,\n    TeamDistributionStudentService,\n    TeamService,\n    DistributeStudentsService,\n    SelfEducationService,\n    TaskVerificationsService,\n    CourseMentorsService,\n    CourseStudentsService,\n    MentorReviewsService,\n    ExpelledStatsService,\n  ],\n  exports: [CourseTasksService, CourseUsersService, CoursesService, StudentsService, ExpelledStatsService],\n})\nexport class CoursesModule {}\n"
  },
  {
    "path": "nestjs/src/courses/courses.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Course } from '@entities/course';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { FindOptionsWhere, In, Repository } from 'typeorm';\nimport { UpdateCourseDto, CreateCourseDto } from './dto';\n\n@Injectable()\nexport class CoursesService {\n  constructor(\n    @InjectRepository(Course)\n    private readonly repository: Repository<Course>,\n  ) {}\n\n  public async getAll() {\n    return this.repository.find({ order: { startDate: 'DESC' }, relations: ['discipline'] });\n  }\n\n  public async getById(id: number) {\n    return this.repository.findOneOrFail({ where: { id }, relations: ['discipline'] });\n  }\n\n  public async update(id: number, course: UpdateCourseDto) {\n    await this.repository.update(id, course);\n    const updated = await this.repository.findOneByOrFail({ id });\n    return updated;\n  }\n\n  public async create(course: CreateCourseDto) {\n    const { id } = await this.repository.save(course);\n    const created = await this.repository.findOneByOrFail({ id });\n    return created;\n  }\n\n  public async getByIds(ids: number[], filter?: FindOptionsWhere<Course>) {\n    return this.repository.find({\n      where: {\n        id: In(ids),\n        ...filter,\n      },\n    });\n  }\n\n  public getActiveCourses(relations?: ('students' | 'mentors')[]) {\n    return this.repository.find({\n      where: {\n        completed: false,\n      },\n      relations,\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/course-cross-checks.controller.ts",
    "content": "import {\n  BadRequestException,\n  Controller,\n  DefaultValuePipe,\n  Get,\n  Param,\n  ParseEnumPipe,\n  ParseIntPipe,\n  Query,\n  Req,\n  Res,\n  UseGuards,\n} from '@nestjs/common';\nimport { ApiForbiddenResponse, ApiOperation, ApiResponse, ApiTags, ApiQuery } from '@nestjs/swagger';\nimport { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { CourseTasksService } from '../course-tasks';\nimport { OrderField, OrderDirection, CourseCrossCheckService } from './course-cross-checks.service';\nimport { CrossCheckFeedbackDto, CrossCheckPairResponseDto } from './dto';\nimport { AvailableReviewStatsDto } from './dto/available-review-stats.dto';\nimport { parseAsync } from 'json2csv';\nimport { Response } from 'express';\nimport { StudentId } from 'src/core/decorators';\nimport { FeedbackGuard } from './cross-check-feedback.guard';\n\n@Controller('courses/:courseId/cross-checks')\n@ApiTags('courses tasks')\n@UseGuards(DefaultGuard, CourseGuard)\nexport class CourseCrossCheckController {\n  constructor(\n    private courseCrossCheckService: CourseCrossCheckService,\n    private courseTasksService: CourseTasksService,\n  ) {}\n\n  @Get('/pairs')\n  @ApiOperation({ operationId: 'getCrossCheckPairs' })\n  @ApiForbiddenResponse()\n  @ApiResponse({ type: CrossCheckPairResponseDto })\n  @ApiQuery({ name: 'orderBy', required: false })\n  @ApiQuery({ name: 'orderDirection', required: false })\n  @ApiQuery({ name: 'checker', required: false })\n  @ApiQuery({ name: 'student', required: false })\n  @ApiQuery({ name: 'url', required: false })\n  @ApiQuery({ name: 'task', required: false })\n  @RequiredRoles([CourseRole.Manager, CourseRole.Dementor, Role.Admin], true)\n  @UseGuards(DefaultGuard, RoleGuard)\n  public async getPairs(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Query('pageSize') pageSize: number = 100,\n    @Query('current') current: number = 1,\n    @Query('orderBy', new DefaultValuePipe(OrderField.Student), new ParseEnumPipe(OrderField)) orderBy: OrderField,\n    @Query('orderDirection', new DefaultValuePipe(OrderDirection.Asc), new ParseEnumPipe(OrderDirection))\n    orderDirection: OrderDirection,\n    @Query('checker') checker: string,\n    @Query('student') student: string,\n    @Query('url') url: string,\n    @Query('task') task: string,\n  ) {\n    const { items, pagination } = await this.courseCrossCheckService.findPairs(\n      courseId,\n      { pageSize, current },\n      { checker, student, url, task },\n      { orderBy, orderDirection },\n    );\n    return new CrossCheckPairResponseDto(items, pagination);\n  }\n\n  @Get('/available-review-stats')\n  @ApiOperation({ operationId: 'getAvailableCrossCheckReviewStats' })\n  @ApiForbiddenResponse()\n  @ApiResponse({ type: [AvailableReviewStatsDto] })\n  @RequiredRoles([CourseRole.Student])\n  @UseGuards(DefaultGuard, RoleGuard)\n  public async getAvailableCrossCheckReviewStats(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Req() req: CurrentRequest,\n  ) {\n    const studentId = req.user.courses[courseId]?.studentId;\n    if (!studentId) throw new BadRequestException();\n    const crossChecks = await this.courseTasksService.getAvailableCrossChecks(courseId);\n    if (crossChecks.length === 0) return [];\n    const res = await this.courseCrossCheckService.getAvailableCrossChecksStats(crossChecks, studentId);\n    return res.map(e => new AvailableReviewStatsDto(e));\n  }\n\n  @Get(':courseTaskId/csv')\n  @ApiOperation({ operationId: 'getCrossCheckCsv' })\n  @ApiForbiddenResponse()\n  @RequiredRoles([CourseRole.Dementor, CourseRole.Manager, Role.Admin])\n  @UseGuards(DefaultGuard, RoleGuard)\n  public async getSolutionsUrls(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n    @Res() res: Response,\n  ) {\n    const [courseTask, solutionUrls] = await Promise.all([\n      this.courseTasksService.getById(courseTaskId),\n      this.courseCrossCheckService.getSolutionsUrls(courseId, courseTaskId),\n    ]);\n\n    const parsedData = await parseAsync(solutionUrls, { fields: ['githubId', 'solutionUrl'] });\n\n    res.setHeader('Content-Type', 'text/csv');\n    res.setHeader('Content-disposition', `filename=${courseTask.task.name}.csv`);\n\n    res.end(parsedData);\n  }\n\n  @Get(':courseTaskId/feedbacks/my')\n  @ApiOperation({ operationId: 'getMyCrossCheckFeedbacks' })\n  @ApiForbiddenResponse()\n  @ApiResponse({ type: CrossCheckFeedbackDto })\n  @RequiredRoles([CourseRole.Manager, Role.Admin, CourseRole.Student], true)\n  @UseGuards(DefaultGuard, RoleGuard, FeedbackGuard)\n  public async getMyCrossCheckFeedbacks(\n    @StudentId() studentId: number,\n    @Param('courseId', ParseIntPipe) _courseId: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n  ) {\n    const [crossCheckSolutionReviews, taskSolution] = await Promise.all([\n      this.courseCrossCheckService.getCrossCheckSolutionReviews(studentId, courseTaskId),\n      this.courseCrossCheckService.getTaskSolution(studentId, courseTaskId),\n    ]);\n    return new CrossCheckFeedbackDto(crossCheckSolutionReviews, taskSolution);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/course-cross-checks.service.spec.ts",
    "content": "import { getRepositoryToken } from '@nestjs/typeorm';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { TaskSolutionChecker } from '@entities/taskSolutionChecker';\nimport { CourseCrossCheckService } from './course-cross-checks.service';\nimport { TaskSolution } from '@entities/taskSolution';\nimport { TaskSolutionResult } from '@entities/taskSolutionResult';\n\nconst mockRawData = [\n  {\n    githubId: 1,\n    url: 'htpps://example.com',\n    omittedField: 'foo',\n  },\n  {\n    githubId: 2,\n    url: 'htpps://example.com',\n    omittedField: 'bar',\n  },\n];\n\nconst mockTaskSolutionRepositoryFactory = vi.fn(() => ({\n  createQueryBuilder: vi.fn(() => ({\n    leftJoin: vi.fn().mockReturnThis(),\n    addSelect: vi.fn().mockReturnThis(),\n    where: vi.fn().mockReturnThis(),\n    andWhere: vi.fn().mockReturnThis(),\n    getRawMany: () => Promise.resolve(mockRawData),\n  })),\n}));\n\nconst mockCourseId = 12345;\nconst mockCourseTaskId = 54321;\n\ndescribe('CourseCrossCheckService', () => {\n  let service: CourseCrossCheckService;\n\n  beforeAll(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CourseCrossCheckService,\n        {\n          provide: getRepositoryToken(TaskSolution),\n          useFactory: mockTaskSolutionRepositoryFactory,\n        },\n        {\n          provide: getRepositoryToken(TaskSolutionChecker),\n          useValue: {},\n        },\n        {\n          provide: getRepositoryToken(TaskSolutionResult),\n          useValue: {},\n        },\n      ],\n    }).compile();\n\n    service = module.get<CourseCrossCheckService>(CourseCrossCheckService);\n  });\n\n  describe('getSolutionsUrls', () => {\n    it('should return transformed data from repositories correctly', async () => {\n      const pairs = await service.getSolutionsUrls(mockCourseId, mockCourseTaskId);\n\n      expect(pairs).toStrictEqual(\n        mockRawData.map(data => ({\n          githubId: data.githubId,\n          solutionUrl: data.url,\n        })),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/course-cross-checks.service.ts",
    "content": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { Task } from '@entities/task';\nimport { User } from '@entities/user';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { TaskSolutionChecker } from '@entities/taskSolutionChecker';\nimport { Repository } from 'typeorm';\nimport { Student } from '@entities/student';\nimport { TaskSolution } from '@entities/taskSolution';\nimport {\n  CrossCheckMessage,\n  CrossCheckMessageAuthorRole,\n  ScoreRecord,\n  TaskSolutionResult,\n} from '@entities/taskSolutionResult';\nimport { CourseTask } from '@entities/courseTask';\nimport { UsersService } from 'src/users/users.service';\nimport { PersonDto } from 'src/core/dto';\nimport { CrossCheckCriteriaDataDto, CrossCheckMessageDto } from './dto';\nimport { Discord } from 'src/profile/dto';\n\nexport type CrossCheckPair = {\n  id: number;\n  score: number;\n  comment: string;\n  student: Pick<User, 'githubId' | 'id' | 'firstName' | 'lastName'>;\n  checker: Pick<User, 'githubId' | 'id' | 'firstName' | 'lastName'>;\n  privateRepository?: string;\n  historicalScores: null | ScoreRecord[];\n  messages: null | CrossCheckMessage[];\n  url: string;\n  courseTask: Pick<Task, 'id' | 'name'>;\n  submittedDate: string;\n  reviewedDate: string;\n};\n\nexport type Pagination = {\n  pageSize: number;\n  current: number;\n  total: number;\n  totalPages: number;\n};\n\nexport type AvailableCrossCheckStats = {\n  name: string;\n  id: number;\n  checksCount: number;\n  completedChecksCount: number;\n};\n\nenum FilterField {\n  Checker = 'checker',\n  Student = 'student',\n  Url = 'url',\n  Task = 'task',\n}\n\nexport enum OrderField {\n  Checker = 'checker',\n  Student = 'student',\n  Url = 'url',\n  Task = 'task',\n  Score = 'score',\n  ReviewedDate = 'reviewedDate',\n  SubmittedDate = 'submittedDate',\n}\n\nexport enum OrderDirection {\n  Asc = 'ASC',\n  Desc = 'DESC',\n}\n\nconst orderFieldMapping: Record<OrderField, string> = {\n  checker: 'chu.githubId',\n  student: 'stu.githubId',\n  task: 't.name',\n  score: 'tsr.score',\n  url: 'ts.url',\n  submittedDate: 'ts.updatedDate',\n  reviewedDate: 'tsr.updatedDate',\n};\n\nexport type CrossCheckSolutionReview = {\n  id: number;\n  dateTime: number;\n  comment: string;\n  criteria?: CrossCheckCriteriaDataDto[];\n  author: {\n    id: number;\n    name: string;\n    githubId: string;\n    discord: Discord | null;\n  } | null;\n  score: number;\n  messages: CrossCheckMessageDto[];\n};\n\n@Injectable()\nexport class CourseCrossCheckService {\n  constructor(\n    @InjectRepository(TaskSolutionChecker)\n    private readonly taskSolutionCheckerRepository: Repository<TaskSolutionChecker>,\n    @InjectRepository(TaskSolution)\n    private readonly taskSolutionRepository: Repository<TaskSolution>,\n    @InjectRepository(TaskSolutionResult)\n    private readonly TaskSolutionResultRepository: Repository<TaskSolutionResult>,\n  ) {}\n\n  public async findPairs(\n    courseId: number,\n    pagination: { pageSize: number; current: number } = { pageSize: 100, current: 1 },\n    filter?: Partial<Record<FilterField, string>>,\n    order?: {\n      orderBy: OrderField;\n      orderDirection: OrderDirection;\n    },\n  ): Promise<{ items: CrossCheckPair[]; pagination: Pagination }> {\n    const query = this.taskSolutionCheckerRepository\n      .createQueryBuilder('tsc')\n      .leftJoin(CourseTask, 'ct', 'tsc.\"courseTaskId\" = ct.id')\n      .leftJoin(Task, 't', 't.id = ct.\"taskId\"')\n      .leftJoin(Student, 'st', 'tsc.\"studentId\" = st.id')\n      .leftJoin(User, 'stu', 'st.\"userId\" = stu.id')\n      .leftJoin(Student, 'ch', 'tsc.\"checkerId\" = ch.id')\n      .leftJoin(User, 'chu', 'ch.\"userId\" = chu.id')\n      .leftJoin(TaskSolution, 'ts', 'ts.\"courseTaskId\" = tsc.\"courseTaskId\" AND ts.\"studentId\" = st.\"id\"')\n      .leftJoin(\n        TaskSolutionResult,\n        'tsr',\n        'tsr.\"courseTaskId\" = tsc.\"courseTaskId\" AND tsr.\"studentId\" = tsc.\"studentId\" AND tsr.\"checkerId\" = tsc.\"checkerId\"',\n      )\n      .addSelect(['tsr.score', 'tsr.comment', 'tsr.updatedDate', 'tsr.historicalScores', 'tsr.messages'])\n      .addSelect(['st.repository'])\n      .addSelect(['stu.githubId', 'stu.id', 'stu.firstName', 'stu.lastName'])\n      .addSelect(['chu.githubId', 'chu.id', 'chu.firstName', 'chu.lastName'])\n      .addSelect(['ts.url', 'ts.updatedDate'])\n      .addSelect(['t.name', 't.id'])\n      .where('ct.\"courseId\" = :courseId', { courseId });\n\n    if (filter?.checker) {\n      query.andWhere('chu.\"githubId\" ILIKE :checker', { checker: `%${filter.checker}%` });\n    }\n    if (filter?.student) {\n      query.andWhere('stu.\"githubId\" ILIKE :student', { student: `%${filter.student}%` });\n    }\n    if (filter?.task) {\n      query.andWhere('t.name ILIKE :task', { task: `%${filter.task}%` });\n    }\n    if (filter?.url) {\n      query.andWhere('ts.url ILIKE :url', { url: `%${filter.url}%` });\n    }\n\n    if (order?.orderBy && orderFieldMapping[order.orderBy]) {\n      query.orderBy(orderFieldMapping[order.orderBy], order.orderDirection ?? 'ASC');\n    }\n\n    const [courseTasks, total] = await Promise.all([\n      query\n        .limit(pagination.pageSize)\n        .offset((pagination.current - 1) * pagination.pageSize)\n        .getRawMany(),\n      query.getCount(),\n    ]);\n\n    const result = courseTasks.map((e: any) => {\n      return {\n        checker: {\n          firstName: e.chu_firstName,\n          lastName: e.chu_lastName,\n          githubId: e.chu_githubId,\n          id: e.tsc_checkerId,\n        },\n        student: {\n          firstName: e.stu_firstName,\n          lastName: e.stu_lastName,\n          githubId: e.stu_githubId,\n          id: e.tsc_studentId,\n        },\n        courseTask: {\n          name: e.t_name,\n          id: e.tsc_courseTaskId,\n        },\n        url: e.ts_url,\n        privateRepository: e.st_repository,\n        score: e.tsr_score,\n        comment: e.tsr_comment,\n        submittedDate: e.ts_updatedDate,\n        reviewedDate: e.tsr_historicalScores?.at(-1)?.dateTime,\n        messages: e.tsr_messages,\n        historicalScores: e.tsr_historicalScores,\n        id: e.tsc_id,\n      } as CrossCheckPair;\n    });\n\n    return {\n      items: result,\n      pagination: {\n        current: Number(pagination.current),\n        pageSize: Number(pagination.pageSize),\n        total,\n        totalPages: Math.ceil(total / pagination.pageSize),\n      },\n    };\n  }\n\n  public async getSolutionsUrls(courseId: number, courseTaskId: number) {\n    const query = this.taskSolutionRepository\n      .createQueryBuilder('ts')\n      .leftJoin(CourseTask, 'ct', 'ts.\"courseTaskId\" = ct.id')\n      .leftJoin(Student, 'st', 'ts.\"studentId\" = st.id')\n      .leftJoin(User, 'stu', 'stu.\"id\" = st.\"userId\"')\n      .addSelect(['stu.\"githubId\"', 'ts.url'])\n      .where('ct.\"courseId\" = :courseId', { courseId })\n      .andWhere('ct.\"id\" = :courseTaskId', { courseTaskId });\n\n    const rawData = await query.getRawMany<{ githubId: string; url: string }>();\n\n    const result = rawData.map(data => ({\n      githubId: data.githubId,\n      solutionUrl: data.url,\n    }));\n\n    return result;\n  }\n\n  public async getAvailableCrossChecksStats(\n    tasks: CourseTask[],\n    studentId: number,\n  ): Promise<AvailableCrossCheckStats[]> {\n    const res = await this.taskSolutionCheckerRepository\n      .createQueryBuilder('tsc')\n      .leftJoin(\n        TaskSolutionResult,\n        'tsr',\n        'tsr.\"courseTaskId\" = tsc.\"courseTaskId\" AND tsr.\"studentId\" = tsc.\"studentId\" AND tsr.\"checkerId\" = tsc.\"checkerId\"',\n      )\n      .addSelect(['tsr.score'])\n      .where('tsc.courseTaskId IN (:...ids)', { ids: tasks.map(i => i.id) })\n      .andWhere('tsc.\"checkerId\" = :studentId', { studentId })\n      .getRawMany();\n\n    return tasks\n      .map(t => {\n        const checks = res.filter(el => t.id === el.tsc_courseTaskId);\n\n        return {\n          name: t.task.name,\n          id: t.id,\n          checksCount: checks.length,\n          completedChecksCount: checks.filter(c => c.tsr_score !== null).length,\n        };\n      })\n      .filter(el => el.checksCount !== 0);\n  }\n\n  public isCrossCheckTask(courseTask: Partial<CourseTask>) {\n    return courseTask.checker === 'crossCheck';\n  }\n\n  public async getCrossCheckSolutionReviews(\n    studentId: number,\n    courseTaskId: number,\n  ): Promise<CrossCheckSolutionReview[]> {\n    const taskSolutionResults = await this.TaskSolutionResultRepository.createQueryBuilder('tsr')\n      .select(['tsr.id', 'tsr.comment', 'tsr.anonymous', 'tsr.score', 'tsr.messages', 'tsr.historicalScores'])\n      .innerJoin('tsr.checker', 'checker')\n      .innerJoin('checker.user', 'user')\n      .addSelect(['checker.id', ...UsersService.getPrimaryUserFields('user')])\n      .where('\"tsr\".\"studentId\" = :studentId', { studentId })\n      .andWhere('\"tsr\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n      .getMany();\n\n    return taskSolutionResults.map(taskSolutionResult => this.transformToCrossCheckSolutionReview(taskSolutionResult));\n  }\n\n  private transformToCrossCheckSolutionReview(taskSolutionResult: TaskSolutionResult): CrossCheckSolutionReview {\n    const author = this.extractAuthor(taskSolutionResult);\n    const { dateTime, criteria } = this.getLastCheck(taskSolutionResult);\n    const messages = this.getMessages(taskSolutionResult);\n\n    return {\n      dateTime,\n      author,\n      messages,\n      id: taskSolutionResult.id,\n      comment: taskSolutionResult.comment ?? '',\n      score: taskSolutionResult.score,\n      criteria,\n    };\n  }\n\n  private extractAuthor(taskSolutionResult: TaskSolutionResult) {\n    if (taskSolutionResult.anonymous) {\n      return null;\n    }\n\n    return {\n      id: taskSolutionResult.checker.user.id,\n      name: PersonDto.getName(taskSolutionResult.checker.user),\n      githubId: taskSolutionResult.checker.user.githubId,\n      discord: taskSolutionResult.checker.user.discord,\n    };\n  }\n\n  private getLastCheck(taskSolutionResult: TaskSolutionResult) {\n    const [lastCheck] = taskSolutionResult.historicalScores.sort((a, b) => b.dateTime - a.dateTime);\n\n    if (!lastCheck) {\n      throw new BadRequestException('No historical scores found');\n    }\n\n    return lastCheck;\n  }\n\n  private getMessages(taskSolutionResult: TaskSolutionResult) {\n    if (taskSolutionResult.anonymous) {\n      return taskSolutionResult.messages.map(message => ({\n        ...message,\n        author: message.role === CrossCheckMessageAuthorRole.Reviewer ? null : message.author,\n      }));\n    }\n\n    return taskSolutionResult.messages;\n  }\n\n  public async getTaskSolution(studentId: number, courseTaskId: number) {\n    const taskSolution = await this.taskSolutionRepository\n      .createQueryBuilder('ts')\n      .where('\"ts\".\"studentId\" = :studentId', { studentId })\n      .andWhere('\"ts\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n      .getOne();\n\n    return taskSolution;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/cross-check-feedback.guard.ts",
    "content": "import { Injectable, CanActivate, ExecutionContext, BadRequestException, UnauthorizedException } from '@nestjs/common';\nimport { CourseCrossCheckService } from './course-cross-checks.service';\nimport { CourseRole } from '@entities/session';\nimport { CourseTasksService } from '../course-tasks';\n\n@Injectable()\nexport class FeedbackGuard implements CanActivate {\n  constructor(\n    private readonly courseCrossCheckService: CourseCrossCheckService,\n    private readonly courseTasksService: CourseTasksService,\n  ) {}\n\n  canActivate(context: ExecutionContext): boolean | Promise<boolean> {\n    const request = context.switchToHttp().getRequest();\n    const { courseId, courseTaskId } = request.params;\n    const studentId = request.user.courses[courseId]?.studentId;\n    const isManager = request.user.isAdmin || request.user.courses[courseId]?.roles.includes(CourseRole.Manager);\n\n    if (!studentId && !isManager) {\n      throw new UnauthorizedException('Not a valid student for this course');\n    }\n\n    return this.validateTask(courseTaskId);\n  }\n\n  async validateTask(courseTaskId: number): Promise<boolean> {\n    const courseTask = await this.courseTasksService.getById(courseTaskId);\n\n    if (courseTask == null) {\n      throw new BadRequestException('not valid student or course task');\n    }\n\n    if (!this.courseCrossCheckService.isCrossCheckTask(courseTask)) {\n      throw new BadRequestException('not supported task');\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/dto/available-review-stats.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { AvailableCrossCheckStats } from '../course-cross-checks.service';\n\nexport class AvailableReviewStatsDto {\n  constructor(stats: AvailableCrossCheckStats) {\n    this.name = stats.name;\n    this.id = stats.id;\n    this.checksCount = stats.checksCount ?? 0;\n    this.completedChecksCount = stats.completedChecksCount ?? 0;\n  }\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public checksCount: number;\n\n  @ApiProperty()\n  public completedChecksCount: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/dto/check-tasks-pairs.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IdNameDto, PaginationDto, PersonDto } from 'src/core/dto';\nimport { CrossCheckPair, Pagination } from '../course-cross-checks.service';\nimport {\n  CrossCheckMessage,\n  CrossCheckMessageAuthor,\n  CrossCheckMessageAuthorRole,\n  ScoreRecord,\n} from '@entities/taskSolutionResult';\nimport { CrossCheckCriteriaDataDto } from './cross-check-criteria-data.dto';\nimport { IsOptional } from 'class-validator';\n\nexport class HistoricalScoreDto {\n  constructor(historicalScore: ScoreRecord) {\n    this.comment = historicalScore.comment;\n    this.dateTime = new Date(historicalScore.dateTime);\n    this.criteria = historicalScore.criteria;\n  }\n\n  @ApiProperty()\n  public comment: string;\n\n  @ApiProperty()\n  public dateTime: Date;\n\n  @ApiProperty({ type: [CrossCheckCriteriaDataDto], required: false })\n  public criteria?: CrossCheckCriteriaDataDto[];\n}\n\nexport class CrossCheckMessageAuthorDto {\n  constructor(crossCheckMessageAuthor: CrossCheckMessageAuthor) {\n    this.githubId = crossCheckMessageAuthor.githubId;\n    this.id = crossCheckMessageAuthor.id;\n  }\n\n  @ApiProperty()\n  public githubId: string;\n\n  @ApiProperty()\n  public id: number;\n}\n\nexport class CrossCheckMessageDto {\n  constructor(crossCheckMessage: CrossCheckMessage) {\n    this.content = crossCheckMessage.content;\n    this.author = crossCheckMessage.author ? new CrossCheckMessageAuthorDto(crossCheckMessage.author) : null;\n    this.timestamp = crossCheckMessage.timestamp;\n    this.isReviewerRead = crossCheckMessage.isReviewerRead;\n    this.isStudentRead = crossCheckMessage.isStudentRead;\n    this.role = crossCheckMessage.role;\n  }\n\n  @ApiProperty({ type: CrossCheckMessageAuthorDto, nullable: true })\n  public author: CrossCheckMessageAuthorDto | null;\n\n  @ApiProperty()\n  public content: string;\n\n  @ApiProperty()\n  public timestamp: string;\n\n  @ApiProperty()\n  public isReviewerRead: boolean;\n\n  @ApiProperty()\n  public isStudentRead: boolean;\n\n  @ApiProperty({ enum: CrossCheckMessageAuthorRole })\n  public role: CrossCheckMessageAuthorRole;\n}\n\nexport class CrossCheckPairDto {\n  constructor(pair: CrossCheckPair) {\n    this.checker = new PersonDto(pair.checker);\n    this.comment = pair.comment;\n    this.task = new IdNameDto(pair.courseTask);\n    this.id = pair.id;\n    this.reviewedDate = pair.reviewedDate;\n    this.privateRepository = pair.privateRepository;\n    this.score = pair.score;\n    this.student = new PersonDto(pair.student);\n    this.submittedDate = pair.submittedDate;\n    this.url = pair.url;\n    this.historicalScores = pair.historicalScores?.map(historicalScore => new HistoricalScoreDto(historicalScore));\n    this.messages = pair.messages?.map(message => new CrossCheckMessageDto(message));\n  }\n\n  @ApiProperty({ type: PersonDto })\n  public student: PersonDto;\n\n  @ApiProperty({ type: PersonDto })\n  public checker: PersonDto;\n\n  @ApiProperty({ type: IdNameDto })\n  public task: IdNameDto;\n\n  @ApiProperty()\n  public score: number;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public comment: string;\n\n  @ApiProperty()\n  public url: string;\n\n  @ApiProperty()\n  public reviewedDate: string;\n\n  @ApiProperty()\n  @IsOptional()\n  public privateRepository?: string;\n\n  @ApiProperty()\n  public submittedDate: string;\n\n  @ApiProperty({ type: [HistoricalScoreDto] })\n  public historicalScores?: HistoricalScoreDto[];\n\n  @ApiProperty({ type: [CrossCheckMessageDto] })\n  public messages?: CrossCheckMessageDto[];\n}\n\nexport class CrossCheckPairResponseDto {\n  constructor(items: CrossCheckPair[], pagination: Pagination) {\n    this.items = items.map(item => new CrossCheckPairDto(item));\n    this.pagination = new PaginationDto(\n      pagination.pageSize,\n      pagination.current,\n      pagination.total,\n      pagination.totalPages,\n    );\n  }\n\n  @ApiProperty({ type: [CrossCheckPairDto] })\n  public items: CrossCheckPairDto[];\n\n  @ApiProperty()\n  public pagination: PaginationDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/dto/cross-check-criteria-data.dto.ts",
    "content": "import { CrossCheckCriteriaType } from '@entities/taskCriteria';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class CrossCheckCriteriaDataDto {\n  @ApiProperty()\n  @IsString()\n  key: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  max?: number;\n\n  @ApiProperty()\n  @IsString()\n  text: string;\n\n  @ApiProperty({ enum: CrossCheckCriteriaType })\n  @IsEnum(CrossCheckCriteriaType)\n  type: CrossCheckCriteriaType;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsNumber()\n  point?: number;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsString()\n  textComment?: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/dto/cross-check-feedback.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { CrossCheckMessageDto } from './check-tasks-pairs.dto';\nimport { Discord } from 'src/profile/dto';\nimport { CrossCheckSolutionReview } from '../course-cross-checks.service';\nimport { TaskSolution } from '@entities/taskSolution';\nimport { CrossCheckCriteriaDataDto } from './cross-check-criteria-data.dto';\n\nexport class CrossCheckAuthorDto {\n  @ApiProperty({ required: true })\n  public id: number;\n\n  @ApiProperty({ required: true })\n  public name: string;\n\n  @ApiProperty({ required: true })\n  public githubId: string;\n\n  @ApiProperty({ nullable: true, type: Discord })\n  discord: Discord | null;\n}\n\nexport class CrossCheckSolutionReviewDto {\n  @ApiProperty({ required: true })\n  public id: number;\n\n  @ApiProperty({ required: true })\n  public dateTime: number;\n\n  @ApiProperty({ required: true })\n  public comment: string;\n\n  @ApiProperty({ required: false, type: [CrossCheckCriteriaDataDto] })\n  public criteria?: CrossCheckCriteriaDataDto[];\n\n  @ApiProperty({ nullable: true, type: CrossCheckAuthorDto })\n  public author: CrossCheckAuthorDto | null;\n\n  @ApiProperty({ required: true })\n  public score: number;\n\n  @ApiProperty({ required: true, type: [CrossCheckMessageDto] })\n  public messages: CrossCheckMessageDto[];\n}\n\nexport class CrossCheckFeedbackDto {\n  constructor(crossCheckSolutionReviews: CrossCheckSolutionReview[], taskSolution: TaskSolution | null) {\n    this.reviews = crossCheckSolutionReviews;\n    this.url = taskSolution?.url;\n  }\n\n  @ApiProperty({ required: false })\n  public url?: string;\n\n  @ApiProperty({ required: false, type: [CrossCheckSolutionReviewDto] })\n  public reviews?: CrossCheckSolutionReviewDto[];\n}\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/dto/index.ts",
    "content": "export * from './check-tasks-pairs.dto';\nexport * from './cross-check-feedback.dto';\nexport * from './cross-check-criteria-data.dto';\n"
  },
  {
    "path": "nestjs/src/courses/cross-checks/index.ts",
    "content": "export * from './course-cross-checks.controller';\nexport * from './course-cross-checks.service';\n"
  },
  {
    "path": "nestjs/src/courses/dto/course.dto.ts",
    "content": "import { Course } from '@entities/course';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IdNameDto } from '../../core/dto';\n\nexport class CourseDto {\n  constructor(course: Course) {\n    this.id = course.id;\n    this.alias = course.alias;\n    this.name = course.name;\n    this.fullName = course.fullName;\n    this.descriptionUrl = course.descriptionUrl;\n    this.description = course.description;\n    this.startDate = course.startDate?.toISOString() ?? null;\n    this.endDate = course.endDate?.toISOString() ?? null;\n    this.completed = course.completed;\n    this.planned = course.planned;\n    this.certificateIssuer = course.certificateIssuer;\n    this.createdDate = course.createdDate.toISOString();\n    this.updatedDate = course.updatedDate.toISOString();\n    this.locationName = course.locationName;\n    this.discordServerId = course.discordServerId;\n    this.inviteOnly = course.inviteOnly;\n    this.usePrivateRepositories = course.usePrivateRepositories;\n    this.registrationEndDate = course.registrationEndDate?.toISOString() ?? null;\n    this.personalMentoring = course.personalMentoring;\n    this.personalMentoringStartDate = course.personalMentoringStartDate?.toISOString() ?? null;\n    this.personalMentoringEndDate = course.personalMentoringEndDate?.toISOString() ?? null;\n    this.logo = course.logo;\n    this.discipline = course.discipline ? { id: course.discipline.id, name: course.discipline.name } : null;\n    this.minStudentsPerMentor = course.minStudentsPerMentor;\n    this.certificateThreshold = course.certificateThreshold;\n    this.wearecommunityUrl = course.wearecommunityUrl;\n    this.certificateDisciplines = course.certificateDisciplines?.map(id => Number(id)) ?? null;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  createdDate: string;\n\n  @ApiProperty()\n  updatedDate: string;\n\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  fullName: string;\n\n  @ApiProperty()\n  alias: string;\n\n  @ApiProperty()\n  description: string;\n\n  @ApiProperty()\n  descriptionUrl: string;\n\n  @ApiProperty()\n  year: number;\n\n  @ApiProperty()\n  startDate: string;\n\n  @ApiProperty()\n  endDate: string;\n\n  @ApiProperty({ type: 'string', nullable: true })\n  registrationEndDate: string | null;\n\n  @ApiProperty()\n  primarySkillId: string;\n\n  @ApiProperty()\n  primarySkillName: string;\n\n  @ApiProperty()\n  locationName: string;\n\n  @ApiProperty()\n  discordServerId: number;\n\n  @ApiProperty()\n  completed: boolean;\n\n  @ApiProperty()\n  planned: boolean;\n\n  @ApiProperty()\n  inviteOnly: boolean;\n\n  @ApiProperty()\n  certificateIssuer: string;\n\n  @ApiProperty()\n  usePrivateRepositories: boolean;\n\n  @ApiProperty()\n  personalMentoring: boolean;\n\n  @ApiProperty({ type: 'string', nullable: true })\n  personalMentoringStartDate: string | null;\n\n  @ApiProperty({ type: 'string', nullable: true })\n  personalMentoringEndDate: string | null;\n\n  @ApiProperty()\n  logo: string;\n\n  @ApiProperty({ nullable: true, type: IdNameDto })\n  discipline: IdNameDto | null;\n\n  @ApiProperty()\n  minStudentsPerMentor: number;\n\n  @ApiProperty()\n  certificateThreshold: number;\n\n  @ApiProperty({ nullable: true, type: String })\n  wearecommunityUrl: string | null;\n\n  @ApiProperty({ nullable: true, type: [Number] })\n  certificateDisciplines: number[] | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/dto/create-course.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsArray, IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class CreateCourseDto {\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @IsString()\n  @ApiProperty()\n  startDate: string;\n\n  @IsString()\n  @ApiProperty()\n  endDate: string;\n\n  @IsString()\n  @ApiProperty()\n  fullName: string;\n\n  @IsString()\n  @ApiProperty()\n  alias: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  registrationEndDate?: string;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  completed?: boolean;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  planned?: boolean;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  inviteOnly?: boolean;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  description?: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  descriptionUrl?: string;\n\n  @IsNumber()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  disciplineId?: number;\n\n  @IsNumber()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  discordServerId?: number;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  usePrivateRepositories?: boolean;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  certificateIssuer?: string;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  personalMentoring?: boolean;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  personalMentoringStartDate?: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  personalMentoringEndDate?: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  logo?: string;\n\n  @IsNumber()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  minStudentsPerMentor?: number;\n\n  @IsNumber()\n  @ApiProperty({ required: true })\n  certificateThreshold: number;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty()\n  wearecommunityUrl?: string;\n\n  @IsArray()\n  @IsOptional()\n  @ApiProperty({ nullable: true, type: [String] })\n  certificateDisciplines?: string[] | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/dto/export-course.dto.ts",
    "content": "import { Course } from '@entities/course';\n\nexport class ExportCourseDto {\n  constructor(course: Course) {\n    this.id = course.id;\n    this.name = course.name;\n    this.fullName = course.fullName;\n    this.startDate = course.startDate.toISOString();\n    this.endDate = course.endDate.toISOString();\n    this.alias = course.alias;\n    this.discipline = course.discipline ? { id: course.discipline.id, name: course.discipline.name } : null;\n    this.description = course.description;\n    this.descriptionUrl = course.descriptionUrl;\n    this.registrationEndDate = course.registrationEndDate?.toISOString() ?? null;\n    this.personalMentoringStartDate = course.personalMentoringStartDate?.toISOString() ?? null;\n    this.personalMentoringEndDate = course.personalMentoringEndDate?.toISOString() ?? null;\n    this.wearecommunityUrl = course.wearecommunityUrl || null;\n  }\n\n  id: number;\n  name: string;\n  fullName: string;\n  alias: string;\n  description: string;\n  descriptionUrl: string;\n  discipline: { id: number; name: string } | null;\n  registrationEndDate: string | null;\n  startDate: string;\n  endDate: string;\n  personalMentoringStartDate: string | null;\n  personalMentoringEndDate: string | null;\n  wearecommunityUrl: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/dto/index.ts",
    "content": "export * from './course.dto';\nexport * from './leave-course.dto';\nexport * from './update-course.dto';\nexport * from './create-course.dto';\nexport * from './export-course.dto';\n"
  },
  {
    "path": "nestjs/src/courses/dto/leave-course.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsOptional, IsString } from 'class-validator';\n\nexport class LeaveCourseRequestDto {\n  @ApiProperty({ required: false, type: [String] })\n  @IsArray()\n  @IsString({ each: true })\n  @IsOptional()\n  reasonForLeaving?: string[];\n\n  @ApiProperty({ required: false })\n  @IsString()\n  @IsOptional()\n  otherComment?: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/dto/update-course.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsArray, IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class UpdateCourseDto {\n  @IsString()\n  @ApiPropertyOptional()\n  name?: string;\n\n  @IsString()\n  @ApiPropertyOptional()\n  fullName?: string;\n\n  @IsString()\n  @ApiPropertyOptional()\n  alias?: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  description?: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  descriptionUrl?: string;\n\n  @ApiPropertyOptional()\n  year?: number;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  startDate?: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  endDate?: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional({ type: 'string', nullable: true })\n  registrationEndDate?: string | null;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  locationName?: string;\n\n  @IsNumber()\n  @IsOptional()\n  @ApiPropertyOptional()\n  discordServerId?: number;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiPropertyOptional()\n  completed?: boolean;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiPropertyOptional()\n  planned?: boolean;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiPropertyOptional()\n  inviteOnly?: boolean;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  certificateIssuer?: string;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiPropertyOptional()\n  usePrivateRepositories?: boolean;\n\n  @IsBoolean()\n  @IsOptional()\n  @ApiPropertyOptional()\n  personalMentoring?: boolean;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional({ type: 'string', nullable: true })\n  personalMentoringStartDate?: string | null;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional({ type: 'string', nullable: true })\n  personalMentoringEndDate?: string | null;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional()\n  logo?: string;\n\n  @IsNumber()\n  @IsOptional()\n  @ApiPropertyOptional()\n  disciplineId?: number;\n\n  @IsNumber()\n  @IsOptional()\n  @ApiPropertyOptional()\n  minStudentsPerMentor?: number;\n\n  @IsNumber()\n  @ApiProperty({ required: true })\n  certificateThreshold: number;\n\n  @IsString()\n  @IsOptional()\n  @ApiPropertyOptional({ nullable: true, type: 'string' })\n  wearecommunityUrl?: string | null;\n\n  @IsArray()\n  @IsOptional()\n  @ApiPropertyOptional({ nullable: true, type: [String] })\n  certificateDisciplines?: string[] | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/dto/used-course.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'class-validator';\n\nexport class UsedCourseDto {\n  constructor(course: { name: string; isActive: boolean }) {\n    this.isActive = course.isActive;\n    this.name = course.name;\n  }\n\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  isActive: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/courses/expelled-stats.service.test.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { NotFoundException } from '@nestjs/common';\nimport { Repository } from 'typeorm';\nimport { CourseLeaveSurveyResponse } from '@entities/index';\nimport { ExpelledStatsService } from './expelled-stats.service';\n\nconst mockSurvey = { id: '1', courseId: 1 } as CourseLeaveSurveyResponse;\n\nconst mockFindResponse = [mockSurvey];\nconst mockDeleteResponse = { affected: 1 };\nconst mockCreateResponse = mockSurvey;\nconst mockSaveResponse = mockSurvey;\n\nconst mockFind = vi.fn(() => Promise.resolve(mockFindResponse));\nconst mockDelete = vi.fn(() => Promise.resolve(mockDeleteResponse));\nconst mockCreate = vi.fn(() => mockCreateResponse);\nconst mockSave = vi.fn(() => Promise.resolve(mockSaveResponse));\n\nconst mockSurveyRepositoryFactory = () => ({\n  find: mockFind,\n  delete: mockDelete,\n  create: mockCreate,\n  save: mockSave,\n});\n\ndescribe('ExpelledStatsService', () => {\n  let service: ExpelledStatsService;\n  let repository: Repository<CourseLeaveSurveyResponse>;\n\n  beforeEach(async () => {\n    vi.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        ExpelledStatsService,\n        {\n          provide: getRepositoryToken(CourseLeaveSurveyResponse),\n          useFactory: mockSurveyRepositoryFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<ExpelledStatsService>(ExpelledStatsService);\n    repository = module.get<Repository<CourseLeaveSurveyResponse>>(getRepositoryToken(CourseLeaveSurveyResponse));\n  });\n\n  describe('findAll', () => {\n    it('should return all expelled stats', async () => {\n      const result = await service.findAll();\n\n      expect(result).toEqual(mockFindResponse);\n      expect(repository.find).toHaveBeenCalledWith({\n        relations: ['user', 'course'],\n      });\n    });\n  });\n\n  describe('findByCourseId', () => {\n    it('should return expelled stats for a course', async () => {\n      const courseId = 7;\n      const result = await service.findByCourseId(courseId);\n\n      expect(result).toEqual(mockFindResponse);\n      expect(repository.find).toHaveBeenCalledWith({\n        where: { courseId },\n        relations: ['user', 'course'],\n      });\n    });\n  });\n\n  describe('remove', () => {\n    it('should delete expelled stat by id', async () => {\n      await service.remove('1');\n\n      expect(repository.delete).toHaveBeenCalledWith('1');\n    });\n\n    it('should throw when stat is not found', async () => {\n      mockDelete.mockResolvedValueOnce({ affected: 0 });\n\n      await expect(service.remove('missing')).rejects.toThrow(NotFoundException);\n    });\n  });\n\n  describe('submitLeaveSurvey', () => {\n    it('should create and save survey response', async () => {\n      const result = await service.submitLeaveSurvey(10, 20, ['reason'], 'note');\n\n      expect(result).toEqual(mockSaveResponse);\n      expect(repository.create).toHaveBeenCalledWith({\n        userId: 10,\n        courseId: 20,\n        reasonForLeaving: ['reason'],\n        otherComment: 'note',\n        submittedAt: expect.any(Date),\n      });\n      expect(repository.save).toHaveBeenCalledWith(mockCreateResponse);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/expelled-stats.service.ts",
    "content": "import { Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CourseLeaveSurveyResponse } from '@entities/index';\n\n@Injectable()\nexport class ExpelledStatsService {\n  constructor(\n    @InjectRepository(CourseLeaveSurveyResponse)\n    private readonly surveyRepository: Repository<CourseLeaveSurveyResponse>,\n  ) {}\n\n  async findAll(): Promise<CourseLeaveSurveyResponse[]> {\n    return this.surveyRepository.find({\n      relations: ['user', 'course'],\n    });\n  }\n\n  async findByCourseId(courseId: number): Promise<CourseLeaveSurveyResponse[]> {\n    return this.surveyRepository.find({\n      where: { courseId },\n      relations: ['user', 'course'],\n    });\n  }\n\n  async remove(id: string): Promise<void> {\n    const result = await this.surveyRepository.delete(id);\n    if (result.affected === 0) {\n      throw new NotFoundException(`Expelled stat with ID \"${id}\" not found`);\n    }\n  }\n\n  async submitLeaveSurvey(\n    userId: number,\n    courseId: number,\n    reasonForLeaving?: string[],\n    otherComment?: string,\n  ): Promise<CourseLeaveSurveyResponse> {\n    const surveyResponse = this.surveyRepository.create({\n      userId,\n      courseId,\n      reasonForLeaving,\n      otherComment,\n      submittedAt: new Date(),\n    });\n    return this.surveyRepository.save(surveyResponse);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/index.ts",
    "content": "export * from './course-tasks/course-tasks.service';\nexport * from './courses.controller';\n"
  },
  {
    "path": "nestjs/src/courses/interviews/cross-mentor-distribution.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { max } from 'lodash';\nimport { shuffleRec } from '../../utils';\n\nexport type CrossMentor = { id: number; students: { id: number }[] | null };\n\n@Injectable()\nexport class CrossMentorDistributionService {\n  public distribute(\n    mentors: CrossMentor[],\n    existingPairs: { studentId: number; mentorId: number }[],\n    registeredStudentsIds?: number[],\n    defaultMaxStudents = 1,\n  ) {\n    const initialMentorStudentsMap = mentors.reduce<Record<number, number[]>>((acc, m) => {\n      acc[m.id] = (m.students ?? []).map(s => s.id);\n      return acc;\n    }, {});\n\n    let students = mentors\n      .map(m => m.students ?? [])\n      .reduce((acc, v) => acc.concat(v), [] as { id: number }[])\n      .filter(v => !existingPairs.find(p => p.studentId === v.id))\n      .filter(v => registeredStudentsIds?.includes(v.id) ?? true);\n\n    const maxStudentsPerMentor = mentors.map(({ id, students }) => {\n      const assignedCount = existingPairs.filter(p => p.mentorId === id).length;\n      const maxStudentsCount = Math.max((students?.length ?? 0) - assignedCount, 0);\n      return { id, maxStudents: maxStudentsCount };\n    });\n\n    const maxStudentsTotal = maxStudentsPerMentor.reduce((acc, m) => acc + m.maxStudents, 0);\n\n    if (students.length < maxStudentsTotal && registeredStudentsIds) {\n      students = students.concat(\n        registeredStudentsIds\n          .filter(id => !existingPairs.find(p => p.studentId === id) && !students.find(st => st.id === id))\n          .slice(0, maxStudentsTotal - students.length)\n          .map(id => ({ id })),\n      );\n    }\n\n    const randomStudents = shuffleRec(students);\n\n    // distribute students to mentors by round-robin\n    const maxStudentsMap = maxStudentsPerMentor.reduce(\n      (acc, m) => {\n        acc[m.id] = m.maxStudents;\n        return acc;\n      },\n      {} as Record<number, number>,\n    );\n\n    if (registeredStudentsIds && randomStudents.length < maxStudentsTotal) {\n      const filteredMentors = mentors.filter(m => (maxStudentsMap[m.id] ?? defaultMaxStudents) > 0);\n      const maxStudentsPerMentorValue = max(filteredMentors.map(m => maxStudentsMap[m.id] ?? 0)) ?? 0;\n      const mentorsQueue: number[] = [];\n      for (let i = 0; i < maxStudentsPerMentorValue; i++) {\n        filteredMentors.forEach((mentor, idx) => {\n          if ((maxStudentsMap[mentor.id] ?? 0) > i) {\n            mentorsQueue.push(idx);\n          }\n        });\n      }\n      mentorsQueue.reverse();\n\n      // nullify students for mentors\n      for (const mentor of mentors) {\n        mentor.students = [];\n      }\n\n      const unassignedStudents: { id: number }[] = [];\n\n      randomStudents.forEach(student => {\n        let mentorIdx = mentorsQueue.pop();\n        let assigned = false;\n\n        while (mentorIdx != null) {\n          const mentor = filteredMentors[mentorIdx];\n          if (!mentor) {\n            mentorIdx = mentorsQueue.pop();\n            continue;\n          }\n          const wasAssignedToThisMentor = initialMentorStudentsMap[mentor.id]?.includes(student.id);\n          if (!wasAssignedToThisMentor) {\n            mentor.students = mentor.students ? mentor.students.concat([student]) : [student];\n            assigned = true;\n            break;\n          }\n          const nextMentorIdx = mentorsQueue.pop();\n          mentorsQueue.unshift(mentorIdx);\n          mentorIdx = nextMentorIdx;\n        }\n\n        if (!assigned) {\n          unassignedStudents.push(student);\n        }\n      });\n\n      return {\n        mentors,\n        unassignedStudents,\n      };\n    }\n\n    for (const mentor of mentors) {\n      const maxStudents = maxStudentsMap[mentor.id] ?? defaultMaxStudents;\n      const mentorOriginalStudents = initialMentorStudentsMap[mentor.id] ?? [];\n      const mentorStudents: { id: number }[] = [];\n      for (let i = 0; i < randomStudents.length && mentorStudents.length < maxStudents; i++) {\n        const student = randomStudents[i];\n        if (student && !mentorOriginalStudents.includes(student.id)) {\n          const spliced = randomStudents.splice(i, 1)[0];\n          if (spliced) {\n            mentorStudents.push(spliced);\n          }\n          i--;\n        }\n      }\n      mentor.students = mentorStudents;\n    }\n\n    return {\n      mentors,\n      unassignedStudents: randomStudents,\n    };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/available-student.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class AvailableStudentDto {\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  id: number;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  githubId: string;\n\n  @IsString()\n  @ApiProperty({ nullable: true, type: String })\n  cityName: string | null;\n\n  @IsString()\n  @ApiProperty({ nullable: true, type: String })\n  countryName: string | null;\n\n  @IsBoolean()\n  @ApiProperty()\n  isGoodCandidate?: boolean;\n\n  @IsNumber()\n  @ApiProperty({ nullable: true, type: String })\n  rating?: number | null;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  totalScore: number;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  registeredDate: string;\n\n  @IsOptional()\n  @ApiProperty()\n  maxScore?: number;\n\n  @IsOptional()\n  @ApiProperty()\n  feedbackVersion?: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/get-interview-feedback.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber } from 'class-validator';\n\nexport class InterviewFeedbackDto {\n  constructor(data: {\n    feedback: {\n      json: Record<string, unknown>;\n      version: number;\n    } | null;\n    isCompleted: boolean;\n    maxScore: number;\n  }) {\n    this.version = data.feedback?.version;\n    this.json = data.feedback?.json;\n    this.maxScore = data.maxScore;\n    this.isCompleted = data.isCompleted ?? false;\n  }\n\n  @IsNumber()\n  @ApiProperty({ required: false })\n  version?: number;\n\n  @ApiProperty({ required: false })\n  @IsNotEmpty()\n  json?: Record<string, unknown>;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  isCompleted: boolean;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  maxScore: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/index.ts",
    "content": "export * from './interview.dto';\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/interview-comment.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nexport class InterviewCommentDto {\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  id: number;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty({ nullable: true, type: String })\n  commentToStudent: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/interview-distribute.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsOptional } from 'class-validator';\n\nexport class InterviewDistributeResponseDto {\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  courseTaskId: number;\n\n  @ApiProperty()\n  mentorId: number;\n\n  @ApiProperty()\n  studentId: number;\n\n  @ApiProperty()\n  createdDate: string;\n\n  @ApiProperty()\n  updatedDate: string;\n}\n\nexport class InterviewDistributeDto {\n  @ApiProperty({ default: false })\n  @IsOptional()\n  @IsBoolean()\n  clean: boolean = false;\n\n  @ApiProperty({ default: true })\n  @IsOptional()\n  @IsBoolean()\n  registrationEnabled: boolean = true;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/interview-pair.dto.ts",
    "content": "import { InterviewStatus } from '@common/models';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum, IsNotEmpty, IsNumber } from 'class-validator';\nimport { PersonDto } from 'src/core/dto';\n\nexport class InterviewPairDto {\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  id: number;\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty({ nullable: true, type: Number })\n  result: number | null;\n\n  @IsNotEmpty()\n  @IsEnum(InterviewStatus)\n  @ApiProperty({\n    enum: [InterviewStatus.Completed, InterviewStatus.NotCompleted],\n    enumName: 'InterviewStatus',\n  })\n  status: InterviewStatus.Completed | InterviewStatus.NotCompleted;\n\n  @IsNotEmpty()\n  @ApiProperty()\n  interviewer: PersonDto;\n\n  @IsNotEmpty()\n  @ApiProperty()\n  student: PersonDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/interview.dto.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { TaskType } from '@entities/task';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nclass Attributes {\n  @ApiProperty({ nullable: true, required: false })\n  public template?: string;\n}\n\n@ApiResponse({})\nexport class InterviewDto {\n  constructor(courseTask: CourseTask) {\n    this.id = courseTask.id;\n    this.type = courseTask.type;\n    this.name = courseTask.task.name;\n    this.startDate = courseTask.studentStartDate as string;\n    this.endDate = courseTask.studentEndDate as string;\n    this.description = courseTask.task.description;\n    this.descriptionUrl = courseTask.task.descriptionUrl;\n    this.attributes = courseTask.task?.attributes;\n    this.studentRegistrationStartDate = courseTask.studentRegistrationStartDate\n      ? courseTask.studentRegistrationStartDate.toString()\n      : null;\n  }\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  id: number;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  type: TaskType;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  startDate: string;\n\n  @ApiProperty()\n  endDate: string;\n\n  @IsString()\n  @ApiProperty({ nullable: true })\n  description?: string;\n\n  @IsString()\n  @ApiProperty()\n  descriptionUrl: string;\n\n  @ApiProperty({ type: Attributes })\n  attributes: Attributes;\n\n  @ApiProperty({ type: Date, description: 'Date when student can register for the interview' })\n  studentRegistrationStartDate: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/put-interview-feedback.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class PutInterviewFeedbackDto {\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  version: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  json: Record<string, unknown>;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsString()\n  decision?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsBoolean()\n  isGoodCandidate?: boolean;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  isCompleted: boolean;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  score: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/dto/registration-interview.dto.ts",
    "content": "import { StageInterviewStudent } from '@entities/stageInterviewStudent';\nimport { TaskInterviewStudent } from '@entities/taskInterviewStudent';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class RegistrationInterviewDto {\n  constructor(taskInterviewStudent: TaskInterviewStudent | StageInterviewStudent) {\n    this.id = taskInterviewStudent.id;\n    this.registrationDate = taskInterviewStudent.createdDate;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  registrationDate: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/index.ts",
    "content": "export * from './interviews.service';\nexport * from './interviews.controller';\nexport * from './interviewFeedback.service';\n"
  },
  {
    "path": "nestjs/src/courses/interviews/interviewFeedback.service.ts",
    "content": "import { Repository } from 'typeorm';\nimport { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { StageInterview, StageInterviewFeedback } from '@entities/index';\nimport { StudentsService } from '../students';\nimport { PutInterviewFeedbackDto } from './dto/put-interview-feedback.dto';\nimport { InterviewCommentDto } from './dto/interview-comment.dto';\n\ninterface InterviewDecisionCommentFeedback {\n  steps: {\n    decision: {\n      values?: {\n        comment?: string;\n      };\n    };\n  };\n}\n\n@Injectable()\nexport class InterviewFeedbackService {\n  constructor(\n    @InjectRepository(StageInterview)\n    readonly stageInterviewRepository: Repository<StageInterview>,\n    @InjectRepository(StageInterviewFeedback)\n    readonly stageInterviewFeedbackRepository: Repository<StageInterviewFeedback>,\n    readonly studentsService: StudentsService,\n  ) {}\n\n  public async getCourseStageInterviewsComment(courseId: number, studentId: number) {\n    const records = await this.stageInterviewFeedbackRepository\n      .createQueryBuilder('sif')\n      .leftJoinAndSelect('sif.stageInterview', 'si')\n      .where('si.courseId = :courseId', { courseId })\n      .andWhere('si.studentId = :studentId', { studentId })\n      .select(['si.id', 'sif.json'])\n      .getRawMany();\n\n    const iterviewComments = records.map<InterviewCommentDto>(interviewFeedback => {\n      const feedback = JSON.parse(interviewFeedback.sif_json) as InterviewDecisionCommentFeedback;\n      const commentToStudent = feedback.steps.decision.values?.comment ?? null;\n\n      return {\n        id: Number(interviewFeedback.si_id),\n        commentToStudent,\n      };\n    });\n\n    return iterviewComments;\n  }\n\n  public async getStageInterviewFeedback(interviewId: number, interviewerGithubId: string) {\n    const interview = await this.stageInterviewRepository.findOne({\n      where: {\n        id: interviewId,\n        mentor: {\n          user: {\n            githubId: interviewerGithubId,\n          },\n        },\n      },\n      relations: ['stageInterviewFeedbacks', 'mentor', 'mentor.user', 'courseTask'],\n    });\n\n    if (!interview) {\n      throw new NotFoundException(`Interview not found ${interviewId}`);\n    }\n\n    const { courseTask, stageInterviewFeedbacks } = interview;\n    const feedback = stageInterviewFeedbacks.pop();\n\n    return {\n      feedback: feedback ? this.parseFeedback(feedback) : null,\n      isCompleted: interview.isCompleted,\n      maxScore: courseTask.maxScore,\n    };\n  }\n\n  public async upsertInterviewFeedback({\n    interviewId,\n    dto,\n    interviewerId,\n  }: {\n    interviewId: number;\n    dto: PutInterviewFeedbackDto;\n    interviewerId: number;\n  }) {\n    const interview = await this.stageInterviewRepository.findOneBy({ id: interviewId, mentorId: interviewerId });\n\n    if (!interview) {\n      throw new ForbiddenException();\n    }\n\n    const { studentId } = interview;\n    const { decision, isGoodCandidate, isCompleted, score } = dto;\n\n    await Promise.all([\n      this.saveFeedback(interview.id, dto),\n      this.stageInterviewRepository.update(interviewId, {\n        isCompleted,\n        decision,\n        isGoodCandidate,\n        score,\n      }),\n      decision === 'yes' ? this.studentsService.setMentor(studentId, interviewerId) : Promise.resolve(),\n    ]);\n  }\n\n  private async saveFeedback(stageInterviewId: number, data: PutInterviewFeedbackDto) {\n    const feedback = await this.stageInterviewFeedbackRepository.findOne({ where: { stageInterviewId } });\n\n    const newFeedback = { stageInterviewId, json: JSON.stringify(data.json), version: data.version };\n\n    if (feedback) {\n      await this.stageInterviewFeedbackRepository.update(feedback.id, newFeedback);\n    } else {\n      await this.stageInterviewFeedbackRepository.insert(newFeedback);\n    }\n  }\n\n  /**\n   * `json` stores the feedback in the form, which depends on the version.\n   */\n  private parseFeedback(feedback: StageInterviewFeedback) {\n    const { json, version } = feedback;\n\n    return {\n      json: json ? (JSON.parse(json) as Record<string, unknown>) : {},\n      version: version ?? 0,\n    };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/interviews.controller.ts",
    "content": "import {\n  BadRequestException,\n  Body,\n  ConflictException,\n  Controller,\n  ForbiddenException,\n  Get,\n  NotFoundException,\n  Param,\n  ParseArrayPipe,\n  ParseIntPipe,\n  Post,\n  Query,\n  Req,\n  UseGuards,\n  UseInterceptors,\n} from '@nestjs/common';\nimport { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';\nimport {\n  ApiBadRequestResponse,\n  ApiConflictResponse,\n  ApiForbiddenResponse,\n  ApiOkResponse,\n  ApiOperation,\n  ApiParam,\n  ApiQuery,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { DEFAULT_CACHE_TTL } from '../../constants';\nimport { InterviewDto } from './dto';\nimport { AvailableStudentDto } from './dto/available-student.dto';\nimport { InterviewsService } from './interviews.service';\nimport { TaskType } from '@entities/task';\nimport { InterviewFeedbackService } from './interviewFeedback.service';\nimport { InterviewFeedbackDto } from './dto/get-interview-feedback.dto';\nimport { PutInterviewFeedbackDto } from './dto/put-interview-feedback.dto';\nimport { RegistrationInterviewDto } from './dto/registration-interview.dto';\nimport { InterviewPairDto } from './dto/interview-pair.dto';\nimport { InterviewCommentDto } from './dto/interview-comment.dto';\nimport { CourseTasksService } from '../course-tasks';\nimport { InterviewDistributeDto, InterviewDistributeResponseDto } from './dto/interview-distribute.dto';\n\n@Controller('courses/:courseId/interviews')\n@ApiTags('courses interviews')\n@UseGuards(DefaultGuard, CourseGuard, RoleGuard)\nexport class InterviewsController {\n  constructor(\n    private interviewsService: InterviewsService,\n    private interviewFeedbackService: InterviewFeedbackService,\n    private courseTasksService: CourseTasksService,\n  ) {}\n\n  @Get()\n  @CacheTTL(DEFAULT_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @ApiOkResponse({ type: [InterviewDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiQuery({ name: 'disabled', required: false })\n  @ApiQuery({ name: 'types', required: false })\n  @ApiOperation({ operationId: 'getInterviews' })\n  public async getInterviews(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Query('disabled') disabled?: boolean,\n    @Query('types', new ParseArrayPipe({ optional: true })) types?: string[],\n  ) {\n    const data = await this.interviewsService.getAll(courseId, {\n      disabled,\n      types: types as TaskType[],\n    });\n    return data.map(item => new InterviewDto(item));\n  }\n\n  @Get('/comments')\n  @ApiOkResponse({ type: [InterviewCommentDto] })\n  @ApiForbiddenResponse()\n  @ApiOperation({ operationId: 'getStageInterviewsCommentToStudent' })\n  @RequiredRoles([CourseRole.Student, Role.Admin])\n  public async getStageInterviewsCommentToStudent(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Req() req: CurrentRequest,\n  ) {\n    const { user } = req;\n    const studentId = user.courses[courseId]?.studentId;\n\n    if (!studentId) {\n      if (!user.isAdmin) {\n        throw new ForbiddenException(`You are not a student of course ${courseId}`);\n      }\n\n      return [];\n    }\n\n    const commentsToStudent = await this.interviewFeedbackService.getCourseStageInterviewsComment(courseId, studentId);\n    return commentsToStudent;\n  }\n\n  @Get('/:interviewId')\n  @CacheTTL(DEFAULT_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @ApiOkResponse({ type: InterviewDto })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiParam({ name: 'courseId', type: Number })\n  @ApiOperation({ operationId: 'getInterview' })\n  public async getInterview(@Param('interviewId', ParseIntPipe) interviewId: number) {\n    const data = await this.interviewsService.getById(interviewId);\n    if (!data) {\n      throw new NotFoundException(`Interview ${interviewId} doesn't exist`);\n    }\n    return new InterviewDto(data);\n  }\n\n  @Get('/:interviewId/pairs')\n  @CacheTTL(DEFAULT_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @ApiOkResponse({ type: [InterviewPairDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiParam({ name: 'courseId', type: Number })\n  @ApiOperation({ operationId: 'getInterviewPairs' })\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  public async getInterviewPairs(@Param('interviewId', ParseIntPipe) interviewId: number) {\n    const data = await this.interviewsService.getInterviewPairs(interviewId);\n    return data;\n  }\n\n  @Post('/:interviewId/register')\n  @ApiOkResponse()\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'registerToInterview' })\n  @RequiredRoles([CourseRole.Student], true)\n  public async registerToInterview(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('interviewId', ParseIntPipe) interviewId: number,\n    @Req() req: CurrentRequest,\n  ) {\n    const { user } = req;\n    const interview = await this.interviewsService.getById(interviewId);\n\n    if (!interview) {\n      throw new NotFoundException(`Interview ${interviewId} doesn't exist`);\n    }\n    if (interview.studentRegistrationStartDate && new Date() < interview.studentRegistrationStartDate) {\n      throw new BadRequestException('Student registration is not available yet');\n    }\n    const taskInterviewStudent =\n      interview.type === TaskType.StageInterview\n        ? await this.interviewsService.registerStudentToStageInterview(courseId, user.githubId)\n        : await this.interviewsService.registerStudentToInterview(courseId, interviewId, user.githubId);\n\n    return new RegistrationInterviewDto(taskInterviewStudent);\n  }\n\n  @Post('/:courseTaskId/auto-distribute')\n  @ApiOkResponse({ type: [InterviewDistributeResponseDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiConflictResponse()\n  @ApiOperation({ operationId: 'distributeInterviewPairs' })\n  @RequiredRoles([CourseRole.Manager, Role.Admin])\n  public async distribute(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n    @Body() dto: InterviewDistributeDto,\n  ) {\n    const courseTask = await this.courseTasksService.getById(courseTaskId);\n\n    if (!courseTask) {\n      throw new BadRequestException('Not valid course task');\n    }\n\n    if (courseTask.isCreatingInterviewPairs) {\n      throw new ConflictException('Course task is already being processed');\n    }\n\n    try {\n      await this.courseTasksService.changeCourseTaskProcessing(courseTaskId, true);\n\n      const result = await this.interviewsService.distributeInterviewPairs(courseId, courseTaskId, {\n        clean: dto.clean,\n        registrationEnabled: dto.registrationEnabled,\n      });\n\n      if (result === null || result.length === 0) {\n        throw new BadRequestException('No interview pairs were created');\n      }\n\n      return result;\n    } finally {\n      await this.courseTasksService.changeCourseTaskProcessing(courseTaskId, false);\n    }\n  }\n\n  @Get('/:interviewId/students/available')\n  @ApiOkResponse({ type: [AvailableStudentDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'getAvailableStudents' })\n  @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager, Role.Admin], true)\n  public async getAvailableStudents(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('interviewId', ParseIntPipe) interviewId: number,\n  ) {\n    const interview = await this.interviewsService.getById(interviewId);\n\n    if (!interview) {\n      throw new NotFoundException(`Interview ${interviewId} doesn't exist`);\n    }\n    if (interview.type === 'stage-interview') {\n      return this.interviewsService.getStageInterviewAvailableStudents(courseId);\n    }\n\n    if (interview.type === 'interview') {\n      return this.interviewsService.getInterviewRegisteredStudents(courseId, +interviewId);\n    }\n\n    throw new BadRequestException('Invalid interview id');\n  }\n\n  // use `type` as a way to differentiate between stage-interview and interview.\n  @Get('/:interviewId/:type/feedback')\n  @ApiOkResponse({ type: InterviewFeedbackDto })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'getInterviewFeedback' })\n  @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager, Role.Admin], true)\n  public async getInterviewFeedback(\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('interviewId', ParseIntPipe) interviewId: number,\n    @Param('type') type: 'stage-interview' | 'interview',\n    @Req() req: CurrentRequest,\n  ) {\n    const { user } = req;\n\n    if (type !== 'stage-interview') {\n      throw new BadRequestException('Only stage interviews are supported now.');\n    }\n\n    const interview = await this.interviewFeedbackService.getStageInterviewFeedback(interviewId, user.githubId);\n\n    return new InterviewFeedbackDto(interview);\n  }\n\n  @Post('/:interviewId/:type/feedback')\n  @ApiOkResponse()\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'createInterviewFeedback' })\n  @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager], true)\n  public async createInterviewFeedback(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('interviewId', ParseIntPipe) interviewId: number,\n    @Param('type') type: 'stage-interview' | 'interview',\n    @Body() dto: PutInterviewFeedbackDto,\n    @Req() req: CurrentRequest,\n  ) {\n    const { user } = req;\n\n    const interviewerId = user.courses[courseId]?.mentorId;\n    if (!interviewerId) {\n      throw new ForbiddenException(`You are not a mentor of course ${courseId}`);\n    }\n\n    if (type !== 'stage-interview') {\n      throw new BadRequestException('Only stage interviews are supported now.');\n    }\n\n    await this.interviewFeedbackService.upsertInterviewFeedback({\n      interviewId,\n      dto,\n      interviewerId,\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/interviews/interviews.service.ts",
    "content": "import { In, Repository } from 'typeorm';\nimport { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { InterviewPair, InterviewStatus, StageInterviewFeedbackJson } from '@common/models';\nimport { CourseTask } from '@entities/courseTask';\nimport { StageInterview } from '@entities/stageInterview';\nimport { TaskInterviewStudent } from '@entities/taskInterviewStudent';\nimport { TaskInterviewResult } from '@entities/taskInterviewResult';\nimport { TaskChecker } from '@entities/taskChecker';\nimport { UsersService } from 'src/users/users.service';\nimport { Mentor, StageInterviewStudent, Student } from '@entities/index';\nimport { AvailableStudentDto } from './dto/available-student.dto';\nimport { TaskType } from '@entities/task';\nimport { CrossMentorDistributionService } from './cross-mentor-distribution.service';\nimport { InterviewDistributeDto } from './dto/interview-distribute.dto';\nimport { UserNotificationsService } from 'src/users-notifications';\n\n@Injectable()\nexport class InterviewsService {\n  constructor(\n    @InjectRepository(CourseTask)\n    readonly courseTaskRepository: Repository<CourseTask>,\n    @InjectRepository(TaskInterviewStudent)\n    readonly taskInterviewStudentRepository: Repository<TaskInterviewStudent>,\n    @InjectRepository(TaskChecker)\n    readonly taskCheckerRepository: Repository<TaskChecker>,\n    @InjectRepository(TaskInterviewResult)\n    readonly taskInterviewResultRepository: Repository<TaskInterviewResult>,\n    @InjectRepository(Student)\n    readonly studentRepository: Repository<Student>,\n    @InjectRepository(StageInterviewStudent)\n    readonly stageInterviewStudentRepository: Repository<StageInterviewStudent>,\n    @InjectRepository(Mentor)\n    readonly mentorRepository: Repository<Mentor>,\n    @InjectRepository(TaskInterviewStudent)\n    readonly interviewRepository: Repository<TaskInterviewStudent>,\n    private crossMentorService: CrossMentorDistributionService,\n    private userNotificationsService: UserNotificationsService,\n  ) {}\n\n  public getAll(\n    courseId: number,\n    filter: {\n      disabled?: boolean;\n      types?: TaskType[];\n    },\n  ) {\n    const { disabled = false, types = [TaskType.Interview] } = filter;\n    return this.courseTaskRepository.find({\n      where: { courseId, type: In(types), disabled },\n      relations: ['task'],\n    });\n  }\n\n  public getById(id: number) {\n    return this.courseTaskRepository.findOne({\n      where: { id },\n      relations: ['task'],\n    });\n  }\n\n  public static getLastStageInterview = (stageInterviews: StageInterview[]) => {\n    const [lastInterview] = stageInterviews\n      .filter(interview => interview.isCompleted)\n      .map(({ stageInterviewFeedbacks, score, courseTask }) =>\n        stageInterviewFeedbacks.map(feedback => ({\n          date: feedback.updatedDate,\n          rating:\n            score ??\n            InterviewsService.getInterviewRatings(JSON.parse(feedback.json) as StageInterviewFeedbackJson).rating,\n          version: feedback.version,\n          maxScore: courseTask?.maxScore,\n        })),\n      )\n      .reduce((acc, cur) => acc.concat(cur), [])\n      .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());\n\n    return lastInterview;\n  };\n\n  public async getInterviewRegisteredStudents(courseId: number, courseTaskId: number): Promise<AvailableStudentDto[]> {\n    const records = await this.taskInterviewStudentRepository\n      .createQueryBuilder('is')\n      .innerJoin('is.student', 'student')\n      .innerJoin('student.user', 'user')\n      .leftJoin('student.taskChecker', 'taskChecker', 'taskChecker.courseTaskId = :courseTaskId', { courseTaskId })\n      .addSelect([\n        'student.id',\n        'student.totalScore',\n        'student.mentorId',\n        ...UsersService.getPrimaryUserFields(),\n        'taskChecker.id',\n      ])\n      .where('is.courseId = :courseId', { courseId })\n      .andWhere('is.courseTaskId = :courseTaskId', { courseTaskId })\n      .andWhere('student.isExpelled = false')\n      .andWhere('taskChecker.id IS NULL')\n      .orderBy('student.totalScore', 'DESC')\n      .getMany();\n\n    return records.map(record => ({\n      id: record.student.id,\n      name: UsersService.getFullName(record.student.user),\n      githubId: record.student.user.githubId,\n      cityName: record.student.user.cityName,\n      countryName: record.student.user.countryName,\n      totalScore: record.student.totalScore,\n      registeredDate: record.createdDate,\n    }));\n  }\n\n  public async getInterviewPairs(courseTaskId: number): Promise<InterviewPair[]> {\n    const records = await this.taskCheckerRepository\n      .createQueryBuilder('tc')\n      .leftJoin('tc.mentor', 'mentor')\n      .leftJoin('tc.student', 'student')\n      .leftJoin('mentor.user', 'mentorUser')\n      .leftJoin('student.user', 'studentUser')\n      .leftJoin(\n        TaskInterviewResult,\n        'tir',\n        'tc.studentId = tir.studentId AND tc.courseTaskId = tir.courseTaskId AND tc.mentorId = tir.mentorId',\n      )\n      .addSelect([\n        'tc.id',\n        'tir.score',\n        'mentorUser.id',\n        'mentorUser.firstName',\n        'mentorUser.lastName',\n        'mentorUser.githubId',\n        'studentUser.id',\n        'studentUser.firstName',\n        'studentUser.lastName',\n        'studentUser.githubId',\n      ])\n      .where('tc.courseTaskId = :courseTaskId', { courseTaskId })\n      .getRawMany();\n\n    return records.map(record => ({\n      id: record.tc_id,\n      result: record.tir_score,\n      status: record.tir_score || record.tir_score === 0 ? InterviewStatus.Completed : InterviewStatus.NotCompleted,\n      interviewer: {\n        id: record.mentorUser_id,\n        githubId: record.mentorUser_githubId,\n        name: UsersService.getFullName({\n          firstName: record.mentorUser_firstName,\n          lastName: record.mentorUser_lastName,\n        }),\n      },\n      student: {\n        id: record.studentUser_id,\n        githubId: record.studentUser_githubId,\n        name: UsersService.getFullName({\n          firstName: record.studentUser_firstName,\n          lastName: record.studentUser_lastName,\n        }),\n      },\n    }));\n  }\n\n  /**\n   * TODO: rewrite it. Hard to maintain and understand\n   */\n  public async getStageInterviewAvailableStudents(courseId: number): Promise<AvailableStudentDto[]> {\n    const { entities, raw } = await this.studentRepository\n      .createQueryBuilder('student')\n      .innerJoin(StageInterviewStudent, 'sis', 'sis.studentId = student.id')\n      .innerJoin('student.user', 'user')\n      .leftJoin('student.stageInterviews', 'si')\n      .leftJoin('si.stageInterviewFeedbacks', 'sif')\n      .leftJoin('si.courseTask', 'courseTask')\n      .addSelect([\n        ...UsersService.getPrimaryUserFields(),\n        'si.id',\n        'si.isGoodCandidate',\n        'si.isCompleted',\n        'si.isCanceled',\n        'si.score',\n        'si.decision',\n        'sif.json',\n        'sif.updatedDate',\n        'sif.version',\n        'sis.createdDate',\n        'courseTask.maxScore',\n      ])\n      .where(\n        [\n          `student.courseId = :courseId`,\n          `student.isFailed = false`,\n          `student.isExpelled = false`,\n          `student.mentorId IS NULL`,\n          `student.mentoring <> false`,\n        ].join(' AND '),\n        { courseId },\n      )\n      .orderBy('student.totalScore', 'DESC')\n      .getRawAndEntities();\n\n    const result = entities\n      .filter(s => {\n        return (\n          !s.stageInterviews ||\n          s.stageInterviews.length === 0 ||\n          s.stageInterviews.every(i => (i.isCompleted && i.decision !== 'draft') || i.isCanceled)\n        );\n      })\n      .map(student => {\n        const { id, user, totalScore } = student;\n        const stageInterviews: StageInterview[] = student.stageInterviews || [];\n        const lastStageInterview = InterviewsService.getLastStageInterview(stageInterviews);\n        return {\n          id,\n          totalScore,\n          githubId: user.githubId,\n          name: UsersService.getFullName(student.user),\n          cityName: user.cityName,\n          countryName: user.countryName,\n          isGoodCandidate: this.isGoodCandidate(stageInterviews),\n          rating: lastStageInterview?.rating,\n          maxScore: lastStageInterview?.maxScore,\n          feedbackVersion: lastStageInterview?.version,\n          registeredDate: raw.find(item => item.student_id === student.id)?.sis_createdDate,\n        };\n      });\n\n    return result;\n  }\n\n  /**\n   * @deprecated - should be removed once Artsiom A. makes migration of the legacy feedback format\n   */\n  private static getInterviewRatings({ skills, programmingTask, resume }: StageInterviewFeedbackJson) {\n    const commonSkills = Object.values(skills?.common ?? {}).filter(Boolean) as number[];\n    const dataStructuresSkills = Object.values(skills?.dataStructures ?? {}).filter(Boolean) as number[];\n\n    const htmlCss = skills?.htmlCss.level;\n    const common = commonSkills.reduce((acc, cur) => acc + cur, 0) / commonSkills.length;\n    const dataStructures = dataStructuresSkills.reduce((acc, cur) => acc + cur, 0) / dataStructuresSkills.length;\n\n    if (resume?.score !== undefined) {\n      const rating = resume.score;\n      return { rating, htmlCss, common, dataStructures };\n    }\n\n    const ratingsCount = 4;\n    const ratings = [htmlCss, common, dataStructures, programmingTask.codeWritingLevel].filter(Boolean) as number[];\n    const rating = (ratings.length === ratingsCount ? ratings.reduce((sum, num) => sum + num) / ratingsCount : 0) * 10;\n\n    return { rating, htmlCss, common, dataStructures };\n  }\n\n  private isGoodCandidate(stageInterviews: StageInterview[]) {\n    return stageInterviews.some(i => i.isCompleted && i.isGoodCandidate);\n  }\n\n  public async registerStudentToStageInterview(courseId: number, githubId: string) {\n    const student = await this.studentRepository.findOneOrFail({ where: { courseId, user: { githubId } } });\n    if (student.isExpelled) {\n      throw new BadRequestException('Student is expelled');\n    }\n\n    const studentId = student.id;\n    const record = await this.stageInterviewStudentRepository.findOne({ where: { courseId, studentId } });\n    if (record) {\n      throw new BadRequestException('Student is already registered');\n    }\n    await this.stageInterviewStudentRepository.insert({ courseId, studentId });\n    return this.stageInterviewStudentRepository.findOneByOrFail({ courseId, studentId });\n  }\n\n  public async registerStudentToInterview(courseId: number, courseTaskId: number, githubId: string) {\n    const student = await this.studentRepository.findOneOrFail({ where: { courseId, user: { githubId } } });\n    if (student.isExpelled) {\n      throw new BadRequestException('Student is expelled');\n    }\n\n    const record = await this.taskInterviewStudentRepository.findOne({\n      where: { courseId, studentId: student.id, courseTaskId },\n    });\n    if (record) {\n      throw new BadRequestException('Student is already registered');\n    }\n\n    const taskInterviewStudent = await this.taskInterviewStudentRepository.save({\n      courseId,\n      studentId: student.id,\n      courseTaskId,\n    });\n    return taskInterviewStudent;\n  }\n\n  public async distributeInterviewPairs(\n    courseId: number,\n    courseTaskId: number,\n    { clean, registrationEnabled }: InterviewDistributeDto,\n  ) {\n    const courseTask = await this.courseTaskRepository.findOne({ where: { id: courseTaskId }, select: ['id'] });\n\n    if (!courseTask) {\n      return null;\n    }\n\n    const mentors = await this.mentorRepository\n      .createQueryBuilder('mentor')\n      .innerJoinAndSelect('mentor.user', 'user')\n      .leftJoinAndSelect('mentor.students', 'students')\n      .where('mentor.courseId = :courseId', { courseId })\n      .andWhere('mentor.isExpelled = false')\n      .andWhere('(students.isExpelled = false OR students IS NULL)')\n      .getMany();\n\n    if (mentors.length === 0) {\n      return [];\n    }\n\n    if (clean) {\n      await this.taskCheckerRepository.delete({ courseTaskId });\n    }\n\n    let registeredStudentsIds: number[] | undefined = undefined;\n\n    if (registrationEnabled) {\n      const students = await this.interviewRepository.find({\n        where: { courseId, courseTaskId },\n        select: ['studentId'],\n      });\n      registeredStudentsIds = students.map(s => s.studentId);\n    }\n\n    const existingPairs: TaskChecker[] = clean\n      ? []\n      : await this.taskCheckerRepository.find({ where: { courseTaskId } });\n\n    const { mentors: crossMentors } = this.crossMentorService.distribute(mentors, existingPairs, registeredStudentsIds);\n\n    const taskCheckPairs = crossMentors\n      .map(stm => stm.students?.map(s => ({ courseTaskId, mentorId: stm.id, studentId: s.id })) ?? [])\n      .reduce((acc, student) => acc.concat(student), []);\n\n    if (taskCheckPairs.length > 0) {\n      await this.taskCheckerRepository.save(taskCheckPairs);\n\n      await Promise.all(\n        taskCheckPairs.map(async pair => {\n          const student = await this.studentRepository.findOne({\n            where: { courseId, id: pair.studentId },\n            relations: ['user'],\n          });\n          const mentor = await this.mentorRepository.findOne({\n            where: { courseId, id: pair.mentorId },\n            relations: ['user'],\n            select: ['id', 'user'],\n          });\n          if (student && mentor) {\n            const interviewerFirstName = !mentor.user.firstName ? '' : mentor.user.firstName.trim();\n            const interviewerLastName = !mentor.user.lastName ? '' : mentor.user.lastName.trim();\n            const interviewerFullName = `${interviewerFirstName}${interviewerFirstName ? ' ' : ''}${interviewerLastName}`;\n            await this.userNotificationsService.sendEventNotification({\n              userId: student.user.id,\n              notificationId: 'interviewerAssigned',\n              data: {\n                interviewer: {\n                  id: mentor.id,\n                  githubId: mentor.user.githubId,\n                  name: interviewerFullName,\n                },\n              },\n            });\n          }\n        }),\n      );\n    }\n\n    return taskCheckPairs;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentor-reviews/dto/index.ts",
    "content": "export * from './mentor-reviews-query.dto';\nexport * from './mentor-reviews.dto';\n"
  },
  {
    "path": "nestjs/src/courses/mentor-reviews/dto/mentor-review-assign.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsInt, IsOptional } from 'class-validator';\n\nexport class MentorReviewAssignDto {\n  @ApiProperty()\n  @IsInt()\n  courseTaskId: number;\n\n  @ApiPropertyOptional()\n  @IsOptional()\n  @IsInt()\n  mentorId?: number;\n\n  @ApiProperty()\n  @IsInt()\n  studentId: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentor-reviews/dto/mentor-reviews-query.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsOptional, IsString } from 'class-validator';\n\nexport class MentorReviewsQueryDto {\n  @ApiProperty()\n  @IsString()\n  public current: string;\n\n  @ApiProperty()\n  @IsString()\n  public pageSize: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  public tasks: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  public student: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  public checker: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  public sortField: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  public sortOrder: 'ASC' | 'DESC';\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentor-reviews/dto/mentor-reviews.dto.ts",
    "content": "import { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { PaginationMeta } from 'src/core/paginate';\nimport { PaginationMetaDto } from 'src/core/paginate/dto/Paginate.dto';\nimport { TaskSolution } from '@entities/taskSolution';\n\nexport class MentorReviewDto {\n  constructor(taskSolution: TaskSolution) {\n    const taskResult = taskSolution.student.taskResults?.[0];\n\n    this.id = taskSolution.id;\n    this.taskName = taskSolution.courseTask.task.name;\n    this.taskId = taskSolution.courseTask.id;\n    this.solutionUrl = taskSolution.url;\n    this.submittedAt = new Date(taskSolution.createdDate);\n    this.checker = this.getChecker(taskSolution);\n    this.score = taskResult?.score;\n    this.maxScore = taskSolution.courseTask.maxScore;\n    this.student = taskSolution.student.user.githubId;\n    this.studentId = taskSolution.student.id;\n    this.reviewedAt = taskResult?.updatedDate ? new Date(taskResult.updatedDate) : undefined;\n    this.taskDescriptionUrl = taskSolution.courseTask.task.descriptionUrl;\n  }\n\n  private getChecker(taskSolution: TaskSolution) {\n    const taskResult = taskSolution.student.taskResults?.[0];\n\n    if (taskResult?.score !== undefined) {\n      return taskResult.lastChecker?.githubId;\n    }\n\n    return taskSolution.student.taskChecker?.[0]?.mentor.user.githubId ?? taskSolution.student.mentor?.user.githubId;\n  }\n\n  @ApiProperty({ description: 'Task solution id' })\n  id: number;\n\n  @ApiProperty({ description: 'Course task name' })\n  taskName: string;\n\n  @ApiProperty({ description: 'Course task id' })\n  taskId: number;\n\n  @ApiProperty({ description: 'Task solution url' })\n  solutionUrl: string;\n\n  @ApiProperty({ description: 'Task solution submission date' })\n  submittedAt: Date;\n\n  @ApiProperty({ description: 'Checker github id' })\n  checker?: string;\n\n  @ApiProperty({ description: 'Task solution score' })\n  score?: number;\n\n  @ApiProperty({ description: 'Task max score' })\n  maxScore: number;\n\n  @ApiProperty({ description: 'Student github id' })\n  student: string;\n\n  @ApiProperty({ description: 'Student id' })\n  studentId: number;\n\n  @ApiProperty({ description: 'Task solution review date' })\n  reviewedAt?: Date;\n\n  @ApiProperty({ description: 'Task description url' })\n  taskDescriptionUrl: string;\n}\n\n@ApiResponse({})\nexport class MentorReviewsDto {\n  constructor(data: { items: TaskSolution[]; meta: PaginationMeta }) {\n    this.content = data.items.map(review => new MentorReviewDto(review));\n    this.pagination = new PaginationMetaDto(data.meta);\n  }\n\n  @ApiProperty({ type: [MentorReviewDto] })\n  content: MentorReviewDto[];\n\n  @ApiProperty({ type: PaginationMetaDto })\n  pagination: PaginationMetaDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentor-reviews/index.ts",
    "content": "export * from './mentor-reviews.controller';\nexport * from './mentor-reviews.service';\n"
  },
  {
    "path": "nestjs/src/courses/mentor-reviews/mentor-reviews.controller.ts",
    "content": "import { CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { MentorReviewsService } from './mentor-reviews.service';\nimport { Body, Controller, Get, Param, ParseIntPipe, Post, Query, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { MentorReviewsDto, MentorReviewsQueryDto } from './dto';\nimport { MentorReviewAssignDto } from './dto/mentor-review-assign.dto';\n\n@Controller('course/:courseId/mentor-reviews')\n@ApiTags('mentor-reviews')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class MentorReviewsController {\n  constructor(private mentorReviewsService: MentorReviewsService) {}\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getMentorReviews' })\n  @ApiOkResponse({ type: MentorReviewsDto })\n  @RequiredRoles([CourseRole.Dementor, CourseRole.Manager, Role.Admin], true)\n  public async getMentorReviews(\n    @Query() query: MentorReviewsQueryDto,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ) {\n    const page = parseInt(query.current);\n    const limit = parseInt(query.pageSize);\n    const { student, checker, tasks, sortField, sortOrder } = query;\n    const mentorReviews = await this.mentorReviewsService.getMentorReviews(\n      courseId,\n      page,\n      limit,\n      tasks,\n      student,\n      checker,\n      sortField,\n      sortOrder,\n    );\n\n    return new MentorReviewsDto(mentorReviews);\n  }\n\n  @Post('/')\n  @ApiOperation({ operationId: 'assignReviewer' })\n  @ApiOkResponse({})\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  public async assignReviewer(@Param('courseId', ParseIntPipe) _courseId: number, @Body() dto: MentorReviewAssignDto) {\n    return await this.mentorReviewsService.assignReviewer(dto);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentor-reviews/mentor-reviews.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\n\nimport { TaskSolution } from '@entities/taskSolution';\nimport { paginate } from 'src/core/paginate';\nimport { Checker } from '@entities/courseTask';\nimport { TaskChecker } from '../../../../server/src/models';\nimport { MentorReviewAssignDto } from './dto/mentor-review-assign.dto';\n\n@Injectable()\nexport class MentorReviewsService {\n  constructor(\n    @InjectRepository(TaskSolution)\n    readonly taskSolutionRepository: Repository<TaskSolution>,\n    @InjectRepository(TaskChecker)\n    readonly taskCheckerRepository: Repository<TaskChecker>,\n  ) {}\n\n  private buildMentorReviewsQuery({\n    courseId,\n    tasks,\n    student,\n    checker,\n    sortField,\n    sortOrder,\n  }: {\n    courseId: number;\n    tasks?: string;\n    student?: string;\n    checker?: string;\n    sortField?: string;\n    sortOrder?: 'ASC' | 'DESC';\n  }) {\n    const query = this.taskSolutionRepository\n      .createQueryBuilder('taskSolution')\n      .innerJoin('taskSolution.courseTask', 'courseTask')\n      .innerJoin('courseTask.task', 'task')\n      .innerJoin('taskSolution.student', 'student')\n      .innerJoin('student.user', 'studentUser')\n      .leftJoin('student.mentor', 'studentMentor')\n      .leftJoin('studentMentor.user', 'studentMentorUser')\n      .leftJoinAndSelect(\n        'student.taskResults',\n        'taskResult',\n        'taskResult.courseTaskId = taskSolution.courseTaskId AND taskResult.studentId = taskSolution.studentId',\n      )\n      .leftJoinAndSelect(\n        'student.taskChecker',\n        'taskChecker',\n        'taskChecker.studentId = taskSolution.studentId AND taskChecker.courseTaskId = taskSolution.courseTaskId',\n      )\n      .leftJoin('taskChecker.mentor', 'mentor')\n      .leftJoin('mentor.user', 'mentorUser')\n      .leftJoin('taskResult.lastChecker', 'lastChecker')\n      .where('student.courseId = :courseId AND student.isExpelled = false', { courseId })\n      .andWhere('courseTask.checker = :checker', { checker: Checker.Mentor })\n      .select([\n        'student.id',\n        'student.mentorId',\n        'studentUser.firstName',\n        'studentUser.lastName',\n        'studentUser.githubId',\n        'task.name',\n        'task.descriptionUrl',\n        'courseTask.id',\n        'courseTask.maxScore',\n        'taskSolution.studentId',\n        'taskSolution.url',\n        'taskSolution.id',\n        'taskSolution.createdDate',\n        'taskResult.score',\n        'taskResult.lastCheckerId',\n        'taskResult.updatedDate',\n        'taskChecker.mentorId',\n        'taskChecker.mentor',\n        'mentor.id',\n        'mentorUser.githubId',\n        'studentMentor.id',\n        'studentMentorUser.githubId',\n        'lastChecker.githubId',\n      ]);\n\n    if (tasks) {\n      const taskIds = tasks.split(',').map(id => parseInt(id));\n      query.andWhere('courseTask.id IN (:...taskIds)', { taskIds });\n    }\n\n    if (student) {\n      query.andWhere('studentUser.githubId ILIKE :student', { student: `%${student}%` });\n    }\n\n    if (checker) {\n      query.andWhere('COALESCE(lastChecker.githubId, mentorUser.githubId, studentMentorUser.githubId) ILIKE :checker', {\n        checker: `%${checker}%`,\n      });\n    }\n\n    if (sortField && sortOrder) {\n      if (sortField === 'submittedAt') {\n        query.orderBy('taskSolution.createdDate', sortOrder);\n      }\n\n      if (sortField === 'reviewedAt') {\n        query.orderBy('taskResult.updatedDate', sortOrder);\n      }\n    }\n\n    query.addOrderBy('taskSolution.id', 'ASC');\n\n    return query;\n  }\n\n  public async getMentorReviews(\n    courseId: number,\n    page: number,\n    limit: number,\n    tasks?: string,\n    student?: string,\n    checker?: string,\n    sortField?: string,\n    sortOrder?: 'ASC' | 'DESC',\n  ) {\n    const query = this.buildMentorReviewsQuery({ courseId, tasks, student, checker, sortField, sortOrder });\n    const data = await paginate(query, { page, limit });\n\n    return data;\n  }\n\n  private async findTaskCheckerRecord(courseTaskId: number, studentId: number) {\n    return await this.taskCheckerRepository.findOne({\n      where: { studentId, courseTaskId },\n      select: {\n        id: true,\n      },\n    });\n  }\n\n  public async assignReviewer({ courseTaskId, mentorId, studentId }: MentorReviewAssignDto) {\n    const taskCheckerRecord = await this.findTaskCheckerRecord(courseTaskId, studentId);\n\n    if (!mentorId) {\n      if (taskCheckerRecord) {\n        return await this.taskCheckerRepository.delete(taskCheckerRecord.id);\n      }\n\n      return;\n    }\n\n    if (taskCheckerRecord) {\n      return await this.taskCheckerRepository.update(taskCheckerRecord.id, { courseTaskId, mentorId, studentId });\n    }\n\n    return await this.taskCheckerRepository.insert({\n      courseTaskId,\n      studentId,\n      mentorId,\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentors/dto/index.ts",
    "content": ""
  },
  {
    "path": "nestjs/src/courses/mentors/dto/mentor-dashboard.dto.ts",
    "content": "import { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { SolutionItem } from '../mentors.service';\n\nexport enum SolutionItemStatus {\n  InReview = 'in-review',\n  Done = 'done',\n  RandomTask = 'random-task',\n}\n\n@ApiResponse({})\nexport class MentorDashboardDto {\n  constructor(item: SolutionItem) {\n    this.studentName = item.person.name;\n    this.studentGithubId = item.person.githubId;\n    this.taskName = item.taskName;\n    this.taskDescriptionUrl = item.taskDescriptionUrl;\n    this.courseTaskId = item.courseTaskId;\n    this.maxScore = item.maxScore;\n    this.resultScore = item.resultScore ?? null;\n    this.solutionUrl = item.solutionUrl;\n    this.status = item.status;\n    this.endDate = item.endDate;\n  }\n\n  @ApiProperty()\n  studentGithubId: string;\n\n  @ApiProperty()\n  studentName: string;\n\n  @ApiProperty()\n  taskName: string;\n\n  @ApiProperty()\n  taskDescriptionUrl: string;\n\n  @ApiProperty()\n  courseTaskId: number;\n\n  @ApiProperty()\n  maxScore: number;\n\n  @ApiProperty({ nullable: true, type: Number })\n  resultScore: number | null;\n\n  @ApiProperty({ type: String })\n  solutionUrl: string;\n\n  @ApiProperty({ enum: SolutionItemStatus, type: SolutionItemStatus, enumName: 'SolutionItemStatusEnum' })\n  public status: SolutionItemStatus;\n\n  @ApiProperty({ type: String })\n  endDate: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentors/dto/mentor-options.dto.ts",
    "content": "import { Student } from '@entities/index';\nimport { Mentor } from '@entities/mentor';\nimport { PreferredStudentsLocation } from '@entities/mentorRegistry';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsArray, IsNumber, IsString } from 'class-validator';\nimport { PersonDto } from 'src/core/dto';\n\ntype StudentInput = Student & { user: { githubId: string; firstName: string; lastName: string } };\n\nclass StudentsDto {\n  @ApiProperty()\n  @IsNumber()\n  id: number;\n\n  @ApiProperty()\n  @IsString()\n  githubId: string;\n\n  @ApiProperty()\n  @IsString()\n  name: string;\n\n  constructor(student: StudentInput) {\n    this.id = student.id;\n    this.githubId = student.user.githubId;\n    this.name = PersonDto.getName(student.user);\n  }\n}\n\n@ApiResponse({})\nexport class MentorOptionsDto {\n  constructor(\n    mentor: Omit<Mentor, 'students'> & {\n      students: StudentInput[];\n    },\n  ) {\n    this.maxStudentsLimit = mentor.maxStudentsLimit;\n    this.preferedStudentsLocation = mentor.studentsPreference as PreferredStudentsLocation;\n    this.students = mentor.students.map(student => new StudentsDto(student));\n  }\n\n  @ApiProperty()\n  @IsNumber()\n  maxStudentsLimit: number;\n\n  @ApiProperty({ enum: PreferredStudentsLocation })\n  preferedStudentsLocation: PreferredStudentsLocation;\n\n  @ApiProperty({ type: [StudentsDto] })\n  @IsArray()\n  students: StudentsDto[];\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentors/dto/mentor-student.dto.ts",
    "content": "import { Student } from '@entities/student';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsArray, IsString } from 'class-validator';\nimport { StudentDto } from '../../students/dto';\nimport { StudentFeedbackDto } from '../../students/feedbacks/dto';\n\n@ApiResponse({})\nexport class MentorStudentDto extends StudentDto {\n  constructor(student: Student) {\n    super(student);\n    this.feedbacks = student.feedbacks?.map(feedback => new StudentFeedbackDto(feedback)) ?? [];\n    this.repoUrl = student.repository ? student.repository : null;\n  }\n\n  @ApiProperty({ type: [StudentFeedbackDto] })\n  @IsArray()\n  feedbacks: StudentFeedbackDto[];\n\n  @ApiProperty({ nullable: true, type: String })\n  @IsString()\n  repoUrl: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentors/index.ts",
    "content": "export * from './mentors.service';\nexport * from './mentors.controller';\n"
  },
  {
    "path": "nestjs/src/courses/mentors/mentors.controller.ts",
    "content": "import {\n  Controller,\n  ForbiddenException,\n  Get,\n  NotFoundException,\n  Param,\n  ParseIntPipe,\n  Req,\n  UseGuards,\n} from '@nestjs/common';\nimport {\n  ApiBadRequestResponse,\n  ApiForbiddenResponse,\n  ApiNotFoundResponse,\n  ApiOkResponse,\n  ApiOperation,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { MentorsService } from '.';\nimport { CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, RoleGuard } from '../../auth';\nimport { MentorDashboardDto } from './dto/mentor-dashboard.dto';\nimport { MentorOptionsDto } from './dto/mentor-options.dto';\nimport { MentorStudentDto } from './dto/mentor-student.dto';\n\n@Controller('mentors')\n@ApiTags('mentors')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class MentorsController {\n  constructor(private mentorsService: MentorsService) {}\n\n  @Get('/:mentorId/course/:courseId/options')\n  @ApiOperation({ operationId: 'getMentorOptions' })\n  @ApiOkResponse({ type: MentorOptionsDto })\n  @ApiForbiddenResponse()\n  @ApiNotFoundResponse()\n  @RequiredRoles([CourseRole.Mentor])\n  public async getMentorOptions(\n    @Param('mentorId', ParseIntPipe) mentorId: number,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Req() req: CurrentRequest,\n  ) {\n    const courseMentorId = req.user.courses[courseId]?.mentorId;\n    if (courseMentorId !== mentorId) {\n      throw new ForbiddenException();\n    }\n\n    const mentor = await this.mentorsService.getMentorOptions(mentorId);\n\n    if (!mentor) {\n      throw new NotFoundException();\n    }\n\n    return new MentorOptionsDto(mentor);\n  }\n\n  @Get('/:mentorId/students')\n  @ApiOperation({ operationId: 'getMentorStudents' })\n  @ApiOkResponse({ type: [MentorStudentDto] })\n  @ApiBadRequestResponse()\n  @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager])\n  public async getMentorStudents(@Param('mentorId', ParseIntPipe) mentorId: number, @Req() req: CurrentRequest) {\n    const items = await this.mentorsService.getStudents(mentorId, req.user.id);\n    return items.map(item => new MentorStudentDto(item));\n  }\n\n  @Get('/:mentorId/course/:courseId/students')\n  @ApiOperation({ operationId: 'getCourseStudentsCount' })\n  @ApiOkResponse({ type: Number })\n  @ApiBadRequestResponse()\n  @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager])\n  public async getCourseStudentsCount(\n    @Param('mentorId', ParseIntPipe) mentorId: number,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ) {\n    return await this.mentorsService.getCourseStudentsCount(mentorId, courseId);\n  }\n\n  @Get('/:mentorId/course/:courseId/dashboard')\n  @ApiOperation({ operationId: 'getMentorDashboardData' })\n  @ApiOkResponse({ type: [MentorDashboardDto] })\n  @ApiBadRequestResponse()\n  @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager])\n  public async getMentorDashboardData(\n    @Param('mentorId', ParseIntPipe) mentorId: number,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ): Promise<MentorDashboardDto[]> {\n    return await this.mentorsService.getStudentsTasks(mentorId, courseId);\n  }\n\n  @Get('/:mentorId/course/:courseId/random-task')\n  @ApiOperation({ operationId: 'getRandomTask' })\n  @ApiOkResponse({})\n  @ApiBadRequestResponse()\n  @RequiredRoles([CourseRole.Mentor, CourseRole.Supervisor, CourseRole.Manager])\n  public async getRandomTask(\n    @Param('mentorId', ParseIntPipe) mentorId: number,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ) {\n    return await this.mentorsService.getRandomTask(mentorId, courseId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/mentors/mentors.service.ts",
    "content": "import { Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\n\nimport { Mentor } from '@entities/mentor';\nimport { Student } from '@entities/student';\nimport { CourseTask, Checker } from '@entities/courseTask';\nimport { TaskResult } from '@entities/taskResult';\nimport { TaskSolution } from '@entities/taskSolution';\nimport { Task } from '@entities/task';\n\nimport { MentorBasic, MentorDetails } from '@common/models';\n\nimport { PersonDto } from 'src/core/dto';\nimport { MentorDashboardDto, SolutionItemStatus } from './dto/mentor-dashboard.dto';\nimport { addWeeks } from 'date-fns';\nimport { TaskChecker, User } from '../../../../server/src/models';\nimport { PreferredStudentsLocation } from '@entities/mentorRegistry';\n\nexport interface SolutionItem {\n  maxScore: number;\n  taskName: string;\n  taskDescriptionUrl: string;\n  courseTaskId: number;\n  resultScore: number | null;\n  solutionUrl: string;\n  status: SolutionItemStatus;\n  endDate: string;\n  person: PersonDto;\n}\n\n@Injectable()\nexport class MentorsService {\n  constructor(\n    @InjectRepository(Mentor)\n    readonly mentorsRepository: Repository<Mentor>,\n    @InjectRepository(Student)\n    readonly studentRepository: Repository<Student>,\n    @InjectRepository(TaskSolution)\n    readonly taskSolutionRepository: Repository<TaskSolution>,\n    @InjectRepository(TaskChecker)\n    readonly taskCheckerRepository: Repository<TaskChecker>,\n  ) {}\n\n  public static convertMentorToMentorBasic(mentor: Mentor): MentorBasic {\n    const user = mentor.user;\n    return {\n      id: mentor.id,\n      name: PersonDto.getName(user),\n      githubId: user.githubId,\n      cityName: user.cityName ?? '',\n      countryName: user.countryName ?? '',\n      isActive: !mentor.isExpelled,\n      students: mentor.students?.filter(s => !s.isExpelled && !s.isFailed).map(s => ({ id: s.id })) ?? [],\n    };\n  }\n\n  public static convertMentorToMentorDetails(mentor: Mentor): MentorDetails {\n    const mentorBasic = MentorsService.convertMentorToMentorBasic(mentor);\n    const user = mentor.user;\n    return {\n      ...mentorBasic,\n      students: mentor.students ?? [],\n      cityName: user.cityName ?? '',\n      countryName: user.countryName ?? '',\n      maxStudentsLimit: mentor.maxStudentsLimit,\n      studentsPreference: mentor.studentsPreference ?? PreferredStudentsLocation.ANY,\n      studentsCount: mentor.students ? mentor.students.length : 0,\n      screenings: {\n        total: mentor.stageInterviews ? mentor.stageInterviews.length : 0,\n      },\n    };\n  }\n\n  public getById(mentorId: number) {\n    return this.mentorsRepository.findOne({\n      where: { id: mentorId },\n    });\n  }\n\n  public getByUserId(courseId: number, userId: number) {\n    return this.mentorsRepository.findOne({\n      where: { courseId, userId },\n    });\n  }\n\n  public getStudents(mentorId: number, userId: number) {\n    return this.studentRepository\n      .createQueryBuilder('student')\n      .leftJoinAndSelect('student.user', 'user')\n      .leftJoinAndSelect('student.feedbacks', 'feedback', 'feedback.auhtorId = :userId', { userId })\n      .where('student.mentorId = :mentorId', { mentorId })\n      .getMany();\n  }\n\n  public async getCourseStudentsCount(mentorId: number, courseId: number) {\n    return await this.studentRepository.count({\n      where: { mentorId, courseId },\n    });\n  }\n\n  private async getSolutions(mentorId: number, courseId: number): Promise<SolutionItem[]> {\n    const solutions = await this.taskSolutionRepository\n      .createQueryBuilder('ts')\n      .leftJoin(TaskResult, 'tr', 'tr.\"studentId\" = ts.\"studentId\" AND tr.\"courseTaskId\" = ts.\"courseTaskId\"')\n      .leftJoin(TaskChecker, 'tc', 'tc.\"studentId\" = ts.\"studentId\" AND tc.\"courseTaskId\" = ts.\"courseTaskId\"')\n      .innerJoin(CourseTask, 'ct', 'ct.id = ts.\"courseTaskId\"')\n      .innerJoin(Task, 't', 't.id = ct.\"taskId\"')\n      .innerJoin(Student, 's', 's.id = ts.\"studentId\"')\n      .innerJoin(User, 'u', 'u.id = s.\"userId\"')\n      .select([\n        's.id',\n        's.mentorId',\n        'u.firstName',\n        'u.lastName',\n        'u.githubId',\n        't.name',\n        't.descriptionUrl',\n        'ct.id',\n        'ct.maxScore',\n        'ct.studentEndDate',\n        'ts.studentId',\n        'tr.score',\n        'ts.url',\n      ])\n      .where('s.\"courseId\" = :courseId', { courseId })\n      .andWhere('ct.checker = :checker', { checker: Checker.Mentor })\n      .andWhere('s.\"mentorId\" = :mentorId', { mentorId })\n      .orWhere('tc.\"mentorId\" = :mentorId', { mentorId })\n      .orderBy('ct.\"studentEndDate\"', 'DESC')\n      .getRawMany();\n\n    return solutions.map(s => ({\n      taskName: s.t_name,\n      courseTaskId: s.ct_id,\n      maxScore: s.ct_maxScore,\n      resultScore: s.tr_score,\n      solutionUrl: s.ts_url,\n      taskDescriptionUrl: s.t_descriptionUrl,\n      status: this.getStatus(s.s_mentorId, s.tr_score),\n      endDate: addWeeks(new Date(s.ct_studentEndDate), 2).toISOString(),\n      person: new PersonDto({\n        id: s.s_id,\n        firstName: s.u_firstName,\n        lastName: s.u_lastName,\n        githubId: s.u_githubId,\n      }),\n    }));\n  }\n\n  private getStatus(mentorId: number, resultScore: number) {\n    // resultScore = 0 should be considered as a result\n    const hasScore = resultScore !== null;\n    if (!mentorId && !hasScore) {\n      return SolutionItemStatus.RandomTask;\n    }\n\n    return hasScore ? SolutionItemStatus.Done : SolutionItemStatus.InReview;\n  }\n\n  public async getStudentsTasks(mentorId: number, courseId: number): Promise<MentorDashboardDto[]> {\n    const solutions = await this.getSolutions(mentorId, courseId);\n    return solutions.map(solution => new MentorDashboardDto(solution));\n  }\n\n  private async getRandomSolution(courseId: number): Promise<{ courseTaskId: number; studentId: number }> {\n    const task = await this.taskSolutionRepository\n      .createQueryBuilder('ts')\n      .leftJoin(TaskResult, 'tr', 'tr.\"studentId\" = ts.\"studentId\" AND tr.\"courseTaskId\" = ts.\"courseTaskId\"')\n      .leftJoin(TaskChecker, 'tc', 'tc.\"studentId\" = ts.\"studentId\" AND tc.\"courseTaskId\" = ts.\"courseTaskId\"')\n      .innerJoin(CourseTask, 'ct', 'ct.id = ts.\"courseTaskId\"')\n      .innerJoin(Student, 's', 's.id = ts.\"studentId\"')\n      .select(['ts.studentId', 'ts.courseTaskId', 'tc.id'])\n      .where('s.\"courseId\" = :courseId', { courseId })\n      .andWhere('s.\"isExpelled\" = false')\n      .andWhere('s.\"mentorId\" IS NULL')\n      .andWhere('ct.checker = :checker', { checker: Checker.Mentor })\n      .andWhere('tr.\"score\" IS NULL')\n      .andWhere('tc.\"id\" IS NULL')\n      .orderBy('s.\"totalScore\"', 'DESC')\n      .getOneOrFail();\n\n    return {\n      courseTaskId: task.courseTaskId,\n      studentId: task.studentId,\n    };\n  }\n\n  public async getRandomTask(mentorId: number, courseId: number) {\n    const { courseTaskId, studentId } = await this.getRandomSolution(courseId);\n\n    if (courseTaskId && studentId) {\n      const checker: Partial<TaskChecker> = {\n        courseTaskId,\n        studentId,\n        mentorId,\n      };\n\n      return await this.taskCheckerRepository.insert(checker);\n    }\n\n    throw new NotFoundException();\n  }\n\n  public async getMentorOptions(mentorId: number) {\n    return this.mentorsRepository.findOne({\n      where: { id: mentorId },\n      select: {\n        students: {\n          id: true,\n          user: {\n            githubId: true,\n          },\n        },\n      },\n      relations: {\n        students: {\n          user: true,\n        },\n      },\n    }) as Promise<\n      | (Omit<Mentor, 'students'> & {\n          students: (Student & { user: { githubId: string; firstName: string; lastName: string } })[];\n        })\n      | null\n    >;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/score/dto/score-query.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsString, IsOptional } from 'class-validator';\n\nexport const orderByFieldMapping = {\n  rank: 'student.rank',\n  totalScore: 'student.totalScore',\n  crossCheckScore: 'student.crossCheckScore',\n  githubId: 'user.githubId',\n  name: 'user.firstName',\n  cityName: 'user.cityName',\n  mentor: 'mu.githubId',\n  totalScoreChangeDate: 'student.totalScoreChangeDate',\n  repositoryLastActivityDate: 'student.repositoryLastActivityDate',\n};\n\nexport type OrderDirection = 'asc' | 'desc';\n\nexport type OrderField = keyof typeof orderByFieldMapping;\n\nexport class ScoreQueryDto {\n  @ApiProperty()\n  @IsString()\n  public activeOnly: 'true' | 'false';\n\n  @ApiProperty({ enum: Object.keys(orderByFieldMapping) })\n  @IsString()\n  public orderBy: OrderField;\n\n  @ApiProperty({ enum: ['asc', 'desc'] })\n  @IsString()\n  public orderDirection: OrderDirection;\n\n  @ApiProperty()\n  @IsString()\n  public current: string;\n\n  @ApiProperty()\n  @IsString()\n  public pageSize: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  githubId?: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  name?: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  'mentor.githubId'?: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  cityName?: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/score/dto/score.dto.ts",
    "content": "import { ApiProperty, ApiResponse } from '@nestjs/swagger';\n\nimport { Student } from '@entities/student';\nimport { User } from '@entities/user';\n\nimport { StudentDto } from 'src/courses/students/dto';\nimport { PaginationMetaDto } from 'src/core/paginate/dto/Paginate.dto';\nimport { PaginationMeta } from 'src/core/paginate';\nimport { Contacts } from '@common/models';\nimport { IsOptional, IsString } from 'class-validator';\n\nclass MentorDto {\n  constructor(mentor: { id: number; githubId: string; name: string }) {\n    this.id = mentor.id;\n    this.githubId = mentor.githubId;\n    this.name = mentor.name;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  githubId: string;\n\n  @ApiProperty()\n  name: string;\n}\n\nclass TaskResultsDto {\n  constructor(task: { courseTaskId: number; score: number }) {\n    this.courseTaskId = task.courseTaskId;\n    this.score = task.score;\n  }\n\n  @ApiProperty()\n  courseTaskId: number;\n\n  @ApiProperty()\n  score: number;\n}\n\nexport class ContactsDto implements Contacts {\n  constructor(contacts: Partial<Contacts>) {\n    this.phone = contacts.phone || null;\n    this.email = contacts.email || null;\n    this.skype = contacts.skype || null;\n    this.whatsApp = contacts.whatsApp || null;\n    this.telegram = contacts.telegram || null;\n    this.notes = contacts.notes || null;\n    this.linkedIn = contacts.linkedIn || null;\n  }\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  phone: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  email: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  epamEmail: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  skype: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  whatsApp: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  telegram: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  notes: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  linkedIn: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  discord: string | null;\n}\n\nexport class ScoreStudentDto extends StudentDto {\n  constructor(\n    student: Student,\n    user: User,\n    mentor:\n      | {\n          id: number;\n          githubId: string;\n          name: string;\n        }\n      | undefined,\n    taskResults: {\n      courseTaskId: number;\n      score: number;\n    }[],\n    contacts: Partial<Contacts> | undefined,\n  ) {\n    super(student);\n    this.mentor = mentor ? new MentorDto(mentor) : undefined;\n    this.githubId = user.githubId;\n    this.totalScoreChangeDate = student.totalScoreChangeDate;\n    this.crossCheckScore = student.crossCheckScore;\n    this.repositoryLastActivityDate = student.repositoryLastActivityDate;\n    this.taskResults = taskResults.map(taskResult => new TaskResultsDto(taskResult));\n    this.isActive = !student.isExpelled && !student.isFailed;\n    this.contacts = contacts && new ContactsDto(contacts);\n  }\n\n  @ApiProperty({ type: MentorDto, nullable: true })\n  mentor: MentorDto | undefined;\n\n  @ApiProperty()\n  githubId: string;\n\n  @ApiProperty({ type: Date })\n  totalScoreChangeDate: Date;\n\n  @ApiProperty({ type: Number })\n  crossCheckScore: number;\n\n  @ApiProperty({ type: Date })\n  repositoryLastActivityDate: Date;\n\n  @ApiProperty({ type: [TaskResultsDto] })\n  taskResults: TaskResultsDto[];\n\n  @ApiProperty({ type: Boolean })\n  isActive: boolean;\n\n  @ApiProperty({ type: ContactsDto })\n  contacts: ContactsDto | undefined;\n}\n\n@ApiResponse({})\nexport class ScoreDto {\n  constructor(students: ScoreStudentDto[], paginationMeta: PaginationMeta) {\n    this.content = students;\n    this.pagination = new PaginationMetaDto(paginationMeta);\n  }\n\n  @ApiProperty({ type: [ScoreStudentDto] })\n  content: ScoreStudentDto[];\n\n  @ApiProperty({ type: PaginationMetaDto })\n  pagination: PaginationMetaDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/score/index.ts",
    "content": "export * from './score.controller';\nexport * from './score.service';\nexport * from './write-score.service';\n"
  },
  {
    "path": "nestjs/src/courses/score/score.controller.ts",
    "content": "import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { Controller, Get, Param, ParseIntPipe, Query, UseGuards, UseInterceptors } from '@nestjs/common';\nimport { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';\n\nimport { CourseGuard, DefaultGuard } from 'src/auth';\nimport { DEFAULT_CACHE_TTL } from 'src/constants';\n\nimport { ScoreQueryDto, OrderDirection, OrderField } from './dto/score-query.dto';\nimport { ScoreService } from './score.service';\nimport { ScoreDto, ScoreStudentDto } from './dto/score.dto';\n\n@Controller('course/:courseId/students/score')\n@ApiTags('students score')\nexport class ScoreController {\n  constructor(private scoreService: ScoreService) {}\n\n  @Get('/')\n  @UseGuards(DefaultGuard, CourseGuard)\n  @ApiOperation({ operationId: 'getScore' })\n  @ApiOkResponse({ type: ScoreDto })\n  @CacheTTL(DEFAULT_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  public async getScore(@Query() query: ScoreQueryDto, @Param('courseId', ParseIntPipe) courseId: number) {\n    const orderBy: OrderField = query.orderBy ?? 'totalScore';\n    const orderDirection: OrderDirection = (query.orderDirection?.toUpperCase() as OrderDirection) ?? 'DESC';\n    const page = parseInt(query.current);\n    const limit = parseInt(query.pageSize);\n\n    const score = await this.scoreService.getScore({\n      courseId,\n      filter: query,\n      orderBy: { field: orderBy, direction: orderDirection },\n      page,\n      limit,\n    });\n\n    return score;\n  }\n\n  @Get('/:githubId')\n  @UseGuards(DefaultGuard, CourseGuard)\n  @ApiOperation({ operationId: 'getStudentScore' })\n  @ApiOkResponse({ type: ScoreStudentDto })\n  public async getStudentScore(@Param('courseId', ParseIntPipe) courseId: number, @Param('githubId') githubId: string) {\n    const studentScore = await this.scoreService.getStudentScore({ githubId, courseId });\n    return studentScore;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/score/score.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { Repository } from 'typeorm';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport * as _ from 'lodash';\n\nimport { Student } from '@entities/student';\n\nimport { paginate } from '../../core/paginate';\nimport { MentorsService } from '../../courses/mentors';\n\nimport { orderByFieldMapping, OrderDirection, OrderField, ScoreQueryDto } from './dto/score-query.dto';\nimport { InterviewsService } from '../interviews';\nimport { ScoreDto, ScoreStudentDto } from './dto/score.dto';\nimport { TaskResult } from '@entities/taskResult';\nimport { UsersService } from 'src/users/users.service';\n\nconst defaultFilter: Partial<ScoreQueryDto> = {\n  activeOnly: 'false',\n  githubId: '',\n  name: '',\n  'mentor.githubId': '',\n  cityName: '',\n};\n\nconst defaultOrder: { field: OrderField; direction: OrderDirection } = {\n  field: 'rank',\n  direction: 'asc',\n};\n\n@Injectable()\nexport class ScoreService {\n  constructor(\n    @InjectRepository(Student)\n    readonly studentRepository: Repository<Student>,\n\n    @InjectRepository(TaskResult)\n    readonly taskResultRepository: Repository<TaskResult>,\n  ) {}\n\n  public async getScore({\n    filter = defaultFilter,\n    orderBy = defaultOrder,\n    page,\n    limit,\n    courseId,\n  }: {\n    filter: Partial<ScoreQueryDto>;\n    orderBy: { field: OrderField; direction: OrderDirection };\n    page: number;\n    limit: number;\n    courseId: number;\n  }) {\n    const query = this.buildScoreQuery({ filter, orderBy, courseId });\n    const { items: studentsContent, meta: paginationMeta } = await paginate(query, { page, limit });\n    const students = studentsContent.map(student => this.convertToScoreStudentDto(student));\n\n    return new ScoreDto(students, paginationMeta);\n  }\n\n  public async getStudentScore({ courseId, githubId }: { courseId: number; githubId: string }) {\n    const studentScoreContent = await this.buildBasicScoreQuery({ courseId })\n      .andWhere('\"user\".\"githubId\" = :githubId', { githubId })\n      .getOne();\n\n    const studentScore = studentScoreContent ? this.convertToScoreStudentDto(studentScoreContent) : null;\n\n    return studentScore;\n  }\n\n  private buildScoreQuery({\n    filter,\n    orderBy,\n    courseId,\n  }: {\n    filter: Partial<ScoreQueryDto>;\n    orderBy: { field: OrderField; direction: OrderDirection };\n    courseId: number;\n  }) {\n    let query = this.buildBasicScoreQuery({ courseId });\n\n    if (filter.activeOnly === 'true') {\n      query = query.andWhere('student.\"isFailed\" = false').andWhere('student.\"isExpelled\" = false');\n    }\n\n    if (filter.name) {\n      query = query.andWhere('(\"user\".\"firstName\" ILIKE :searchText OR \"user\".\"lastName\" ILIKE :searchText)', {\n        searchText: `%${filter.name}%`,\n      });\n    }\n\n    if (filter.cityName) {\n      query = query.andWhere('\"user\".\"cityName\" ILIKE :searchCityNameText', {\n        searchCityNameText: `%${filter.cityName}%`,\n      });\n    }\n\n    if (filter['mentor.githubId']) {\n      query = query.andWhere('\"mu\".\"githubId\" ILIKE :searchMentorGithubIdText', {\n        searchMentorGithubIdText: `%${filter['mentor.githubId']}%`,\n      });\n    }\n\n    if (filter.githubId) {\n      query = query.andWhere('(\"user\".\"githubId\" ILIKE :searchGithubIdText)', {\n        searchGithubIdText: `%${filter.githubId}%`,\n      });\n    }\n\n    return query.orderBy(\n      orderByFieldMapping[orderBy.field],\n      orderBy.direction.toUpperCase() as Uppercase<OrderDirection>,\n    );\n  }\n\n  private buildBasicScoreQuery({ courseId }: { courseId: number }) {\n    return this.studentRepository\n      .createQueryBuilder('student')\n      .innerJoin('student.user', 'user')\n      .addSelect(UsersService.getPrimaryUserFields())\n      .leftJoin('student.mentor', 'mentor', 'mentor.\"isExpelled\" = FALSE')\n      .addSelect(['mentor.id', 'mentor.userId'])\n      .leftJoin('student.taskResults', 'tr')\n      .addSelect(['tr.id', 'tr.score', 'tr.courseTaskId', 'tr.studentId', 'tr.courseTask'])\n      .leftJoin('tr.courseTask', 'ct')\n      .addSelect(['ct.disabled', 'ct.id'])\n      .leftJoin('student.taskInterviewResults', 'tir')\n      .addSelect(['tir.id', 'tir.score', 'tir.courseTaskId', 'tr.studentId', 'tir.updatedDate'])\n      .leftJoin('mentor.user', 'mu')\n      .addSelect(UsersService.getPrimaryUserFields('mu'))\n      .leftJoin('student.stageInterviews', 'si')\n      .leftJoin('si.stageInterviewFeedbacks', 'sif')\n      .addSelect([\n        'sif.stageInterviewId',\n        'sif.json',\n        'sif.updatedDate',\n        'si.isCompleted',\n        'si.id',\n        'si.courseTaskId',\n        'si.score',\n      ])\n      .where('student.\"courseId\" = :courseId', { courseId });\n  }\n\n  private convertToScoreStudentDto(student: Student) {\n    const [preScreeningInterview] = student.stageInterviews ?? [];\n\n    const preScreeningScore = Math.floor(\n      InterviewsService.getLastStageInterview(student.stageInterviews ?? [])?.rating ?? 0,\n    );\n    const preScreeningInterviewWithScore = preScreeningInterview\n      ? { score: preScreeningScore, courseTaskId: preScreeningInterview.courseTaskId }\n      : undefined;\n\n    const user = student.user;\n    const interviews = _.values(_.groupBy(student.taskInterviewResults ?? [], 'courseTaskId'))\n      .map(arr => _.orderBy(arr, 'updatedDate', 'desc')[0]!)\n      .map(({ courseTaskId, score = 0 }) => ({ courseTaskId, score }));\n\n    const taskResults =\n      student.taskResults\n        ?.filter(({ courseTask: { disabled } }) => !disabled)\n        .map(({ courseTaskId, score }) => ({ courseTaskId, score }))\n        .concat(interviews) ?? [];\n\n    // we have a case when technical screening score are set as task result.\n    if (\n      preScreeningInterviewWithScore &&\n      !taskResults.find(tr => tr.courseTaskId === preScreeningInterviewWithScore.courseTaskId)\n    ) {\n      taskResults.push(preScreeningInterviewWithScore);\n    }\n\n    const mentor = student.mentor ? MentorsService.convertMentorToMentorBasic(student.mentor) : undefined;\n    return new ScoreStudentDto(student, user, mentor, taskResults, undefined);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/score/write-score.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { TaskResult } from '@entities/taskResult';\n\nexport type SaveScoreInput = {\n  authorId?: number;\n  score: number;\n  comment: string;\n  githubPrUrl?: string;\n};\n\n@Injectable()\nexport class WriteScoreService {\n  constructor(\n    @InjectRepository(TaskResult)\n    readonly taskResultRepository: Repository<TaskResult>,\n  ) {}\n\n  public async saveScore(\n    studentId: number,\n    courseTaskId: number,\n    data: SaveScoreInput,\n  ): Promise<TaskResult | undefined> {\n    const { authorId = 0, githubPrUrl = null } = data;\n\n    const comment = this.trimComment(data.comment ?? '');\n    const score = Math.round(data.score);\n\n    const current = await this.taskResultRepository.findOne({\n      where: { studentId, courseTaskId },\n    });\n\n    if (current == null) {\n      await this.taskResultRepository.save({\n        courseTaskId,\n        studentId,\n        score: data.score,\n        comment: data.comment,\n        historicalScores: [this.createHistoricalRecord(data)],\n        lastCheckerId: authorId > 0 ? authorId : undefined,\n        githubPrUrl: data.githubPrUrl,\n      });\n      return;\n    }\n\n    // if nothing changed, do nothing\n    if (current.githubRepoUrl === githubPrUrl && current.comment === comment && current.score === score) {\n      return;\n    }\n\n    let previousScore: TaskResult | undefined;\n    if (current.comment !== comment || current.score !== score) {\n      previousScore = { ...current };\n      current.historicalScores.push(this.createHistoricalRecord(data));\n    }\n\n    if (githubPrUrl) {\n      current.githubPrUrl = githubPrUrl;\n    }\n\n    if (comment) {\n      current.comment = comment;\n    }\n\n    if (score !== current.score) {\n      if (authorId > 0) {\n        current.lastCheckerId = authorId;\n      }\n      current.score = score;\n    }\n\n    await this.taskResultRepository.update(current.id, {\n      score: current.score,\n      comment: current.comment,\n      githubPrUrl: current.githubPrUrl,\n      historicalScores: current.historicalScores,\n      lastCheckerId: current.lastCheckerId,\n    });\n    return previousScore;\n  }\n\n  private createHistoricalRecord(data: Pick<SaveScoreInput, 'authorId' | 'comment' | 'score'>) {\n    return {\n      authorId: data.authorId ?? 0,\n      score: data.score,\n      dateTime: Date.now(),\n      comment: data.comment,\n    };\n  }\n\n  private trimComment(comment: string): string {\n    return comment.substring(0, 8 * 1024);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/course-stats.controller.ts",
    "content": "import {\n  Controller,\n  Delete,\n  ForbiddenException,\n  Get,\n  Param,\n  ParseArrayPipe,\n  ParseIntPipe,\n  Query,\n  Req,\n  UseGuards,\n  UseInterceptors,\n} from '@nestjs/common';\nimport { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';\nimport { ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';\nimport { CourseGuard, CurrentRequest, DefaultGuard, RequiredRoles, RoleGuard } from '../../auth';\nimport { ONE_HOUR_CACHE_TTL } from '../../constants';\nimport { CourseAccessService } from '../course-access.service';\nimport { CourseStatsService } from './course-stats.service';\nimport {\n  CourseStatsDto,\n  CountriesStatsDto,\n  CourseMentorsStatsDto,\n  TaskPerformanceStatsDto,\n  CourseAggregateStatsDto,\n} from './dto';\nimport { ExpelledStatsDto } from './dto/expelled-stats.dto';\nimport { ExpelledStatsService } from '../expelled-stats.service';\nimport { CourseRole, Role } from '../../auth/auth-user.model';\n\n@Controller('courses')\n@ApiTags('course stats')\n@UseGuards(DefaultGuard)\nexport class CourseStatsController {\n  constructor(\n    private courseStatsService: CourseStatsService,\n    private courseAccessService: CourseAccessService,\n    private expelledStatsService: ExpelledStatsService,\n  ) {}\n\n  @Get('/stats/expelled')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor])\n  @ApiOperation({ operationId: 'getExpelledStats' })\n  @ApiOkResponse({ type: [ExpelledStatsDto] })\n  public async getExpelledStats() {\n    const data = await this.expelledStatsService.findAll();\n    return data.map(item => new ExpelledStatsDto(item));\n  }\n\n  @Get('/:courseId/stats/expelled')\n  @UseGuards(DefaultGuard, CourseGuard, RoleGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor], true)\n  @ApiOperation({ operationId: 'getCourseExpelledStats' })\n  @ApiOkResponse({ type: [ExpelledStatsDto] })\n  public async getCourseExpelledStats(@Param('courseId', ParseIntPipe) courseId: number) {\n    const data = await this.expelledStatsService.findByCourseId(courseId);\n    return data.map(item => new ExpelledStatsDto(item));\n  }\n\n  @Delete('/stats/expelled/:id')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor])\n  @ApiOperation({ operationId: 'deleteExpelledStat' })\n  @ApiOkResponse({ type: String }) // Assuming it returns a success message or ID\n  public async deleteExpelledStat(@Param('id') id: string) {\n    await this.expelledStatsService.remove(id);\n    return 'Successfully deleted';\n  }\n\n  @Get('/aggregate/stats')\n  @CacheTTL(ONE_HOUR_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @ApiOperation({ operationId: 'getCoursesStats' })\n  @ApiQuery({\n    name: 'ids',\n    required: true,\n    type: [Number],\n    description: 'List of course IDs',\n  })\n  @ApiQuery({\n    name: 'year',\n    required: true,\n    type: Number,\n    description: 'Year for which stats are fetched',\n  })\n  @ApiOkResponse({ type: CourseAggregateStatsDto })\n  public async getCoursesStats(\n    @Req() req: CurrentRequest,\n    @Query('ids', new ParseArrayPipe({ items: Number })) ids: number[],\n    @Query('year', ParseIntPipe) year: number,\n  ) {\n    const allowedCourseIds = await this.courseAccessService.getUserAllowedCourseIds(req.user, ids, year);\n    const data = await this.courseStatsService.getCoursesStats(allowedCourseIds);\n    return new CourseAggregateStatsDto(data);\n  }\n\n  @Get('/:courseId/stats')\n  @CacheTTL(ONE_HOUR_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @ApiOperation({ operationId: 'getCourseStats' })\n  @ApiOkResponse({ type: CourseStatsDto })\n  public async getCourses(@Req() req: CurrentRequest, @Param('courseId', ParseIntPipe) courseId: number) {\n    if (!this.courseAccessService.canAccessCourse(req.user, courseId)) {\n      throw new ForbiddenException();\n    }\n    const data = await this.courseStatsService.getStudents(courseId);\n    return new CourseStatsDto(data);\n  }\n\n  @Get('/:courseId/stats/mentors')\n  @CacheTTL(ONE_HOUR_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @UseGuards(DefaultGuard, CourseGuard)\n  @ApiOperation({ operationId: 'getCourseMentors' })\n  @ApiOkResponse({ type: CourseMentorsStatsDto })\n  @ApiBadRequestResponse()\n  public async getMentors(@Param('courseId', ParseIntPipe) courseId: number) {\n    const data = await this.courseStatsService.getMentors(courseId);\n    return new CourseMentorsStatsDto(data);\n  }\n\n  @Get('/:courseId/stats/mentors/countries')\n  @CacheTTL(ONE_HOUR_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @UseGuards(DefaultGuard, CourseGuard)\n  @ApiOperation({ operationId: 'getCourseMentorCountries' })\n  @ApiOkResponse({ type: CountriesStatsDto })\n  @ApiBadRequestResponse()\n  public async getMentorCountries(@Param('courseId', ParseIntPipe) courseId: number): Promise<CountriesStatsDto> {\n    const data = await this.courseStatsService.getMentorCountries(courseId);\n    return data;\n  }\n\n  @Get('/:courseId/stats/students/countries')\n  @CacheTTL(ONE_HOUR_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @UseGuards(DefaultGuard, CourseGuard)\n  @ApiOperation({ operationId: 'getCourseStudentCountries' })\n  @ApiOkResponse({ type: CountriesStatsDto })\n  @ApiBadRequestResponse()\n  public async getStudentCountries(@Param('courseId', ParseIntPipe) courseId: number): Promise<CountriesStatsDto> {\n    const data = await this.courseStatsService.getStudentCountries(courseId);\n    return data;\n  }\n\n  @Get('/:courseId/stats/students/certificates/countries')\n  @CacheTTL(ONE_HOUR_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @UseGuards(DefaultGuard, CourseGuard)\n  @ApiOperation({ operationId: 'getCourseStudentCertificatesCountries' })\n  @ApiOkResponse({ type: CountriesStatsDto })\n  @ApiBadRequestResponse()\n  public async getStudentsWithCertificatesCountries(\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ): Promise<CountriesStatsDto> {\n    const data = await this.courseStatsService.getStudentsWithCertificatesCountries(courseId);\n    return data;\n  }\n\n  @Get('/:courseId/stats/task/:taskId/performance')\n  @CacheTTL(ONE_HOUR_CACHE_TTL)\n  @UseInterceptors(CacheInterceptor)\n  @UseGuards(DefaultGuard, CourseGuard)\n  @ApiOperation({ operationId: 'getTaskPerformance' })\n  @ApiOkResponse({ type: TaskPerformanceStatsDto })\n  @ApiBadRequestResponse()\n  public async getTaskPerformance(\n    @Param('courseId', ParseIntPipe) _courseId: number,\n    @Param('taskId', ParseIntPipe) taskId: number,\n  ) {\n    const stats = await this.courseStatsService.getTaskPerformance(taskId);\n    return new TaskPerformanceStatsDto(stats);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/course-stats.service.ts",
    "content": "import { Student } from '@entities/student';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CountriesStatsDto, CountryStatDto } from './dto';\nimport { Certificate, CourseTask, Mentor, StageInterview, TaskInterviewResult, TaskResult } from '@entities/index';\nimport { TaskType } from '@entities/task';\nimport { CourseTasksService } from '../course-tasks';\nimport { CourseTaskDto } from '../course-tasks/dto';\n\n@Injectable()\nexport class CourseStatsService {\n  constructor(\n    private taskService: CourseTasksService,\n    @InjectRepository(Student)\n    private readonly studentRepository: Repository<Student>,\n    @InjectRepository(Mentor)\n    private readonly mentorRepository: Repository<Mentor>,\n    @InjectRepository(CourseTask)\n    readonly courseTaskRepository: Repository<CourseTask>,\n    @InjectRepository(TaskResult)\n    readonly taskResultRepository: Repository<TaskResult>,\n    @InjectRepository(TaskInterviewResult)\n    readonly taskInterviewResultRepository: Repository<TaskInterviewResult>,\n    @InjectRepository(StageInterview)\n    readonly stageInterviewRepository: Repository<StageInterview>,\n  ) {}\n\n  private async getMaxScore(courseId: number): Promise<number> {\n    const { maxScore } = await this.studentRepository\n      .createQueryBuilder('student')\n      .select('MAX(student.totalScore)', 'maxScore')\n      .where('student.courseId = :courseId', { courseId })\n      .getRawOne();\n\n    return Number(maxScore);\n  }\n\n  public async getStudents(courseId: number) {\n    const maxScore = await this.getMaxScore(courseId);\n\n    const queryBuilder = this.studentRepository\n      .createQueryBuilder('student')\n      .leftJoinAndSelect('student.user', 'user')\n      .leftJoin(Certificate, 'certificate', '\"certificate\".\"studentId\" = \"student\".\"id\"')\n      .leftJoin('student.course', 'course')\n      .select('COUNT(*)', 'total_students')\n      .addSelect(\n        'COUNT(CASE WHEN student.isExpelled = false AND student.isFailed = false THEN 1 END)',\n        'active_students',\n      )\n      .addSelect(\n        'COUNT(CASE WHEN student.isExpelled = false AND student.isFailed = false AND student.mentorId IS NOT NULL THEN 1 END)',\n        'students_with_mentor',\n      )\n      .addSelect(\n        'COUNT(DISTINCT CASE WHEN certificate.publicId IS NOT NULL THEN student.id END)',\n        'students_with_certificate',\n      )\n      .addSelect(\n        `COUNT(CASE WHEN student.isExpelled = false AND student.totalScore >= (${maxScore} * course.certificateThreshold / 100) THEN 1 END)`,\n        'eligible_for_certification',\n      )\n      .where('student.courseId = :courseId', { courseId });\n\n    const result = await queryBuilder.getRawOne();\n\n    return {\n      totalStudents: Number(result.total_students),\n      activeStudentsCount: Number(result.active_students),\n      studentsWithMentorCount: Number(result.students_with_mentor),\n      certifiedStudentsCount: Number(result.students_with_certificate),\n      eligibleForCertificationCount: Number(result.eligible_for_certification),\n    };\n  }\n\n  public async getMentors(courseId: number) {\n    const queryBuilder = this.mentorRepository\n      .createQueryBuilder('mentor')\n      .leftJoinAndSelect('mentor.user', 'user')\n      .select('COUNT(*)', 'total_mentors')\n      .addSelect('COUNT(CASE WHEN mentor.isExpelled = false THEN 1 END)', 'active_mentors')\n      .addSelect(\n        \"COUNT(DISTINCT CASE WHEN user.contactsEpamEmail IS NOT NULL AND user.contactsEpamEmail != '' THEN mentor.userId END)\",\n        'mentors_with_email',\n      )\n      .where('mentor.courseId = :courseId', { courseId });\n\n    const result = await queryBuilder.getRawOne();\n\n    return {\n      mentorsTotalCount: Number(result.total_mentors),\n      mentorsActiveCount: Number(result.active_mentors),\n      epamMentorsCount: Number(result.mentors_with_email),\n    };\n  }\n\n  public async getStudentCounts(courseId: number): Promise<{ activeStudentsCount: number; totalStudents: number }> {\n    const totalStudents = await this.studentRepository.count({ where: { courseId } });\n    const activeStudentsCount = await this.studentRepository.count({\n      where: { courseId, isExpelled: false, isFailed: false },\n    });\n\n    return { activeStudentsCount, totalStudents };\n  }\n\n  public async getMentorCountries(courseId: number): Promise<{ countries: CountryStatDto[] }> {\n    return this.getCountries(courseId, this.mentorRepository);\n  }\n\n  public async getStudentCountries(courseId: number): Promise<{ countries: CountryStatDto[] }> {\n    return this.getCountries(courseId, this.studentRepository);\n  }\n\n  public async getStudentsWithCertificatesCountries(courseId: number): Promise<{ countries: CountryStatDto[] }> {\n    const countries = await this.studentRepository\n      .createQueryBuilder('student')\n      .leftJoin('student.user', 'user')\n      .leftJoin(Certificate, 'certificate', 'certificate.studentId = student.id')\n      .select('user.countryName', 'countryName')\n      .addSelect('COUNT(DISTINCT student.id)', 'count')\n      .where('student.courseId = :courseId', { courseId })\n      .andWhere('certificate.publicId IS NOT NULL')\n      .groupBy('user.countryName')\n      .orderBy('COUNT(DISTINCT student.id)', 'DESC')\n      .getRawMany<CountryStatDto>();\n\n    return {\n      countries: countries.map(country => ({\n        countryName: country.countryName,\n        count: Number(country.count),\n      })),\n    };\n  }\n\n  private async getCountries(\n    courseId: number,\n    repository: Repository<Mentor | Student>,\n  ): Promise<{ countries: CountryStatDto[] }> {\n    const countries = await repository\n      .createQueryBuilder('role')\n      .where('role.courseId = :courseId', { courseId })\n      .andWhere('role.isExpelled = false')\n      .leftJoin('role.user', 'user')\n      .select('user.countryName', 'countryName')\n      .addSelect('COUNT(role.id)', 'count')\n      .groupBy('user.countryName')\n      .orderBy('COUNT(role.id)', 'DESC')\n      .getRawMany<CountryStatDto>();\n\n    return {\n      countries: countries.map(country => ({\n        countryName: country.countryName,\n        count: Number(country.count),\n      })),\n    };\n  }\n\n  private getResultRepositoryByTaskType(taskType: TaskType) {\n    switch (taskType) {\n      case 'interview':\n        return this.taskInterviewResultRepository;\n      case 'stage-interview':\n        return this.stageInterviewRepository;\n      default:\n        return this.taskResultRepository;\n    }\n  }\n\n  public async getTaskPerformance(courseTaskId: number) {\n    const courseTask = await this.courseTaskRepository.findOneOrFail({\n      where: { id: courseTaskId },\n      relations: ['task'],\n    });\n    const resultRepository = this.getResultRepositoryByTaskType(courseTask.task.type);\n\n    const ranges = [\n      { key: 'minimalAchievement', minScore: 0, maxScore: 0.2 },\n      { key: 'lowAchievement', minScore: 0.2, maxScore: 0.5 },\n      { key: 'moderateAchievement', minScore: 0.5, maxScore: 0.7 },\n      { key: 'highAchievement', minScore: 0.7, maxScore: 0.9 },\n      { key: 'exceptionalAchievement', minScore: 0.9, maxScore: 1.0 },\n    ];\n\n    const query = await resultRepository\n      .createQueryBuilder('result')\n      .select('COUNT(CASE WHEN result.score > 0 THEN 1 END)', 'totalAchievement')\n      .addSelect(`COUNT(CASE WHEN result.score = ${courseTask.maxScore} THEN 1 END)`, 'perfectScores');\n\n    ranges.forEach(({ key, minScore, maxScore }) => {\n      query.addSelect(\n        `COUNT(CASE WHEN result.score / CAST(${courseTask.maxScore} AS float) >= ${minScore} AND result.score / CAST(${courseTask.maxScore} AS float) < ${maxScore} THEN 1 END)`,\n        key,\n      );\n    });\n\n    const performanceStats = await query.where('result.courseTaskId = :courseTaskId', { courseTaskId }).getRawOne();\n\n    return {\n      totalAchievement: Number(performanceStats.totalAchievement),\n      minimalAchievement: Number(performanceStats.minimalAchievement),\n      lowAchievement: Number(performanceStats.lowAchievement),\n      moderateAchievement: Number(performanceStats.moderateAchievement),\n      highAchievement: Number(performanceStats.highAchievement),\n      exceptionalAchievement: Number(performanceStats.exceptionalAchievement),\n      perfectScores: Number(performanceStats.perfectScores),\n    };\n  }\n\n  private mergeCountries(data: CountriesStatsDto[]): CountriesStatsDto {\n    const countries = data\n      .map(item => item.countries)\n      .flat()\n      .filter(el => !!el.countryName);\n\n    const count = countries.reduce<Record<string, number>>((acc, el) => {\n      const country = el.countryName;\n      if (acc[country]) {\n        acc[country] += el.count;\n      } else {\n        acc[country] = el.count;\n      }\n      return acc;\n    }, {});\n\n    const result: { countryName: string; count: number }[] = [];\n\n    for (const key in count) {\n      result.push({\n        countryName: key,\n        count: count[key] || 0,\n      });\n    }\n\n    return { countries: result } as CountriesStatsDto;\n  }\n\n  private mergeStats<T extends Record<string, number>>(data: T[]): T {\n    const result = data.reduce<Record<string, number>>((acc, el) => {\n      for (const key in el) {\n        const value = el[key] || 0;\n        if (acc[key]) {\n          acc[key] = acc[key] + value;\n        } else {\n          acc[key] = value;\n        }\n      }\n      return acc;\n    }, {});\n\n    return result as T;\n  }\n\n  public async getCoursesStats(ids: number[] = []) {\n    const [\n      studentsStatsResolved,\n      studentsCountriesResolved,\n      mentorsCountriesResolved,\n      mentorsStatsResolved,\n      courseTasksResolved,\n      studentsCertificatesCountriesResolved,\n    ] = await Promise.all([\n      Promise.all(ids.map(courseId => this.getStudents(courseId))),\n      Promise.all(ids.map(courseId => this.getStudentCountries(courseId))),\n      Promise.all(ids.map(courseId => this.getMentorCountries(courseId))),\n      Promise.all(ids.map(courseId => this.getMentors(courseId))),\n      Promise.all(ids.map(courseId => this.taskService.getAll(courseId, undefined, false))),\n      Promise.all(ids.map(courseId => this.getStudentsWithCertificatesCountries(courseId))),\n    ]);\n\n    return {\n      studentsCountries: this.mergeCountries(studentsCountriesResolved),\n      studentsStats: this.mergeStats(studentsStatsResolved),\n      mentorsCountries: this.mergeCountries(mentorsCountriesResolved),\n      mentorsStats: this.mergeStats(mentorsStatsResolved),\n      courseTasks: courseTasksResolved.flat().map(item => new CourseTaskDto(item)),\n      studentsCertificatesCountries: this.mergeCountries(studentsCertificatesCountriesResolved),\n    };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/dto/countries-stats.dto.ts",
    "content": "import { IsString, IsInt, IsArray, ValidateNested } from 'class-validator';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class CountryStatDto {\n  @IsString()\n  @ApiProperty()\n  public countryName: string;\n\n  @IsInt()\n  @ApiProperty()\n  public count: number;\n}\n\nexport class CountriesStatsDto {\n  @IsArray()\n  @ValidateNested({ each: true })\n  @ApiProperty({ type: [CountryStatDto] })\n  public countries: CountryStatDto[];\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/dto/course-mentors-stats.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class CourseMentorsStatsDto {\n  constructor(stats: { mentorsActiveCount: number; mentorsTotalCount: number; epamMentorsCount: number }) {\n    this.mentorsActiveCount = stats.mentorsActiveCount;\n    this.mentorsTotalCount = stats.mentorsTotalCount;\n    this.epamMentorsCount = stats.epamMentorsCount;\n  }\n\n  @ApiProperty()\n  mentorsActiveCount: number;\n\n  @ApiProperty()\n  mentorsTotalCount: number;\n\n  @ApiProperty()\n  epamMentorsCount: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/dto/course-stats.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { CountriesStatsDto } from './countries-stats.dto';\nimport { CourseMentorsStatsDto } from './course-mentors-stats.dto';\nimport { CourseTaskDto } from '../../course-tasks/dto';\n\nexport class CourseStatsDto {\n  constructor(stats: {\n    activeStudentsCount: number;\n    totalStudents: number;\n    studentsWithMentorCount: number;\n    certifiedStudentsCount: number;\n    eligibleForCertificationCount: number;\n  }) {\n    this.activeStudentsCount = stats.activeStudentsCount;\n    this.totalStudents = stats.totalStudents;\n    this.studentsWithMentorCount = stats.studentsWithMentorCount;\n    this.certifiedStudentsCount = stats.certifiedStudentsCount;\n    this.eligibleForCertificationCount = stats.eligibleForCertificationCount;\n  }\n\n  @ApiProperty()\n  activeStudentsCount: number;\n\n  @ApiProperty()\n  totalStudents: number;\n\n  @ApiProperty()\n  studentsWithMentorCount: number;\n\n  @ApiProperty()\n  certifiedStudentsCount: number;\n\n  @ApiProperty()\n  eligibleForCertificationCount: number;\n}\n\nexport class CourseAggregateStatsDto {\n  constructor(stats: {\n    studentsCountries: CountriesStatsDto;\n    studentsStats: CourseStatsDto;\n    mentorsCountries: CountriesStatsDto;\n    mentorsStats: CourseMentorsStatsDto;\n    courseTasks: CourseTaskDto[];\n    studentsCertificatesCountries: CountriesStatsDto;\n  }) {\n    this.studentsCountries = stats.studentsCountries;\n    this.studentsStats = stats.studentsStats;\n    this.mentorsCountries = stats.mentorsCountries;\n    this.mentorsStats = stats.mentorsStats;\n    this.courseTasks = stats.courseTasks;\n    this.studentsCertificatesCountries = stats.studentsCertificatesCountries;\n  }\n\n  @ApiProperty()\n  studentsCountries: CountriesStatsDto;\n\n  @ApiProperty()\n  studentsStats: CourseStatsDto;\n\n  @ApiProperty()\n  mentorsCountries: CountriesStatsDto;\n\n  @ApiProperty()\n  mentorsStats: CourseMentorsStatsDto;\n\n  @ApiProperty({ type: () => [CourseTaskDto] })\n  courseTasks: CourseTaskDto[];\n\n  @ApiProperty()\n  studentsCertificatesCountries: CountriesStatsDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/dto/expelled-stats.dto.ts",
    "content": "import { CourseLeaveSurveyResponse } from '@entities/index';\nimport { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { CourseDto } from 'src/courses/dto';\nimport { UserDto } from 'src/users/dto';\nimport { UsersService } from 'src/users/users.service';\n\nexport class ExpelledStatsDto {\n  constructor(data: CourseLeaveSurveyResponse) {\n    this.id = data.id;\n    this.course = new CourseDto(data.course);\n    this.user = new UserDto({\n      id: data.user.id,\n      githubId: data.user.githubId,\n      name: UsersService.getFullName(data.user),\n    });\n    this.reasonForLeaving = data.reasonForLeaving;\n    this.otherComment = data.otherComment ?? '';\n    this.submittedAt = data.submittedAt.toISOString();\n  }\n\n  @ApiProperty()\n  id: string;\n\n  @ApiProperty({ type: () => CourseDto })\n  course: CourseDto;\n\n  @ApiProperty({ type: () => UserDto })\n  user: UserDto;\n\n  @ApiPropertyOptional({ type: [String] })\n  reasonForLeaving?: string[];\n\n  @ApiProperty()\n  otherComment: string;\n\n  @ApiProperty({ format: 'date-time' })\n  submittedAt: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/dto/index.ts",
    "content": "export * from './course-stats.dto';\nexport * from './countries-stats.dto';\nexport * from './course-mentors-stats.dto';\nexport * from './task-performance-stats.dto';\n"
  },
  {
    "path": "nestjs/src/courses/stats/dto/task-performance-stats.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class TaskPerformanceStatsDto {\n  constructor(stats: {\n    totalAchievement: number;\n    minimalAchievement: number;\n    lowAchievement: number;\n    moderateAchievement: number;\n    highAchievement: number;\n    exceptionalAchievement: number;\n    perfectScores: number;\n  }) {\n    this.totalAchievement = stats.totalAchievement;\n    this.minimalAchievement = stats.minimalAchievement;\n    this.lowAchievement = stats.lowAchievement;\n    this.moderateAchievement = stats.moderateAchievement;\n    this.highAchievement = stats.highAchievement;\n    this.exceptionalAchievement = stats.exceptionalAchievement;\n    this.perfectScores = stats.perfectScores;\n  }\n\n  @ApiProperty({ description: 'Total number of students who submitted the task' })\n  totalAchievement: number;\n\n  @ApiProperty({ description: 'Number of students scoring between 1% and 20% of the maximum points' })\n  minimalAchievement: number;\n\n  @ApiProperty({ description: 'Number of students scoring between 21% and 50% of the maximum points' })\n  lowAchievement: number;\n\n  @ApiProperty({ description: 'Number of students scoring between 51% and 70% of the maximum points' })\n  moderateAchievement: number;\n\n  @ApiProperty({ description: 'Number of students scoring between 71% and 90% of the maximum points' })\n  highAchievement: number;\n\n  @ApiProperty({ description: 'Number of students scoring between 91% and 99% of the maximum points' })\n  exceptionalAchievement: number;\n\n  @ApiProperty({ description: 'Number of students achieving a perfect score of 100%' })\n  perfectScores: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/stats/index.ts",
    "content": "export * from './course-stats.service';\nexport * from './course-stats.controller';\n"
  },
  {
    "path": "nestjs/src/courses/students/dto/index.ts",
    "content": "export * from './student.dto';\nexport * from './user-students.dto';\nexport * from './user-students-query.dto';\n"
  },
  {
    "path": "nestjs/src/courses/students/dto/student.dto.ts",
    "content": "import { Student } from '@entities/student';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsNotEmpty } from 'class-validator';\nimport { PersonDto } from '../../../core/dto';\n\n@ApiResponse({})\nexport class StudentDto extends PersonDto {\n  constructor(student: Student) {\n    super({\n      id: student.id,\n      firstName: student.user.firstName,\n      lastName: student.user.lastName,\n      githubId: student.user.githubId,\n    });\n    this.active = !student.isExpelled;\n    this.cityName = student.user.cityName ?? null;\n    this.countryName = student.user.countryName ?? null;\n    this.rank = student.rank;\n    this.totalScore = student.totalScore;\n  }\n\n  @IsNotEmpty()\n  @ApiProperty()\n  active: boolean;\n\n  @ApiProperty({ type: String, nullable: true })\n  cityName: string | null;\n\n  @ApiProperty({ type: String, nullable: true })\n  countryName: string | null;\n\n  @ApiProperty()\n  totalScore: number;\n\n  @ApiProperty()\n  rank: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/dto/user-students-query.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsString, IsOptional } from 'class-validator';\n\nexport class UserStudentsQueryDto {\n  @ApiProperty()\n  @IsString()\n  public current: string;\n\n  @ApiProperty()\n  @IsString()\n  public pageSize: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  student?: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  country?: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  city?: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  ongoingCourses?: string;\n\n  @ApiPropertyOptional()\n  @IsString()\n  @IsOptional()\n  previousCourses?: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/dto/user-students.dto.ts",
    "content": "import { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { PaginationMeta } from 'src/core/paginate';\nimport { PaginationMetaDto } from 'src/core/paginate/dto/Paginate.dto';\nimport { Student, User } from '@entities/index';\nimport { PersonDto } from 'src/core/dto';\nimport { Discord } from 'src/profile/dto';\n\nclass UserStudentCourseDto {\n  constructor(student: Student) {\n    this.alias = student.course.alias;\n    this.name = student.course.name;\n    this.completed = student.course.completed;\n    this.studentIsExpelled = student.isExpelled;\n    this.certificateId = student.certificate?.publicId;\n    this.hasCertificate = Boolean(student.certificate?.publicId);\n    this.mentorGithubId = student.mentor?.user.githubId;\n    this.mentorFullName = PersonDto.getName({\n      firstName: student.mentor?.user.firstName,\n      lastName: student.mentor?.user.lastName,\n    });\n    this.totalScore = student.totalScore;\n    this.rank = student.rank;\n  }\n\n  @ApiProperty()\n  alias: string;\n\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  hasCertificate: boolean;\n\n  @ApiProperty()\n  completed: boolean;\n\n  @ApiProperty()\n  studentIsExpelled: boolean;\n\n  @ApiProperty()\n  certificateId?: string;\n\n  @ApiProperty()\n  mentorGithubId?: string;\n\n  @ApiProperty()\n  mentorFullName?: string;\n\n  @ApiProperty()\n  totalScore: number;\n\n  @ApiProperty()\n  rank: number;\n}\n\nclass UserStudentDto {\n  constructor(user: User) {\n    this.id = user.id;\n    this.githubId = user.githubId;\n    this.fullName = PersonDto.getName({ firstName: user.firstName, lastName: user.lastName });\n    this.country = user.countryName;\n    this.city = user.cityName;\n    this.languages = user.languages;\n    this.contactsEmail = user.contactsEmail ?? undefined;\n    this.contactsTelegram = user.contactsTelegram ?? undefined;\n    this.contactsLinkedIn = user.contactsLinkedIn ?? undefined;\n    this.contactsSkype = user.contactsSkype ?? undefined;\n    this.contactsPhone = user.contactsPhone ?? undefined;\n    this.discord = user.discord ?? undefined;\n\n    this.onGoingCourses =\n      user.students?.filter(student => !student.course.completed)?.map(student => new UserStudentCourseDto(student)) ??\n      [];\n    this.previousCourses =\n      user.students?.filter(student => student.course.completed)?.map(student => new UserStudentCourseDto(student)) ??\n      [];\n  }\n\n  @ApiProperty({ description: 'User id' })\n  id: number;\n\n  @ApiProperty({ description: 'User github id' })\n  githubId: string;\n\n  @ApiProperty({ description: 'User full name' })\n  fullName: string;\n\n  @ApiProperty({ description: 'User country' })\n  country: string | null;\n\n  @ApiProperty({ description: 'User city' })\n  city: string | null;\n\n  @ApiProperty({ description: 'User email' })\n  contactsEmail?: string;\n\n  @ApiProperty({ description: 'User telegram' })\n  contactsTelegram?: string;\n\n  @ApiProperty({ description: 'User linkedIn' })\n  contactsLinkedIn?: string;\n\n  @ApiProperty({ description: 'User skype' })\n  contactsSkype?: string;\n\n  @ApiProperty({ description: 'User phone' })\n  contactsPhone?: string;\n\n  @ApiProperty({ description: 'User discord', type: Discord })\n  discord?: Discord;\n\n  @ApiProperty({ description: 'User on going courses', type: [UserStudentCourseDto] })\n  onGoingCourses: UserStudentCourseDto[];\n\n  @ApiProperty({ description: 'User previous courses', type: [UserStudentCourseDto] })\n  previousCourses: UserStudentCourseDto[];\n\n  @ApiProperty({ description: 'User languages' })\n  languages: string[];\n}\n\n@ApiResponse({})\nexport class UserStudentsDto {\n  constructor(data: { items: User[]; meta: PaginationMeta }) {\n    this.content = data.items.map(user => new UserStudentDto(user));\n    this.pagination = new PaginationMetaDto(data.meta);\n  }\n\n  @ApiProperty({ type: [UserStudentDto] })\n  content: UserStudentDto[];\n\n  @ApiProperty({ type: PaginationMetaDto })\n  pagination: PaginationMetaDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/feedbacks/dto/create-student-feedback.dto.ts",
    "content": "import { Rate, Recommendation, SoftSkill, StudentFeedbackContent } from '@entities/student-feedback';\nimport { LanguageLevel } from '@entities/data';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { IsArray, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';\n\nclass SoftSkillEntry {\n  @ApiProperty({ enum: SoftSkill })\n  @IsEnum(SoftSkill)\n  @IsNotEmpty()\n  id: SoftSkill;\n\n  @ApiProperty({ enum: Rate })\n  @IsNotEmpty()\n  value: Rate;\n}\n\nexport class StudentFeedbackContentDto implements StudentFeedbackContent {\n  @IsString()\n  @ApiProperty()\n  suggestions: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  recommendationComment: string;\n\n  @ApiProperty({ type: [SoftSkillEntry] })\n  @IsArray()\n  softSkills: SoftSkillEntry[];\n}\n\nexport class CreateStudentFeedbackDto {\n  @ValidateNested()\n  @IsObject()\n  @Type(() => StudentFeedbackContentDto)\n  @ApiProperty({ type: StudentFeedbackContentDto })\n  content: StudentFeedbackContentDto;\n\n  @IsEnum(Recommendation)\n  @IsNotEmpty()\n  @ApiProperty({ enum: Recommendation })\n  recommendation: Recommendation;\n\n  @IsEnum(LanguageLevel)\n  @IsOptional()\n  @ApiProperty({ enum: LanguageLevel })\n  englishLevel: LanguageLevel;\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/feedbacks/dto/index.ts",
    "content": "export * from './create-student-feedback.dto';\nexport * from './student-feedback.dto';\nexport * from './update-student-feedback.dto';\n"
  },
  {
    "path": "nestjs/src/courses/students/feedbacks/dto/student-feedback.dto.ts",
    "content": "import { Recommendation, StudentFeedback } from '@entities/student-feedback';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { LanguageLevel } from '@entities/data';\nimport { PersonDto } from '../../../../core/dto';\nimport { StudentFeedbackContentDto } from './create-student-feedback.dto';\n\n@ApiResponse({})\nexport class StudentFeedbackDto {\n  constructor(studentFeedback: StudentFeedback) {\n    this.content = studentFeedback.content;\n    this.recommendation = studentFeedback.recommendation;\n    this.author = new PersonDto(studentFeedback.author);\n    this.mentor = studentFeedback.mentor\n      ? new PersonDto({ ...studentFeedback.mentor.user, id: studentFeedback.mentor.id })\n      : null;\n    this.id = studentFeedback.id;\n    this.createdDate = studentFeedback.createdDate;\n    this.updatedDate = studentFeedback.updatedDate;\n    this.englishLevel = studentFeedback.englishLevel ?? LanguageLevel.Unkwown;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  createdDate: string;\n\n  @ApiProperty()\n  updatedDate: string;\n\n  @ApiProperty({ type: StudentFeedbackContentDto })\n  content: StudentFeedbackContentDto;\n\n  @ApiProperty({ enum: Recommendation })\n  recommendation: Recommendation;\n\n  @ApiProperty({ type: PersonDto })\n  author: PersonDto;\n\n  @ApiProperty({ type: PersonDto, nullable: true })\n  mentor: PersonDto | null;\n\n  @ApiProperty({ enum: LanguageLevel })\n  englishLevel: LanguageLevel;\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/feedbacks/dto/update-student-feedback.dto.ts",
    "content": "import { Recommendation } from '@entities/student-feedback';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsEnum, IsNotEmptyObject, IsOptional } from 'class-validator';\nimport { LanguageLevel } from '@entities/data';\nimport { StudentFeedbackContentDto } from './create-student-feedback.dto';\n\n@ApiResponse({})\nexport class UpdateStudentFeedbackDto {\n  @IsNotEmptyObject()\n  @IsOptional()\n  @ApiProperty({ type: StudentFeedbackContentDto })\n  content: StudentFeedbackContentDto;\n\n  @IsEnum(Recommendation)\n  @IsOptional()\n  @ApiProperty({ enum: Recommendation })\n  recommendation: Recommendation;\n\n  @IsEnum(LanguageLevel)\n  @IsOptional()\n  @ApiProperty({ enum: LanguageLevel })\n  englishLevel: LanguageLevel;\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/feedbacks/feedbacks.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  ForbiddenException,\n  Get,\n  Param,\n  ParseIntPipe,\n  Patch,\n  Post,\n  Req,\n  UseGuards,\n} from '@nestjs/common';\nimport { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CurrentRequest, DefaultGuard } from 'src/auth';\nimport { StudentsService } from '../students.service';\nimport { CreateStudentFeedbackDto, StudentFeedbackDto, UpdateStudentFeedbackDto } from './dto';\nimport { FeedbacksService } from './feedbacks.service';\n\n@Controller('students/:studentId/feedbacks')\n@ApiTags('students feedbacks')\n@UseGuards(DefaultGuard)\nexport class FeedbacksController {\n  constructor(\n    private stundentsService: StudentsService,\n    private feedbackService: FeedbacksService,\n  ) {}\n\n  @Post('/')\n  @ApiOperation({ operationId: 'createStudentFeedback' })\n  @ApiCreatedResponse({ type: StudentFeedbackDto })\n  public async createStudentFeedback(\n    @Param('studentId', ParseIntPipe) studentId: number,\n    @Body() body: CreateStudentFeedbackDto,\n    @Req() req: CurrentRequest,\n  ) {\n    const hasAccess = await this.stundentsService.canAccessStudent(req.user, studentId);\n    if (!hasAccess) {\n      throw new ForbiddenException();\n    }\n    const feedback = await this.feedbackService.createStudentFeedback(studentId, body, req.user.id);\n    return new StudentFeedbackDto(feedback);\n  }\n\n  @Patch('/:id')\n  @ApiOperation({ operationId: 'updateStudentFeedback' })\n  @ApiOkResponse({ type: StudentFeedbackDto })\n  public async updateStudentFeedback(\n    @Param('studentId', ParseIntPipe) studentId: number,\n    @Param('id', ParseIntPipe) id: number,\n    @Body() body: UpdateStudentFeedbackDto,\n    @Req() req: CurrentRequest,\n  ) {\n    const hasAccess = await this.stundentsService.canAccessStudent(req.user, studentId);\n    if (!hasAccess) {\n      throw new ForbiddenException();\n    }\n    const feedback = await this.feedbackService.update(id, body);\n    return new StudentFeedbackDto(feedback);\n  }\n\n  @Get('/:id')\n  @ApiOperation({ operationId: 'getStudentFeedback' })\n  @ApiOkResponse({ type: StudentFeedbackDto })\n  public async getStudentFeedback(\n    @Param('studentId', ParseIntPipe) studentId: number,\n    @Param('id', ParseIntPipe) id: number,\n    @Req() req: CurrentRequest,\n  ) {\n    const hasAccess = await this.stundentsService.canAccessStudent(req.user, studentId);\n    if (!hasAccess) {\n      throw new ForbiddenException();\n    }\n    const feedback = await this.feedbackService.getById(id);\n    if (feedback.studentId !== studentId) {\n      throw new ForbiddenException();\n    }\n    return new StudentFeedbackDto(feedback);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/feedbacks/feedbacks.service.ts",
    "content": "import { Mentor } from '@entities/mentor';\nimport { Student } from '@entities/student';\nimport { StudentFeedback } from '@entities/student-feedback';\nimport { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CreateStudentFeedbackDto, UpdateStudentFeedbackDto } from './dto';\nimport { PersonDto } from '../../../core/dto';\n\n@Injectable()\nexport class FeedbacksService {\n  constructor(\n    @InjectRepository(StudentFeedback)\n    private readonly studentFeedbacksRepository: Repository<StudentFeedback>,\n    @InjectRepository(Student)\n    private readonly studentsRepository: Repository<Student>,\n    @InjectRepository(Mentor)\n    private readonly mentorsRepository: Repository<Mentor>,\n  ) {}\n\n  public async createStudentFeedback(\n    studentId: number,\n    feedback: CreateStudentFeedbackDto,\n    authorId: number,\n  ): Promise<StudentFeedback> {\n    const student = await this.studentsRepository.findOneByOrFail({ id: studentId });\n    const mentor = await this.mentorsRepository.findOneByOrFail({ userId: authorId, courseId: student.courseId });\n    const current = await this.getByStudentAndMentor(student.id, mentor.id);\n\n    if (current) {\n      throw new BadRequestException('Feedback already exists');\n    }\n\n    const { id } = await this.studentFeedbacksRepository.save<Partial<StudentFeedback>>({\n      studentId: student.id,\n      mentorId: mentor?.id,\n      auhtorId: authorId,\n      content: feedback.content,\n      recommendation: feedback.recommendation,\n      englishLevel: feedback.englishLevel,\n    });\n\n    return this.getById(id);\n  }\n\n  public async update(id: number, feedback: UpdateStudentFeedbackDto): Promise<StudentFeedback> {\n    await this.studentFeedbacksRepository.save({\n      id,\n      content: feedback.content,\n      recommendation: feedback.recommendation,\n      englishLevel: feedback.englishLevel,\n    });\n\n    return this.getById(id);\n  }\n\n  public async getById(id: number): Promise<StudentFeedback> {\n    return this.getStudentFeedbackQuery().where('f.id = :id', { id }).getOneOrFail();\n  }\n\n  public async getByStudentAndMentor(studentId: number, mentorId: number): Promise<StudentFeedback | null> {\n    return (\n      (await this.getStudentFeedbackQuery()\n        .where('f.studentId = :studentId', { studentId })\n        .andWhere('f.mentorId = :mentorId', { mentorId })\n        .getOne()) ?? null\n    );\n  }\n\n  private getStudentFeedbackQuery() {\n    return this.studentFeedbacksRepository\n      .createQueryBuilder('f')\n      .leftJoin('f.mentor', 'mentor')\n      .leftJoin('mentor.user', 'user')\n      .addSelect(['mentor.id', 'mentor.userId'])\n      .leftJoin('f.author', 'author')\n      .addSelect(PersonDto.getQueryFields('author'))\n      .addSelect(PersonDto.getQueryFields('user'));\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/feedbacks/index.ts",
    "content": "export * from './feedbacks.controller';\nexport * from './feedbacks.service';\n"
  },
  {
    "path": "nestjs/src/courses/students/index.ts",
    "content": "export * from './students.service';\nexport * from './students.controller';\n"
  },
  {
    "path": "nestjs/src/courses/students/students.controller.ts",
    "content": "import { Controller, ForbiddenException, Get, Param, ParseIntPipe, Query, Req, UseGuards } from '@nestjs/common';\nimport { ApiBadRequestResponse, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../../auth';\nimport { StudentDto, UserStudentsDto, UserStudentsQueryDto } from './dto';\nimport { StudentsService } from './students.service';\n\n@Controller('students')\n@ApiTags('students')\n@UseGuards(DefaultGuard)\nexport class StudentsController {\n  constructor(private studentsService: StudentsService) {}\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getUserStudents' })\n  @ApiOkResponse({ type: UserStudentsDto })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin, Role.Hirer])\n  public async getUserStudents(@Query() query: UserStudentsQueryDto) {\n    const usersWithoutStudents = await this.studentsService.findUserStudents(query);\n\n    return new UserStudentsDto(usersWithoutStudents);\n  }\n\n  @Get(':studentId')\n  @ApiOkResponse({ type: StudentDto })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'getStudent' })\n  public async getOne(@Param('studentId', ParseIntPipe) studentId: number, @Req() req: CurrentRequest) {\n    const hasAccess = await this.studentsService.canAccessStudent(req.user, studentId);\n    if (!hasAccess) {\n      throw new ForbiddenException();\n    }\n    const data = await this.studentsService.getById(studentId);\n    return new StudentDto(data);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/students/students.service.ts",
    "content": "import { Student } from '@entities/student';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { AuthUser, Role, CourseRole } from '../../auth';\nimport { Brackets, Repository, SelectQueryBuilder } from 'typeorm';\nimport { StageInterview, User } from '@entities/index';\nimport { paginate } from 'src/core/paginate';\nimport { UserStudentsQueryDto } from './dto';\nimport { UsersService } from 'src/users/users.service';\n\n@Injectable()\nexport class StudentsService {\n  constructor(\n    @InjectRepository(Student)\n    readonly studentRepository: Repository<Student>,\n\n    @InjectRepository(StageInterview)\n    readonly stageInterviewRepository: Repository<StageInterview>,\n\n    @InjectRepository(User)\n    private usersRepository: Repository<User>,\n  ) {}\n\n  private buildUserStudentsQuery = (reqQuery: UserStudentsQueryDto): SelectQueryBuilder<User> => {\n    let query = this.usersRepository\n      .createQueryBuilder('user')\n      .innerJoin('user.students', 'student')\n      .innerJoin('student.course', 'course')\n      .leftJoin('student.certificate', 'certificate')\n      .select(this.getSelectUserStudentFields());\n\n    if (reqQuery.student) {\n      this.addStudentSearchConditions(query, reqQuery.student);\n    }\n\n    if (reqQuery.country) {\n      query.andWhere(`\"user\".\"countryName\" ILIKE :country`, { country: `%${reqQuery.country}%` });\n    }\n\n    if (reqQuery.city) {\n      query.andWhere(`\"user\".\"cityName\" ILIKE :city`, { city: `%${reqQuery.city}%` });\n    }\n\n    if (reqQuery.ongoingCourses) {\n      this.addCourseCondition(query, reqQuery.ongoingCourses, 'onGoingCourseIds');\n    }\n\n    if (reqQuery.previousCourses) {\n      this.addPreviousCoursesCondition(query, reqQuery.previousCourses);\n    }\n\n    const subQuery = query.clone().select('user.id');\n\n    query = this.usersRepository\n      .createQueryBuilder('user')\n      .innerJoin('user.students', 'student')\n      .innerJoin('student.course', 'course')\n      .leftJoin('student.certificate', 'certificate')\n      .leftJoin('student.mentor', 'mentor')\n      .leftJoin('mentor.user', 'mentorUser')\n      .select(this.getSelectUserStudentFields())\n      .where('user.id IN (' + subQuery.getQuery() + ')')\n      .setParameters(subQuery.getParameters())\n      .addOrderBy('user.id', 'DESC');\n\n    return query;\n  };\n\n  private getSelectUserStudentFields(): string[] {\n    return [\n      ...UsersService.getPrimaryUserFields('user'),\n      ...UsersService.getUserContactsFields('user'),\n      'user.languages',\n      'student.id',\n      'student.courseId',\n      'student.isExpelled',\n      'student.totalScore',\n      'student.rank',\n      'course.alias',\n      'course.name',\n      'course.completed',\n      'certificate.publicId',\n      'mentorUser.githubId',\n      'mentorUser.firstName',\n      'mentorUser.lastName',\n    ];\n  }\n\n  private addStudentSearchConditions(query: SelectQueryBuilder<User>, studentSearch: string): void {\n    const searchTerms = studentSearch.split(' ');\n\n    searchTerms.forEach((term, index) => {\n      query.andWhere(\n        new Brackets(qb => {\n          qb.where(`\"user\".\"firstName\" ILIKE :searchText${index}`, { [`searchText${index}`]: `%${term}%` })\n            .orWhere(`\"user\".\"lastName\" ILIKE :searchText${index}`, { [`searchText${index}`]: `%${term}%` })\n            .orWhere(`\"user\".\"githubId\" ILIKE :searchText${index}`, { [`searchText${index}`]: `%${term}%` });\n        }),\n      );\n    });\n  }\n\n  private addCourseCondition(query: SelectQueryBuilder<User>, courseIds: string, paramName: string): void {\n    const ids = courseIds.split(',').map(id => parseInt(id));\n    query.andWhere(`student.courseId IN (:...${paramName})`, { [paramName]: ids });\n  }\n\n  private addPreviousCoursesCondition(query: SelectQueryBuilder<User>, previousCourses: string): void {\n    const previousCourseIds = previousCourses.split(',').map(id => parseInt(id));\n    query.andWhere(\n      new Brackets(qb => {\n        qb.where('student.courseId IN (:...previousCourseIds)', { previousCourseIds }).andWhere(\n          'certificate.id IS NOT NULL',\n        );\n      }),\n    );\n  }\n\n  public async findUserStudents(reqQuery: UserStudentsQueryDto) {\n    const page = parseInt(reqQuery.current ?? 1);\n    const limit = parseInt(reqQuery.pageSize ?? 20);\n    const query = this.buildUserStudentsQuery(reqQuery);\n    const data = await paginate(query, { page, limit });\n    return data;\n  }\n\n  public getById(id: number) {\n    return this.studentRepository.findOneOrFail({ where: { id }, relations: ['user'] });\n  }\n\n  public async canAccessStudent(user: AuthUser, studentId: number): Promise<boolean> {\n    const student = await this.studentRepository.findOneBy({ id: studentId });\n    const stageInterviews = await this.stageInterviewRepository.find({ where: { studentId } });\n    if (student == null) {\n      return false;\n    }\n\n    if (user.appRoles?.includes(Role.Admin)) {\n      return true;\n    }\n\n    const { courseId } = student;\n    const courseInfo = user.courses?.[courseId];\n    const currentMentorId = user.courses?.[courseId]?.mentorId;\n\n    if (courseInfo?.roles.includes(CourseRole.Manager)) {\n      return true;\n    }\n\n    if (courseInfo?.roles.includes(CourseRole.Supervisor)) {\n      return true;\n    }\n\n    if (stageInterviews.some(interview => interview.mentorId === currentMentorId)) {\n      return true;\n    }\n\n    if (student.mentorId == null) {\n      return false;\n    }\n\n    return student.mentorId === currentMentorId;\n  }\n\n  /*\n   * sets mentor for student, when mentor accepts the student after technical screening\n   */\n  public async setMentor(studentId: number, mentorId: number) {\n    await this.studentRepository.update(studentId, { mentorId });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-solutions/dto/create-task-solution.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsUrl } from 'class-validator';\n\nexport class SaveTaskSolutionDto {\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsUrl()\n  url: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-solutions/dto/index.ts",
    "content": "export * from './task-solution.dto';\nexport * from './create-task-solution.dto';\n"
  },
  {
    "path": "nestjs/src/courses/task-solutions/dto/task-solution.dto.ts",
    "content": "import { TaskSolution } from '@entities/taskSolution';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber, IsString } from 'class-validator';\n\nexport class TaskSolutionDto {\n  constructor(taskSolution: TaskSolution) {\n    this.id = taskSolution.id;\n    this.url = taskSolution.url;\n    this.courseTaskId = taskSolution.courseTaskId;\n  }\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  id: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  courseTaskId: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsString()\n  url: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-solutions/index.ts",
    "content": "export * from './task-solutions.service';\nexport * from './task-solutions.controller';\n"
  },
  {
    "path": "nestjs/src/courses/task-solutions/task-solutions.controller.ts",
    "content": "import { BadRequestException, Body, Controller, Param, ParseIntPipe, Post, Req, UseGuards } from '@nestjs/common';\nimport { ApiBadRequestResponse, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, RoleGuard } from '../../auth';\nimport { TaskSolutionsService } from './task-solutions.service';\nimport { SaveTaskSolutionDto, TaskSolutionDto } from './dto';\n\n@Controller('courses/:courseId/tasks/:courseTaskId/solutions')\n@ApiTags('courses task solutions')\nexport class TaskSolutionsController {\n  constructor(private taskSolutionsService: TaskSolutionsService) {}\n\n  @Post('/')\n  @ApiOkResponse({ type: TaskSolutionDto })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'createTaskSolution' })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Student])\n  public async createTaskSolution(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n    @Body() dto: SaveTaskSolutionDto,\n  ) {\n    const studentId = req.user.courses[courseId]?.studentId;\n    if (!studentId) {\n      throw new BadRequestException('You are not a student in this course');\n    }\n    const result = await this.taskSolutionsService.saveTaskSolution(courseTaskId, studentId, dto);\n\n    return new TaskSolutionDto(result);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-solutions/task-solutions.service.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { TaskSolution } from '@entities/taskSolution';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\n\n@Injectable()\nexport class TaskSolutionsService {\n  constructor(\n    @InjectRepository(CourseTask)\n    readonly courseTaskRepository: Repository<CourseTask>,\n    @InjectRepository(TaskSolution)\n    readonly taskSolutionRepository: Repository<TaskSolution>,\n  ) {}\n\n  public async saveTaskSolution(courseTaskId: number, studentId: number, data: { url: string }) {\n    const solution = await this.taskSolutionRepository.findOne({ where: { courseTaskId, studentId } });\n    const { id } = await this.taskSolutionRepository.save({\n      id: solution?.id ?? undefined,\n      courseTaskId: courseTaskId,\n      studentId: studentId,\n      url: data.url,\n    });\n    return this.taskSolutionRepository.findOneByOrFail({ id });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/dto/create-task-verification.dto.ts",
    "content": "import { ApiPropertyOptional, ApiResponse } from '@nestjs/swagger';\nimport { IsNumber, IsOptional } from 'class-validator';\n\n@ApiResponse({})\nexport class CreateTaskVerificationDto {\n  constructor(id?: number) {\n    this.id = id;\n  }\n\n  @ApiPropertyOptional()\n  @IsOptional()\n  @IsNumber()\n  readonly id?: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/dto/index.ts",
    "content": "export * from './self-education.dto';\nexport * from './task-verifications-attempts.dto';\nexport * from './create-task-verification.dto';\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/dto/self-education.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\n\nexport type SelfEducationAnswers = {\n  index: number;\n  value: number | number[];\n}[];\n\nexport class SelfEducationQuestionDto {\n  constructor(question: SelfEducationQuestionDto) {\n    this.answers = question.answers;\n    this.question = question.question;\n    this.multiple = question.multiple;\n    this.questionImage = question.questionImage;\n    this.answersType = question.answersType;\n  }\n\n  @ApiProperty()\n  answers: string[];\n\n  @ApiProperty()\n  question: string;\n\n  @ApiProperty()\n  multiple: boolean;\n\n  @ApiPropertyOptional()\n  questionImage?: string;\n\n  @ApiPropertyOptional()\n  answersType?: 'image';\n}\n\nexport class SelfEducationQuestionSelectedAnswersDto extends SelfEducationQuestionDto {\n  constructor(question: SelfEducationQuestionSelectedAnswersDto) {\n    super(question);\n    this.selectedAnswers = question.selectedAnswers;\n  }\n\n  @ApiProperty({ type: [Number] })\n  selectedAnswers: number[];\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/dto/task-verifications-attempts.dto.ts",
    "content": "import { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsNotEmpty, IsNumber } from 'class-validator';\nimport { TaskVerification } from '@entities/taskVerification';\nimport { SelfEducationQuestionSelectedAnswersDto } from './self-education.dto';\n\n@ApiResponse({})\nexport class TaskVerificationAttemptDto {\n  constructor(taskVerification: TaskVerification, questions: SelfEducationQuestionSelectedAnswersDto[]) {\n    this.createdDate = taskVerification.createdDate.getTime();\n    this.courseTaskId = taskVerification.courseTaskId;\n    this.score = taskVerification.score;\n    this.maxScore = taskVerification.courseTask.maxScore;\n    this.questions = questions;\n  }\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  createdDate: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  courseTaskId: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  score: number;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsNumber()\n  maxScore: number;\n\n  @ApiProperty({ type: [SelfEducationQuestionSelectedAnswersDto] })\n  @IsNotEmpty()\n  questions: SelfEducationQuestionSelectedAnswersDto[];\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/self-education.service.test.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { Task, TaskType } from '@entities/task';\nimport { TaskVerification } from '@entities/taskVerification';\nimport { BadRequestException, ForbiddenException } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { WriteScoreService } from '../score/write-score.service';\nimport { SelfEducationAnswers } from './dto';\nimport { SelfEducationAttributes, SelfEducationService } from './self-education.service';\n\nconst createMockCourseTask = (\n  attributes: { public: Partial<SelfEducationAttributes['public']>; answers?: SelfEducationAttributes['answers'] },\n  maxScore: number = 100,\n): CourseTask => {\n  return {\n    id: 1,\n    courseId: 1,\n    maxScore,\n    task: {\n      id: 1,\n      type: TaskType.SelfEducation,\n      attributes: {\n        public: {\n          numberOfQuestions: 2,\n          ...attributes.public,\n        },\n        answers: attributes.answers ?? [0, 1],\n      },\n    } as unknown as Task,\n  } as unknown as CourseTask;\n};\n\ndescribe('SelfEducationService', () => {\n  let service: SelfEducationService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        SelfEducationService,\n        {\n          provide: getRepositoryToken(TaskVerification),\n          useValue: {},\n        },\n        {\n          provide: WriteScoreService,\n          useValue: {},\n        },\n      ],\n    }).compile();\n    service = module.get<SelfEducationService>(SelfEducationService);\n  });\n\n  it('should be defined', () => {\n    expect(service).toBeDefined();\n  });\n\n  describe('verifySelfEducationAnswers', () => {\n    let mockTask: CourseTask;\n    let studentAnswers: SelfEducationAnswers;\n\n    beforeEach(() => {\n      mockTask = createMockCourseTask({\n        public: { numberOfQuestions: 2 },\n        answers: [0, [1, 2]],\n      });\n      studentAnswers = [\n        { index: 0, value: 0 },\n        { index: 1, value: [1, 2] },\n      ];\n    });\n\n    describe('Input Validation', () => {\n      it('should throw if student answers count greater than total answers', () => {\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 1, value: 1 },\n          { index: 2, value: 2 },\n        ];\n\n        mockTask = createMockCourseTask({\n          public: {},\n          answers: [0, 1],\n        });\n\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Incorrect student answers count'),\n        );\n      });\n\n      it('should throw if student answers count is 0', () => {\n        studentAnswers = [];\n\n        mockTask = createMockCourseTask({\n          public: {},\n          answers: [0, 1],\n        });\n\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Incorrect student answers count'),\n        );\n      });\n\n      it('should throw if indices are not unique', () => {\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 0, value: 1 },\n        ];\n        mockTask = createMockCourseTask({ public: { numberOfQuestions: 2 } });\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Submitted answer indices must be unique'),\n        );\n      });\n\n      it('should throw if input value is undefined', () => {\n        studentAnswers = [\n          // @ts-expect-error - test\n          { index: 0, value: undefined },\n          { index: 1, value: 1 },\n        ];\n        mockTask = createMockCourseTask({ public: { numberOfQuestions: 2 } });\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Invalid answer value'),\n        );\n      });\n\n      it('should throw if input value is NaN', () => {\n        studentAnswers = [\n          { index: 0, value: NaN },\n          { index: 1, value: 1 },\n        ];\n        mockTask = createMockCourseTask({ public: { numberOfQuestions: 2 } });\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Invalid answer value'),\n        );\n      });\n\n      it('should throw if index is out of range', () => {\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 2, value: 1 },\n        ];\n        mockTask = createMockCourseTask({ public: {} });\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Invalid answer index'),\n        );\n      });\n\n      it('should throw if index is negative', () => {\n        studentAnswers = [\n          { index: -1, value: 0 },\n          { index: 1, value: 1 },\n        ];\n        mockTask = createMockCourseTask({ public: {} });\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Invalid answer index'),\n        );\n      });\n\n      it('should throw if value is negative', () => {\n        studentAnswers = [\n          { index: 0, value: -1 },\n          { index: 1, value: 1 },\n        ];\n        mockTask = createMockCourseTask({ public: {} });\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, 0)).toThrow(\n          new BadRequestException('Invalid answer value'),\n        );\n      });\n    });\n\n    describe('Time-Based Attempt Restrictions', () => {\n      const hoursRestriction = 2;\n      const attempt = 1;\n      const fixedNow = new Date('2026-01-01T12:00:00.000Z');\n\n      beforeEach(() => {\n        vi.useFakeTimers();\n        vi.setSystemTime(fixedNow);\n        mockTask = createMockCourseTask({\n          public: { oneAttemptPerNumberOfHours: hoursRestriction, strictAttemptsMode: true, maxAttemptsNumber: 5 },\n          answers: [0, [1, 2]],\n        });\n      });\n      afterEach(() => {\n        vi.useRealTimers();\n      });\n\n      it('should throw ForbiddenException if last attempt was less than required hours ago', () => {\n        const lastAttemptTime = new Date(fixedNow.getTime() - (hoursRestriction - 1) * 60 * 60 * 1000).toISOString();\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt, lastAttemptTime)).toThrow(\n          ForbiddenException,\n        );\n      });\n\n      it('should NOT throw if last attempt was exactly required hours ago', () => {\n        const lastAttemptTime = new Date(fixedNow.getTime() - hoursRestriction * 60 * 60 * 1000).toISOString();\n\n        expect(() =>\n          service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt, lastAttemptTime),\n        ).not.toThrow();\n      });\n\n      it('should NOT throw if last attempt was more than required hours ago', () => {\n        const lastAttemptTime = new Date(fixedNow.getTime() - (hoursRestriction + 1) * 60 * 60 * 1000).toISOString();\n\n        expect(() =>\n          service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt, lastAttemptTime),\n        ).not.toThrow();\n      });\n\n      it('should NOT throw if lastAttemptTime is undefined', () => {\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt, undefined)).not.toThrow();\n      });\n\n      it('should NOT throw if oneAttemptPerNumberOfHours is 0', () => {\n        mockTask = createMockCourseTask({ public: { oneAttemptPerNumberOfHours: 0 } });\n        const lastAttemptTime = new Date(fixedNow.getTime() - 60 * 1000).toISOString();\n        expect(() =>\n          service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt, lastAttemptTime),\n        ).not.toThrow();\n      });\n    });\n\n    describe('Count-Based Attempt Restrictions (Strict Mode)', () => {\n      const maxAttempts = 3;\n      beforeEach(() => {\n        mockTask = createMockCourseTask({\n          public: { maxAttemptsNumber: 3, strictAttemptsMode: true, oneAttemptPerNumberOfHours: 0 },\n        });\n      });\n\n      it('should NOT throw if attempt < maxAttemptsNumber', () => {\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, maxAttempts - 1)).not.toThrow();\n      });\n\n      it('should throw ForbiddenException if attempt >= maxAttemptsNumber', () => {\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, maxAttempts)).toThrow(\n          ForbiddenException,\n        );\n        expect(() => service.verifySelfEducationAnswers(mockTask, studentAnswers, maxAttempts + 1)).toThrow(\n          ForbiddenException,\n        );\n      });\n    });\n\n    describe('Count-Based Attempt Restrictions (Non-Strict Mode)', () => {\n      const maxAttempts = 3;\n      const maxScore = 100;\n      const threshold = 50;\n\n      beforeEach(() => {\n        mockTask = createMockCourseTask(\n          {\n            public: {\n              numberOfQuestions: 10,\n              maxAttemptsNumber: maxAttempts,\n              strictAttemptsMode: false,\n              oneAttemptPerNumberOfHours: 0,\n              tresholdPercentage: threshold,\n            },\n            answers: Array.from({ length: 10 }, (_, i) => i),\n          },\n          maxScore,\n        );\n        studentAnswers = Array.from({ length: 10 }, (_, i) => ({\n          index: i,\n          value: i < 8 ? i : 99,\n        }));\n      });\n\n      it('should not halve score if attempt < maxAttemptsNumber', () => {\n        const attempt = maxAttempts - 1;\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt);\n        expect(result.score).toBe(80);\n        expect(result.details).toBe('Accuracy: 80%');\n      });\n\n      it('should halve score and add note if attempt === maxAttemptsNumber', () => {\n        const attempt = maxAttempts;\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt);\n        expect(result.score).toBe(40);\n        expect(result.details).toBe('Accuracy: 80%. Attempts number was over, so score was divided by 2');\n      });\n\n      it('should halve score and add note if attempt > maxAttemptsNumber', () => {\n        const attempt = maxAttempts + 1;\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt);\n        expect(result.score).toBe(40);\n        expect(result.details).toBe('Accuracy: 80%. Attempts number was over, so score was divided by 2');\n      });\n\n      it('should halve score AFTER checking threshold (score 0 remains 0)', () => {\n        mockTask = createMockCourseTask(\n          {\n            public: {\n              numberOfQuestions: 10,\n              maxAttemptsNumber: maxAttempts,\n              strictAttemptsMode: false,\n              oneAttemptPerNumberOfHours: 0,\n              tresholdPercentage: 50,\n            },\n            answers: Array.from({ length: 10 }, (_, i) => i),\n          },\n          maxScore,\n        );\n        studentAnswers = Array.from({ length: 10 }, (_, i) => ({\n          index: i,\n          value: i < 4 ? i : 99,\n        }));\n        const attempt = maxAttempts;\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, attempt);\n        expect(result.score).toBe(0);\n        expect(result.details).toContain(\n          'Your accuracy: 40%. The minimum accuracy for obtaining a score on this test is 50%.',\n        );\n        expect(result.details).toContain('. Attempts number was over, so score was divided by 2');\n      });\n    });\n\n    describe('Answer Verification & Score Calculation', () => {\n      const threshold = 60;\n      const maxScore = 100;\n      const numQuestions = 10;\n\n      beforeEach(() => {\n        mockTask = createMockCourseTask(\n          {\n            public: {\n              numberOfQuestions: numQuestions,\n              tresholdPercentage: threshold,\n              strictAttemptsMode: true,\n              maxAttemptsNumber: 5,\n            },\n            answers: Array.from({ length: numQuestions }, (_, i) => i),\n          },\n          maxScore,\n        );\n      });\n\n      it('should score 0 if accuracy < threshold', () => {\n        studentAnswers = Array.from({ length: numQuestions }, (_, i) => ({\n          index: i,\n          value: i < 5 ? i : 99,\n        }));\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(0);\n        expect(result.details).toBe(\n          `Your accuracy: 50%. The minimum accuracy for obtaining a score on this test is ${threshold}%.`,\n        );\n      });\n\n      it('should calculate score correctly if accuracy === threshold', () => {\n        studentAnswers = Array.from({ length: numQuestions }, (_, i) => ({\n          index: i,\n          value: i < 6 ? i : 99,\n        }));\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(60);\n        expect(result.details).toBe(`Accuracy: 60%`);\n      });\n\n      it('should calculate score correctly if accuracy > threshold', () => {\n        studentAnswers = Array.from({ length: numQuestions }, (_, i) => ({\n          index: i,\n          value: i,\n        }));\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(100);\n        expect(result.details).toBe(`Accuracy: 100%`);\n      });\n\n      it('should score 0 if accuracy is 0%', () => {\n        studentAnswers = Array.from({ length: numQuestions }, (_, i) => ({\n          index: i,\n          value: 99,\n        }));\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(0);\n        expect(result.details).toBe(\n          `Your accuracy: 0%. The minimum accuracy for obtaining a score on this test is ${threshold}%.`,\n        );\n      });\n\n      it('should calculate score based on maxScore', () => {\n        const specificMaxScore = 95;\n        mockTask = createMockCourseTask(\n          {\n            public: { numberOfQuestions: numQuestions, tresholdPercentage: threshold },\n            answers: Array.from({ length: numQuestions }, (_, i) => i),\n          },\n          specificMaxScore,\n        );\n        studentAnswers = Array.from({ length: numQuestions }, (_, i) => ({\n          index: i,\n          value: i < 8 ? i : 99,\n        }));\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        const expectedScore = Math.floor(specificMaxScore * 0.8);\n        expect(result.score).toBe(expectedScore);\n        expect(result.details).toBe(`Accuracy: 80%`);\n      });\n    });\n\n    describe('Answer Verification (Single/Multiple Choice & Order)', () => {\n      beforeEach(() => {\n        mockTask = createMockCourseTask({\n          public: { numberOfQuestions: 2 },\n          answers: [5, [1, 3]],\n        });\n      });\n\n      it('should mark correct for exact match (single and multiple)', () => {\n        studentAnswers = [\n          { index: 0, value: 5 },\n          { index: 1, value: [1, 3] },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.checkedAnswers).toEqual([\n          { index: 0, value: 5, isCorrect: true },\n          { index: 1, value: [1, 3], isCorrect: true },\n        ]);\n        expect(result.score).toBe(100);\n      });\n\n      it('should mark correct for multiple choice regardless of order', () => {\n        studentAnswers = [\n          { index: 0, value: 5 },\n          { index: 1, value: [3, 1] },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.checkedAnswers).toEqual([\n          { index: 0, value: 5, isCorrect: true },\n          { index: 1, value: [3, 1], isCorrect: true },\n        ]);\n        expect(result.score).toBe(100);\n      });\n\n      it('should mark incorrect for wrong single answer', () => {\n        studentAnswers = [\n          { index: 0, value: 4 },\n          { index: 1, value: [1, 3] },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.checkedAnswers).toEqual([\n          { index: 0, value: 4, isCorrect: false },\n          { index: 1, value: [1, 3], isCorrect: true },\n        ]);\n        expect(result.score).toBe(50);\n      });\n\n      it('should mark incorrect for wrong multiple choice answer (partial match)', () => {\n        studentAnswers = [\n          { index: 0, value: 5 },\n          { index: 1, value: [1] },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.checkedAnswers).toEqual([\n          { index: 0, value: 5, isCorrect: true },\n          { index: 1, value: [1], isCorrect: false },\n        ]);\n        expect(result.score).toBe(50);\n      });\n\n      it('should mark incorrect for wrong multiple choice answer (extra value)', () => {\n        studentAnswers = [\n          { index: 0, value: 5 },\n          { index: 1, value: [1, 3, 4] },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.checkedAnswers).toEqual([\n          { index: 0, value: 5, isCorrect: true },\n          { index: 1, value: [1, 3, 4], isCorrect: false },\n        ]);\n        expect(result.score).toBe(50);\n      });\n\n      it('should handle single answers provided as array', () => {\n        studentAnswers = [\n          { index: 0, value: [5] },\n          { index: 1, value: [1, 3] },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.checkedAnswers).toEqual([\n          { index: 0, value: [5], isCorrect: true },\n          { index: 1, value: [1, 3], isCorrect: true },\n        ]);\n        expect(result.score).toBe(100);\n      });\n    });\n\n    describe('Return Value Structure', () => {\n      it('should return the expected structure', () => {\n        mockTask = createMockCourseTask({\n          public: { numberOfQuestions: 2, tresholdPercentage: 0 },\n          answers: [0, 1],\n        });\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 1, value: 99 },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n\n        expect(result).toHaveProperty('checkedAnswers');\n        expect(result).toHaveProperty('score');\n        expect(result).toHaveProperty('details');\n\n        expect(Array.isArray(result.checkedAnswers)).toBe(true);\n        expect(result.checkedAnswers.length).toBe(2);\n        expect(result.checkedAnswers[0]).toEqual({ index: 0, value: 0, isCorrect: true });\n        expect(result.checkedAnswers[1]).toEqual({ index: 1, value: 99, isCorrect: false });\n\n        expect(typeof result.score).toBe('number');\n        expect(result.score).toBe(50);\n\n        expect(typeof result.details).toBe('string');\n        expect(result.details).toBe('Accuracy: 50%');\n      });\n    });\n\n    describe('Edge Cases', () => {\n      it('should handle empty array as answer value', () => {\n        mockTask = createMockCourseTask({\n          public: { numberOfQuestions: 2 },\n          answers: [0, []],\n        });\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 1, value: [] },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.checkedAnswers[1]?.isCorrect).toBe(true);\n      });\n\n      it('should handle 0% threshold correctly', () => {\n        mockTask = createMockCourseTask({\n          public: { numberOfQuestions: 2, tresholdPercentage: 0 },\n          answers: [0, 1],\n        });\n        studentAnswers = [\n          { index: 0, value: 99 },\n          { index: 1, value: 99 },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(0);\n        expect(result.details).toBe('Accuracy: 0%');\n      });\n\n      it('should handle 100% threshold correctly', () => {\n        mockTask = createMockCourseTask({\n          public: { numberOfQuestions: 2, tresholdPercentage: 100 },\n          answers: [0, 1],\n        });\n\n        // All correct - should pass\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 1, value: 1 },\n        ];\n        let result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(100);\n\n        // One wrong - should fail\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 1, value: 99 },\n        ];\n        result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(0);\n        expect(result.details).toBe(\n          'Your accuracy: 50%. The minimum accuracy for obtaining a score on this test is 100%.',\n        );\n      });\n\n      it('should handle mixed single and multiple choice answers correctly', () => {\n        mockTask = createMockCourseTask({\n          public: { numberOfQuestions: 3 },\n          answers: [5, [1, 3], 7],\n        });\n        studentAnswers = [\n          { index: 0, value: 5 },\n          { index: 1, value: [1, 3] },\n          { index: 2, value: 7 },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(100);\n        expect(result.checkedAnswers.every(a => a.isCorrect)).toBe(true);\n      });\n\n      it('should round scores correctly', () => {\n        mockTask = createMockCourseTask(\n          {\n            public: { numberOfQuestions: 3, tresholdPercentage: 0 },\n            answers: [0, 1, 2],\n          },\n          100,\n        );\n\n        // 2/3 correct = 66.67% rounded to 67%\n        studentAnswers = [\n          { index: 0, value: 0 },\n          { index: 1, value: 1 },\n          { index: 2, value: 99 },\n        ];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(67); // Should be rounded correctly - 66.67% of 100 = 66.67 -> floor -> 66\n        expect(result.details).toBe('Accuracy: 67%');\n      });\n\n      it('should handle maxScore of 0', () => {\n        mockTask = createMockCourseTask(\n          {\n            public: { numberOfQuestions: 1, tresholdPercentage: 0 },\n            answers: [1],\n          },\n          0,\n        );\n        studentAnswers = [{ index: 0, value: 1 }];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, 0);\n        expect(result.score).toBe(0);\n        expect(result.details).toBe('Accuracy: 100%');\n      });\n\n      it('should handle extremely large attempt numbers correctly in non-strict mode', () => {\n        mockTask = createMockCourseTask(\n          {\n            public: {\n              numberOfQuestions: 1,\n              tresholdPercentage: 0,\n              maxAttemptsNumber: 3,\n              strictAttemptsMode: false,\n            },\n            answers: [1],\n          },\n          100,\n        );\n\n        studentAnswers = [{ index: 0, value: 1 }];\n        const result = service.verifySelfEducationAnswers(mockTask, studentAnswers, Number.MAX_SAFE_INTEGER);\n        expect(result.score).toBe(50); // Still just divided by 2 once\n        expect(result.details).toBe('Accuracy: 100%. Attempts number was over, so score was divided by 2');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/self-education.service.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { TaskVerification } from '@entities/taskVerification';\nimport { ForbiddenException, Injectable, BadRequestException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { differenceInHours } from 'date-fns';\nimport { Repository } from 'typeorm';\nimport { WriteScoreService } from '../score/write-score.service';\nimport { SelfEducationAnswers } from './dto';\n\ntype CheckedAnswer = SelfEducationAnswers[number] & { isCorrect: boolean };\n\ntype SelfEducationVerificationParams = {\n  courseId: number;\n  courseTask: CourseTask;\n  studentId: number;\n  studentAnswers: SelfEducationAnswers;\n};\n\nexport type SelfEducationAttributes = {\n  public: {\n    maxAttemptsNumber: number;\n    numberOfQuestions: number;\n    tresholdPercentage: number;\n    strictAttemptsMode?: boolean;\n    oneAttemptPerNumberOfHours?: number;\n    questions: {\n      question: string;\n      answers: string[];\n      multiple: boolean;\n    }[];\n  };\n  answers: (number | number[])[];\n};\n\n@Injectable()\nexport class SelfEducationService {\n  constructor(\n    @InjectRepository(TaskVerification)\n    private readonly taskVerificationsRepository: Repository<TaskVerification>,\n\n    private readonly writeScoreService: WriteScoreService,\n  ) {}\n\n  public async createSelfEducationVerification({\n    courseTask,\n    studentId,\n    studentAnswers,\n  }: SelfEducationVerificationParams) {\n    const { id: courseTaskId, type: courseTaskType } = courseTask;\n\n    const verifications = await this.taskVerificationsRepository.find({\n      where: { studentId, courseTaskId },\n      order: { createdDate: 'DESC' },\n    });\n\n    const { score, details, checkedAnswers } = this.verifySelfEducationAnswers(\n      courseTask,\n      studentAnswers,\n      verifications.length,\n      verifications[0]?.createdDate?.toString(),\n    );\n\n    const { id } = await this.taskVerificationsRepository.save({\n      studentId,\n      courseTaskId,\n      score,\n      details,\n      status: 'completed',\n      answers: checkedAnswers,\n    });\n    const result = await this.taskVerificationsRepository.findOneByOrFail({ id });\n    await this.writeScoreService.saveScore(studentId, courseTaskId, { score, comment: details });\n\n    return { ...result, courseTask: { type: courseTaskType } };\n  }\n\n  public verifySelfEducationAnswers(\n    courseTask: CourseTask,\n    studentAnswers: SelfEducationAnswers,\n    attempt: number,\n    lastAttemptTime?: string,\n  ) {\n    const {\n      answers,\n      public: {\n        tresholdPercentage,\n        maxAttemptsNumber,\n        numberOfQuestions,\n        strictAttemptsMode = true,\n        oneAttemptPerNumberOfHours = 0,\n      },\n    } = courseTask.task.attributes as SelfEducationAttributes;\n\n    if (studentAnswers.length > answers.length || studentAnswers.length === 0) {\n      throw new BadRequestException(`Incorrect student answers count`);\n    }\n\n    const isPositiveInteger = (value: number) => Number.isInteger(value) && value >= 0;\n    const isWithinAnswersRange = (index: number) => index >= 0 && index < answers.length;\n\n    // Check if all answer values are integers\n    if (studentAnswers.flatMap(a => (Array.isArray(a.value) ? a.value : [a.value])).some(a => !isPositiveInteger(a))) {\n      throw new BadRequestException('Invalid answer value');\n    }\n\n    if (studentAnswers.some(a => !isPositiveInteger(a.index) || !isWithinAnswersRange(a.index))) {\n      throw new BadRequestException('Invalid answer index');\n    }\n\n    const submittedIndices = new Set(studentAnswers.map(a => a.index));\n    if (submittedIndices.size !== studentAnswers.length) {\n      throw new BadRequestException('Submitted answer indices must be unique');\n    }\n\n    const { maxScore } = courseTask;\n\n    if (\n      !this.isNextSubmitAllowed(oneAttemptPerNumberOfHours, lastAttemptTime) ||\n      (strictAttemptsMode && attempt >= maxAttemptsNumber)\n    ) {\n      throw new ForbiddenException();\n    }\n\n    const checkedAnswers: CheckedAnswer[] = studentAnswers.map(({ index, value: studentAnswer }) => {\n      const answer = answers[index];\n      if (answer === undefined) {\n        throw new BadRequestException('Invalid answer index');\n      }\n\n      const serializedAnswer = this.serializeAnswers(answer);\n      const serializedStudentAnswer = this.serializeAnswers(studentAnswer);\n\n      return { index, value: studentAnswer, isCorrect: serializedAnswer === serializedStudentAnswer };\n    });\n\n    const correctAnswersCount = checkedAnswers.filter(answer => answer.isCorrect === true).length;\n    const correctAnswersPercent = Math.round((100 / numberOfQuestions) * correctAnswersCount);\n\n    let score = correctAnswersPercent < tresholdPercentage ? 0 : Math.floor(maxScore * correctAnswersPercent * 0.01);\n    let details =\n      correctAnswersPercent < tresholdPercentage\n        ? `Your accuracy: ${correctAnswersPercent}%. The minimum accuracy for obtaining a score on this test is ${tresholdPercentage}%.`\n        : `Accuracy: ${correctAnswersPercent}%`;\n\n    if (attempt >= maxAttemptsNumber) {\n      score = Math.floor(score / 2);\n      details += '. Attempts number was over, so score was divided by 2';\n    }\n\n    return {\n      checkedAnswers,\n      score,\n      details,\n    };\n  }\n\n  private isNextSubmitAllowed(hours: number, lastAttemptTime?: string) {\n    if (!hours || !lastAttemptTime) return true;\n\n    return differenceInHours(new Date(), new Date(lastAttemptTime)) >= hours;\n  }\n\n  /**\n   * Sorts the values and joins them with a pipe character.\n   * The final string is used for comparison of answers.\n   */\n  private serializeAnswers(values: number | number[]): string {\n    if (Array.isArray(values)) {\n      return [...values].sort((a, b) => Number(a) - Number(b)).join('|');\n    }\n    return String(values);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/task-verifications.controller.ts",
    "content": "import {\n  BadRequestException,\n  Body,\n  Controller,\n  ForbiddenException,\n  Get,\n  Param,\n  ParseIntPipe,\n  Post,\n  Req,\n  UseGuards,\n} from '@nestjs/common';\nimport {\n  ApiBadRequestResponse,\n  ApiBody,\n  ApiForbiddenResponse,\n  ApiOkResponse,\n  ApiOperation,\n  ApiTags,\n  ApiTooManyRequestsResponse,\n} from '@nestjs/swagger';\nimport { CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, RoleGuard } from '../../auth';\nimport { CreateTaskVerificationDto } from './dto/create-task-verification.dto';\nimport { TaskVerificationAttemptDto } from './dto/task-verifications-attempts.dto';\nimport { TaskVerificationsService } from './task-verifications.service';\n\n@Controller('courses/:courseId/tasks/:courseTaskId/verifications')\n@ApiTags('course task verifications')\nexport class TaskVerificationsController {\n  constructor(private taskVerificationsService: TaskVerificationsService) {}\n\n  @Get('/answers')\n  @ApiOkResponse({ type: [TaskVerificationAttemptDto] })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiOperation({ operationId: 'getAnswers' })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Student], true)\n  public async getAnswers(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n  ) {\n    const studentId = req.user.courses[courseId]?.studentId as number;\n    return this.taskVerificationsService.getAnswersByAttempts(courseTaskId, studentId);\n  }\n\n  @Post('/')\n  @ApiOperation({ operationId: 'createTaskVerification' })\n  @ApiBody({ type: Object, required: true })\n  @ApiOkResponse({ type: CreateTaskVerificationDto })\n  @ApiBadRequestResponse()\n  @ApiTooManyRequestsResponse()\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([CourseRole.Student])\n  public async createVerification(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('courseTaskId', ParseIntPipe) courseTaskId: number,\n    @Body() body: Record<string, unknown>,\n  ) {\n    const courseStatus = req.user.courses[courseId];\n    const studentId = courseStatus?.studentId;\n\n    if (!studentId) {\n      throw new BadRequestException('You are not a student in this course');\n    }\n    if (courseStatus?.isExpelled) {\n      throw new ForbiddenException('You are expelled from this course');\n    }\n\n    const githubId = req.user.githubId;\n    return this.taskVerificationsService.createTaskVerification(courseTaskId, studentId, { githubId, body });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/task-verifications.service.test.ts",
    "content": "import { BadRequestException, HttpStatus } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { CourseTask } from '@entities/courseTask';\nimport { Student } from '@entities/student';\nimport { TaskType } from '@entities/task';\nimport { TaskVerification } from '@entities/taskVerification';\nimport { TaskVerificationsService } from './task-verifications.service';\nimport { CloudApiService } from 'src/cloud-api/cloud-api.service';\nimport { SelfEducationService } from './self-education.service';\n\ndescribe('TaskVerificationsService', () => {\n  let service: TaskVerificationsService;\n\n  const taskVerificationsRepository = {\n    find: vi.fn(),\n    findOne: vi.fn(),\n    save: vi.fn(),\n  };\n\n  const courseTasksRepository = {\n    findOneByOrFail: vi.fn(),\n    findOne: vi.fn(),\n  };\n\n  const studentsRepository = {\n    findOneByOrFail: vi.fn(),\n  };\n\n  const cloudService = {\n    submitTask: vi.fn(),\n  };\n\n  const selfEducationService = {\n    createSelfEducationVerification: vi.fn(),\n  };\n\n  beforeEach(async () => {\n    vi.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        TaskVerificationsService,\n        {\n          provide: getRepositoryToken(TaskVerification),\n          useValue: taskVerificationsRepository,\n        },\n        {\n          provide: getRepositoryToken(CourseTask),\n          useValue: courseTasksRepository,\n        },\n        {\n          provide: getRepositoryToken(Student),\n          useValue: studentsRepository,\n        },\n        {\n          provide: CloudApiService,\n          useValue: cloudService,\n        },\n        {\n          provide: SelfEducationService,\n          useValue: selfEducationService,\n        },\n      ],\n    }).compile();\n\n    service = module.get<TaskVerificationsService>(TaskVerificationsService);\n  });\n\n  describe('getAnswersByAttempts', () => {\n    it('should throw if deadline has not passed yet', async () => {\n      courseTasksRepository.findOneByOrFail.mockResolvedValueOnce({\n        studentEndDate: new Date('2026-01-01T12:10:00.000Z'),\n      });\n\n      vi.useFakeTimers();\n      vi.setSystemTime(new Date('2026-01-01T12:00:00.000Z'));\n\n      await expect(service.getAnswersByAttempts(1, 2)).rejects.toThrow(BadRequestException);\n\n      vi.useRealTimers();\n    });\n  });\n\n  describe('createTaskVerification', () => {\n    const student = { id: 10, courseId: 20 } as Student;\n\n    it('should throw Too Many Requests when pending verification exists', async () => {\n      courseTasksRepository.findOne.mockResolvedValueOnce({\n        id: 11,\n        courseId: 20,\n        studentStartDate: new Date('2026-01-01T08:00:00.000Z'),\n        studentEndDate: new Date('2026-01-01T18:00:00.000Z'),\n        task: { name: 'Task' },\n      });\n      studentsRepository.findOneByOrFail.mockResolvedValueOnce(student);\n      taskVerificationsRepository.findOne.mockResolvedValueOnce({ id: 77 });\n\n      vi.useFakeTimers();\n      vi.setSystemTime(new Date('2026-01-01T12:00:00.000Z'));\n\n      await expect(service.createTaskVerification(11, 10, { githubId: 'gh', body: {} })).rejects.toMatchObject({\n        status: HttpStatus.TOO_MANY_REQUESTS,\n      });\n\n      vi.useRealTimers();\n    });\n\n    it('should throw when task verification is expired', async () => {\n      courseTasksRepository.findOne.mockResolvedValueOnce({\n        id: 11,\n        courseId: 20,\n        studentStartDate: new Date('2026-01-01T08:00:00.000Z'),\n        studentEndDate: new Date('2026-01-01T11:00:00.000Z'),\n        task: { name: 'Task' },\n      });\n      studentsRepository.findOneByOrFail.mockResolvedValueOnce(student);\n      taskVerificationsRepository.findOne.mockResolvedValueOnce(null);\n\n      vi.useFakeTimers();\n      vi.setSystemTime(new Date('2026-01-01T12:00:00.000Z'));\n\n      await expect(service.createTaskVerification(11, 10, { githubId: 'gh', body: {} })).rejects.toThrow('expired');\n\n      vi.useRealTimers();\n    });\n\n    it('should delegate self-education tasks to self-education service', async () => {\n      courseTasksRepository.findOne.mockResolvedValueOnce({\n        id: 11,\n        courseId: 20,\n        type: TaskType.SelfEducation,\n        studentStartDate: new Date('2026-01-01T08:00:00.000Z'),\n        studentEndDate: new Date('2026-01-01T18:00:00.000Z'),\n        task: { name: 'Task', type: TaskType.SelfEducation },\n      });\n      studentsRepository.findOneByOrFail.mockResolvedValueOnce(student);\n      taskVerificationsRepository.findOne.mockResolvedValueOnce(null);\n\n      vi.useFakeTimers();\n      vi.setSystemTime(new Date('2026-01-01T12:00:00.000Z'));\n\n      const result = await service.createTaskVerification(11, 10, {\n        githubId: 'gh',\n        body: [{ index: 0, value: 1 }],\n      });\n\n      expect(result).toEqual({ id: undefined });\n      expect(selfEducationService.createSelfEducationVerification).toHaveBeenCalledTimes(1);\n      expect(taskVerificationsRepository.save).not.toHaveBeenCalled();\n      expect(cloudService.submitTask).not.toHaveBeenCalled();\n\n      vi.useRealTimers();\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/task-verifications/task-verifications.service.ts",
    "content": "import { CourseTask } from '@entities/courseTask';\nimport { Student } from '@entities/index';\nimport { TaskType } from '@entities/task';\nimport { TaskVerification } from '@entities/taskVerification';\nimport { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { isBefore, isValid, subHours } from 'date-fns';\nimport { CloudApiService } from 'src/cloud-api/cloud-api.service';\nimport { MoreThan, Repository } from 'typeorm';\nimport { SelfEducationAnswers, SelfEducationQuestionSelectedAnswersDto, TaskVerificationAttemptDto } from './dto';\nimport { SelfEducationService } from './self-education.service';\n\nexport type VerificationEvent = {\n  id: number;\n  courseTask: {\n    id: number;\n    type: string;\n    [key: string]: unknown;\n  };\n  studentId: number;\n  githubId: string;\n};\n\n@Injectable()\nexport class TaskVerificationsService {\n  constructor(\n    @InjectRepository(TaskVerification)\n    readonly taskVerificationsRepository: Repository<TaskVerification>,\n\n    @InjectRepository(CourseTask)\n    readonly courseTasksRepository: Repository<CourseTask>,\n\n    @InjectRepository(Student)\n    readonly studentsRepository: Repository<Student>,\n\n    readonly cloudService: CloudApiService,\n\n    readonly seflEducationService: SelfEducationService,\n  ) {}\n\n  public async getAnswersByAttempts(courseTaskId: number, studentId: number): Promise<TaskVerificationAttemptDto[]> {\n    const courseTask = await this.courseTasksRepository.findOneByOrFail({ id: courseTaskId });\n\n    const now = new Date();\n    const endDate = courseTask?.studentEndDate ? new Date(courseTask.studentEndDate) : null;\n\n    if (endDate && isValid(endDate) && isBefore(now, endDate)) {\n      throw new BadRequestException('The answers cannot be checked until the deadline has passed.');\n    }\n\n    const taskVerifications = await this.taskVerificationsRepository.find({\n      select: ['createdDate', 'courseTaskId', 'score', 'answers', 'courseTask'],\n      where: { courseTaskId, studentId },\n      relations: ['courseTask', 'courseTask.task'],\n      order: {\n        createdDate: 'desc',\n      },\n    });\n\n    if (taskVerifications && taskVerifications.length > 0) {\n      const hasAnswers = taskVerifications.some(v => v.answers && v.answers.length > 0);\n\n      if (!hasAnswers) {\n        throw new BadRequestException('The answers are not available for this task.');\n      }\n\n      return taskVerifications.map(verification => {\n        const questionsWithIncorrectAnswers: SelfEducationQuestionSelectedAnswersDto[] = verification.answers\n          .filter(answer => !answer.isCorrect)\n          .map(answer => {\n            const taskQuestion = (\n              verification.courseTask.task.attributes as {\n                public: { questions: SelfEducationQuestionSelectedAnswersDto[] };\n              }\n            ).public.questions[answer.index];\n\n            if (!taskQuestion) {\n              return null;\n            }\n\n            return new SelfEducationQuestionSelectedAnswersDto({\n              answers: taskQuestion.answers,\n              selectedAnswers: Array.isArray(answer.value) ? answer.value : [answer.value],\n              multiple: taskQuestion.multiple,\n              question: taskQuestion.question,\n              answersType: taskQuestion.answersType,\n              questionImage: taskQuestion.questionImage,\n            });\n          })\n          .filter((question): question is SelfEducationQuestionSelectedAnswersDto => question !== null);\n\n        return new TaskVerificationAttemptDto(verification, questionsWithIncorrectAnswers);\n      });\n    }\n    throw new BadRequestException('The answers cannot be checked if there were no attempts.');\n  }\n\n  public async createTaskVerification(\n    courseTaskId: number,\n    studentId: number,\n    data: { githubId: string; body: Record<string, unknown> | unknown[] },\n  ): Promise<{ id?: number }> {\n    const [courseTask, student] = await Promise.all([\n      this.courseTasksRepository.findOne({\n        where: { id: courseTaskId },\n        relations: ['task'],\n      }),\n      this.studentsRepository.findOneByOrFail({ id: studentId }),\n    ]);\n\n    if (courseTask == null || student == null) {\n      throw new BadRequestException('No student or not valid course task');\n    }\n\n    if (courseTask.courseId !== student.courseId) {\n      throw new BadRequestException(`Course task does not belong to the student's course`);\n    }\n\n    if (courseTask.studentStartDate && courseTask.studentStartDate > new Date()) {\n      throw new BadRequestException(`Task Verification ${courseTask.task.name} not started`);\n    }\n\n    const existing = await this.taskVerificationsRepository.findOne({\n      where: {\n        status: 'pending',\n        studentId,\n        courseTaskId,\n        updatedDate: MoreThan(subHours(new Date(), 1)),\n      },\n      select: ['id'],\n    });\n\n    if (existing != null) {\n      throw new HttpException(`Task Verification [${existing.id}] is in progress`, HttpStatus.TOO_MANY_REQUESTS);\n    }\n\n    const now = new Date();\n    const endDate = courseTask?.studentEndDate ? new Date(courseTask.studentEndDate) : null;\n\n    if (endDate && isValid(endDate) && isBefore(endDate, now)) {\n      throw new BadRequestException(`Task Verification [${courseTask.id}] expired`);\n    }\n\n    if (courseTask.type === TaskType.SelfEducation) {\n      await this.seflEducationService.createSelfEducationVerification({\n        courseId: courseTask.courseId,\n        courseTask,\n        studentId: student.id,\n        studentAnswers: data.body as SelfEducationAnswers,\n      });\n      return { id: undefined };\n    }\n\n    const { id } = await this.taskVerificationsRepository.save({\n      studentId,\n      courseTaskId,\n      score: 0,\n      status: 'pending',\n    });\n\n    const result: VerificationEvent = {\n      id: id,\n      githubId: data.githubId,\n      studentId: student.id,\n      courseTask: {\n        ...data.body,\n        id: courseTask.id,\n        type: courseTask.type || courseTask.task.type,\n        attributes: courseTask.task.attributes ?? {},\n      },\n    };\n\n    await this.cloudService.submitTask([result]);\n\n    return { id };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/tasks/dto/check-tasks-deadline.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber } from 'class-validator';\n\nexport class CheckTasksDeadlineDto {\n  @ApiProperty()\n  @IsNumber()\n  public deadlineInHours: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/tasks/tasks.controller.ts",
    "content": "import { Body, Controller, Logger, Post, UseGuards } from '@nestjs/common';\nimport { ApiForbiddenResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { UserNotificationsService } from 'src/users-notifications/users.notifications.service';\nimport { CourseGuard, DefaultGuard, RequiredRoles, Role } from '../../auth';\nimport { CheckTasksDeadlineDto } from './dto/check-tasks-deadline';\nimport { TasksService } from './tasks.service';\n\n@Controller('tasks')\n@ApiTags('courses tasks')\n@UseGuards(DefaultGuard, CourseGuard)\nexport class TasksController {\n  private readonly logger = new Logger('tasks');\n\n  constructor(\n    private tasksService: TasksService,\n    private notificationService: UserNotificationsService,\n  ) {}\n\n  @Post('/notify/changes')\n  @ApiOperation({ operationId: 'notifyTasksDeadlines' })\n  @ApiForbiddenResponse()\n  @RequiredRoles([Role.Admin])\n  public async notifyTasksDeadlines(@Body() dto: CheckTasksDeadlineDto) {\n    const students = await this.tasksService.getPendingTasksDeadline(dto.deadlineInHours);\n\n    Promise.resolve().then(\n      () =>\n        // eslint-disable-next-line no-async-promise-executor\n        new Promise(async () => {\n          this.logger.log({ message: 'processing students notifications...' });\n\n          for (const [userId, tasks] of students) {\n            try {\n              await this.notificationService.sendEventNotification({\n                data: { tasks },\n                notificationId: 'taskDeadline',\n                userId,\n              });\n            } catch (e) {\n              this.logger.log({ message: (e as Error).message, userId });\n            }\n          }\n        }),\n    );\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/tasks/tasks.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { CoursesService } from 'src/courses/courses.service';\nimport { CourseTasksService } from 'src/courses';\nimport { Course } from '@entities/course';\nimport { Task } from '@entities/task';\n\n@Injectable()\nexport class TasksService {\n  private readonly logger = new Logger('tasks');\n\n  constructor(\n    private courseService: CoursesService,\n    private courseTasksService: CourseTasksService,\n  ) {}\n\n  public async getPendingTasksDeadline(deadlineWithinHours: number) {\n    const activeCourses = await this.courseService.getActiveCourses(['students']);\n    const studentMap = new Map<number, { course: Omit<Course, 'students'>; task: Task }[]>();\n\n    for (const course of activeCourses) {\n      const { students, ...courseInfo } = course;\n      const courseTasks = await this.courseTasksService.getTasksPendingDeadline(courseInfo.id, {\n        deadlineWithinHours,\n      });\n      this.logger.log({ message: `course: ${course.id} has ${courseTasks.length} tasks pending deadline` });\n\n      const taskSolutions = courseTasks.map(courseTask => ({\n        course: courseInfo,\n        task: courseTask.task,\n        studentHasSolution: new Set<number>(courseTask.taskSolutions?.map(solution => solution.studentId) ?? []),\n      }));\n\n      for (const student of students) {\n        if (student.isExpelled) continue;\n        for (const solutionInfo of taskSolutions) {\n          const { studentHasSolution, ...rest } = solutionInfo;\n          if (!studentHasSolution.has(student.id)) {\n            const studentPendingTasks = studentMap.get(student.userId) ?? [];\n\n            studentPendingTasks.push(rest);\n            studentMap.set(student.userId, studentPendingTasks);\n          }\n        }\n      }\n    }\n    this.logger.log({ message: `students missing deadlines ${studentMap.size}` });\n\n    return studentMap;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/distribute-students.service.ts",
    "content": "import { Student, Team, TeamDistributionStudent } from '@entities/index';\nimport { TeamDistribution } from '@entities/teamDistribution';\nimport { Injectable, InternalServerErrorException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { shuffle } from 'lodash';\nimport { DataSource, Repository } from 'typeorm';\nimport { TeamDistributionStudentService } from './team-distribution-student.service';\nimport { TeamService } from './team.service';\n\n@Injectable()\nexport class DistributeStudentsService {\n  constructor(\n    @InjectRepository(TeamDistribution)\n    private repository: Repository<TeamDistribution>,\n    private teamDistributionStudentService: TeamDistributionStudentService,\n    private teamService: TeamService,\n    private dataSource: DataSource,\n  ) {}\n\n  public getById(id: number) {\n    return this.repository.findOneOrFail({ where: { id } });\n  }\n\n  private getTeamCapacity(teams: Team[], teamSize: number) {\n    return teams.reduce((acc, curr) => acc + teamSize - curr.students.length, 0);\n  }\n\n  private async modifyTeams(\n    teams: Partial<Team>[],\n    teamDistributionStudents: TeamDistributionStudent[],\n    removeTeams = false,\n  ) {\n    const queryRunner = this.dataSource.createQueryRunner();\n    await queryRunner.connect();\n    await queryRunner.startTransaction();\n    try {\n      await Promise.all([\n        queryRunner.manager.save(Team, teams),\n        queryRunner.manager.save(TeamDistributionStudent, teamDistributionStudents),\n      ]);\n      if (removeTeams) {\n        await queryRunner.manager.remove(Team, teams);\n      }\n      await queryRunner.commitTransaction();\n    } catch {\n      await queryRunner.rollbackTransaction();\n      throw new InternalServerErrorException();\n    } finally {\n      await queryRunner.release();\n    }\n  }\n\n  private async removeTeams(teams: Team[], teamDistributionId: number, courseId: number) {\n    const studentIds = teams.flatMap(el => el.students.map(s => s.id));\n    const teamDistributionStudents = await this.teamDistributionStudentService.getTeamDistributionStudents(\n      studentIds,\n      teamDistributionId,\n      courseId,\n    );\n    await this.modifyTeams(\n      teams.map(t => ({ ...t, students: [] })),\n      teamDistributionStudents.map(el => ({ ...el, distributed: false })),\n      true,\n    );\n  }\n\n  private async addStudentsToAvailableTeams(\n    teams: Team[],\n    students: TeamDistributionStudent[],\n    teamsCapacity: number,\n    teamSize: number,\n  ) {\n    let capacity = teamsCapacity;\n    const distributedStudents: TeamDistributionStudent[] = [];\n    const shuffledStudents = shuffle(students);\n\n    while (capacity > 0 && shuffledStudents.length > 0) {\n      const team = teams.find(el => el.students.length < teamSize);\n      const teamDistributionStudent = shuffledStudents.pop();\n      if (teamDistributionStudent && team) {\n        team?.students.push(teamDistributionStudent.student);\n        distributedStudents.push(teamDistributionStudent);\n      }\n      capacity--;\n    }\n    await this.modifyTeams(\n      teams,\n      distributedStudents.map(el => ({ ...el, distributed: true })),\n    );\n  }\n\n  private async createInitialTeams(teamsCount: number, teamDistributionId: number) {\n    const teams: Pick<Team, 'name' | 'students' | 'description' | 'password' | 'teamDistributionId'>[] =\n      await Promise.all(\n        Array(teamsCount)\n          .fill({})\n          .map(async (_, index) => {\n            const password = await this.teamService.generatePassword();\n            return {\n              name: `Random team #${index + 1}`,\n              students: [],\n              description: 'This team was created by random distribution.',\n              password,\n              teamDistributionId,\n            };\n          }),\n      );\n    return teams;\n  }\n\n  private async createRandomTeams(\n    teamDistributionStudents: TeamDistributionStudent[],\n    teamSize: number,\n    teamDistributionId: number,\n  ) {\n    const neededTeamsCount = Math.ceil(teamDistributionStudents.length / teamSize);\n    const teams = await this.createInitialTeams(neededTeamsCount, teamDistributionId);\n    // Note: The Snake Draft algorithm may not work correctly if the number of required teams is less than the team size.\n    if (neededTeamsCount < teamSize) {\n      // If so, assign all students to the teams, making sure not to exceed the team size\n      const shuffledStudents = shuffle(teamDistributionStudents);\n      shuffledStudents.forEach(el => {\n        const team = teams.find(el => el.students.length < teamSize);\n        if (team) {\n          team?.students.push(el.student);\n        }\n      });\n    } else {\n      /*\n      If not, proceed with the snake draft style distribution\nSnake Draft Algorithm:\nThe pick order is reversed each round.\nIf you have the first pick in round one, you will have the last pick in round two and the first pick in round three, and so on.\nThis helps balance the distribution of talent among the teams, allowing each team to get a chance to pick early and late in each round.\n*/\n      const countDistributionRounds = Math.ceil(teamDistributionStudents.length / teamSize);\n      teams.forEach((team, i) => {\n        const students: Student[] = [];\n        for (let round = 1; round <= countDistributionRounds; round++) {\n          let draftPick;\n          if (round % 2 === 0) {\n            draftPick = round * teams.length - i;\n          } else {\n            draftPick = (round - 1) * teams.length + (i + 1);\n          }\n          if (teamDistributionStudents[draftPick - 1]) {\n            students.push(teamDistributionStudents[draftPick - 1]!.student);\n          }\n        }\n        team.students.push(...students);\n      });\n    }\n\n    // Save teams and teamDistributionStudent\n    const updatedTeams = teams.map(t => ({ ...t, teamLeadId: t.students.sort((a, b) => a.rank - b.rank)[0]?.id }));\n    const distributedStudents = teamDistributionStudents.map(s => ({ ...s, distributed: true }));\n    await this.modifyTeams(updatedTeams, distributedStudents);\n  }\n\n  public async distributeStudents(teamDistributionId: number) {\n    const teamDistribution = await this.getById(teamDistributionId);\n    let availableTeams = await this.teamService.getTeamsAvailableForDistribute(\n      teamDistributionId,\n      teamDistribution.strictTeamSize,\n    );\n    let studentsWithoutTeam = await this.teamDistributionStudentService.getStudentsForDistribute(teamDistributionId);\n    const smallTeams = availableTeams.filter(t => t.students.length <= 1);\n    let teamsCapacity = this.getTeamCapacity(availableTeams, teamDistribution.strictTeamSize);\n\n    if (smallTeams.length && teamsCapacity > studentsWithoutTeam.length) {\n      await this.removeTeams(smallTeams, teamDistributionId, teamDistribution.courseId);\n      availableTeams = await this.teamService.getTeamsAvailableForDistribute(\n        teamDistributionId,\n        teamDistribution.strictTeamSize,\n      );\n      studentsWithoutTeam = await this.teamDistributionStudentService.getStudentsForDistribute(teamDistributionId);\n      teamsCapacity = this.getTeamCapacity(availableTeams, teamDistribution.strictTeamSize);\n    }\n    if (teamsCapacity !== 0) {\n      await this.addStudentsToAvailableTeams(\n        availableTeams,\n        studentsWithoutTeam,\n        teamsCapacity,\n        teamDistribution.strictTeamSize,\n      );\n      studentsWithoutTeam = await this.teamDistributionStudentService.getStudentsForDistribute(teamDistributionId);\n    }\n\n    if (studentsWithoutTeam.length) {\n      await this.createRandomTeams(studentsWithoutTeam, teamDistribution.strictTeamSize, teamDistributionId);\n    }\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/create-team-distribution.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, Min } from 'class-validator';\n\nexport class CreateTeamDistributionDto {\n  @IsString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public name: string;\n\n  @IsDateString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public startDate: Date;\n\n  @IsDateString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public endDate: Date;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty()\n  public description: string;\n\n  @IsString()\n  @IsOptional()\n  @ApiProperty()\n  public descriptionUrl: string;\n\n  @IsNumber()\n  @IsNotEmpty()\n  @Min(2)\n  @ApiProperty()\n  public minTeamSize: number;\n\n  @IsNumber()\n  @IsNotEmpty()\n  @ApiProperty()\n  public maxTeamSize: number;\n\n  @IsNumber()\n  @IsNotEmpty()\n  @Min(2)\n  @ApiProperty()\n  public strictTeamSize: number;\n\n  @IsBoolean()\n  @IsNotEmpty()\n  @ApiProperty()\n  public strictTeamSizeMode: boolean;\n\n  @IsNumber()\n  @IsNotEmpty()\n  @ApiProperty()\n  public minTotalScore: number;\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/create-team.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { IsNotEmpty, IsOptional, IsString } from 'class-validator';\n\nexport class CreateTeamDto {\n  @IsString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public name: string;\n\n  @IsString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public description: string;\n\n  @IsString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public chatLink: string;\n\n  @ApiPropertyOptional({ type: [Number] })\n  @IsOptional()\n  public studentIds?: number[];\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/index.ts",
    "content": "export * from './create-team-distribution.dto';\nexport * from './create-team.dto';\nexport * from './join-team.dto';\nexport * from './team-distribution-student.dto';\nexport * from './team-distribution.dto';\nexport * from './team.dto';\nexport * from './update-team-distribution.dto';\nexport * from './update-team-dto';\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/join-team.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class JoinTeamDto {\n  @IsString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public password: string;\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/team-distribution-student.dto.ts",
    "content": "import { Student } from '@entities/index';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { PersonDto } from 'src/core/dto';\nimport { PaginationMeta } from 'src/core/paginate';\nimport { PaginationMetaDto } from 'src/core/paginate/dto/Paginate.dto';\nimport { Discord } from 'src/profile/dto';\n\nexport class TeamDistributionStudentDto {\n  constructor(student: Student) {\n    this.id = student.id;\n    this.fullName = PersonDto.getName({ firstName: student.user.firstName, lastName: student.user.lastName });\n    this.cvLink = student.user.cvLink ?? undefined;\n    this.discord = student.user.discord;\n    this.telegram = student.user.contactsTelegram ?? undefined;\n    this.email = student.user.contactsEmail ?? undefined;\n    this.githubId = student.user.githubId;\n    this.rank = student.rank;\n    this.totalScore = student.totalScore;\n    this.location = `${student.user.cityName ? `${student.user.cityName},` : ''}${\n      student.user.countryName ? ` ${student.user.countryName}` : ''\n    }`;\n    this.cvUuid = student.user.resume?.find(e => e.userId === student.user.id)?.uuid ?? undefined;\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public fullName: string;\n\n  @ApiProperty()\n  public cvLink?: string;\n\n  @ApiProperty({ nullable: true, type: Discord })\n  discord: Discord | null;\n\n  @ApiProperty()\n  public telegram?: string;\n\n  @ApiProperty()\n  public email?: string;\n\n  @ApiProperty()\n  public githubId: string;\n\n  @ApiProperty()\n  public rank: number;\n\n  @ApiProperty()\n  public totalScore: number;\n\n  @ApiProperty()\n  public location: string;\n\n  @ApiProperty()\n  public cvUuid?: string;\n}\n\nexport class StudentsWithoutTeamDto {\n  constructor(students: Student[], paginationMeta: PaginationMeta) {\n    this.content = students.map(s => new TeamDistributionStudentDto(s));\n    this.pagination = new PaginationMetaDto(paginationMeta);\n  }\n\n  @ApiProperty({ type: [TeamDistributionStudentDto] })\n  content: TeamDistributionStudentDto[];\n\n  @ApiProperty({ type: PaginationMetaDto })\n  pagination: PaginationMetaDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/team-distribution.dto.ts",
    "content": "import { TeamDistribution } from '@entities/teamDistribution';\nimport { ApiProperty, ApiResponse } from '@nestjs/swagger';\nimport { IsOptional } from 'class-validator';\nimport { registrationStatusEnum } from '../team-distribution.service';\nimport { TeamDto } from './team.dto';\n\nexport class TeamDistributionDto {\n  constructor(teamDistribution: TeamDistribution & { registrationStatus?: string }) {\n    this.id = teamDistribution.id;\n    this.name = teamDistribution.name;\n    this.startDate = teamDistribution.startDate;\n    this.endDate = teamDistribution.endDate;\n    this.description = teamDistribution.description;\n    this.minTeamSize = teamDistribution.minTeamSize;\n    this.maxTeamSize = teamDistribution.maxTeamSize;\n    this.strictTeamSize = teamDistribution.strictTeamSize;\n    this.strictTeamSizeMode = teamDistribution.strictTeamSizeMode;\n    this.minTotalScore = teamDistribution.minTotalScore;\n    this.descriptionUrl = teamDistribution.descriptionUrl;\n    this.registrationStatus = teamDistribution.registrationStatus ?? null;\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty({ enum: registrationStatusEnum })\n  registrationStatus: string | null;\n\n  @ApiProperty()\n  public startDate: Date;\n\n  @ApiProperty()\n  public endDate: Date;\n\n  @ApiProperty()\n  public description: string;\n\n  @ApiProperty()\n  public descriptionUrl: string;\n\n  @ApiProperty()\n  public minTeamSize: number;\n\n  @ApiProperty()\n  public maxTeamSize: number;\n\n  @ApiProperty()\n  public strictTeamSize: number;\n\n  @ApiProperty()\n  public strictTeamSizeMode: boolean;\n\n  @ApiProperty()\n  public minTotalScore: number;\n}\n\n@ApiResponse({})\nexport class TeamDistributionDetailedDto {\n  constructor(distribution: TeamDistribution, teamsCount: number, studentsWithoutTeamCount: number, team?: TeamDto) {\n    this.studentsWithoutTeamCount = studentsWithoutTeamCount;\n    this.teamsCount = teamsCount;\n    this.id = distribution.id;\n    this.name = distribution.name;\n    this.myTeam = team;\n    this.minTeamSize = distribution.minTeamSize;\n    this.maxTeamSize = distribution.maxTeamSize;\n    this.strictTeamSize = distribution.strictTeamSize;\n    this.strictTeamSizeMode = distribution.strictTeamSizeMode;\n    this.courseId = distribution.courseId;\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public courseId: number;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public studentsWithoutTeamCount: number;\n\n  @ApiProperty()\n  public teamsCount: number;\n\n  @ApiProperty()\n  @IsOptional()\n  public myTeam?: TeamDto;\n\n  @ApiProperty()\n  public minTeamSize: number;\n\n  @ApiProperty()\n  public maxTeamSize: number;\n\n  @ApiProperty()\n  public strictTeamSize: number;\n\n  @ApiProperty()\n  public strictTeamSizeMode: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/team.dto.ts",
    "content": "import { Team } from '@entities/team';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { PaginationMeta } from 'src/core/paginate';\nimport { PaginationMetaDto } from 'src/core/paginate/dto/Paginate.dto';\nimport { TeamDistributionStudentDto } from './team-distribution-student.dto';\n\nexport class TeamPasswordDto {\n  constructor(team: { id: number; password: string }) {\n    this.password = `${team.id}_${team.password}`;\n  }\n\n  @ApiProperty()\n  public password: string;\n}\n\nexport class TeamInfoDto {\n  constructor(team: Team) {\n    this.id = team.id;\n    this.name = team.name;\n    this.chatLink = team.chatLink;\n    this.description = team.description;\n    this.teamLeadId = team.teamLeadId;\n    this.teamDistributionId = team.teamDistributionId;\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public chatLink: string;\n\n  @ApiProperty()\n  public description: string;\n\n  @ApiProperty()\n  public teamLeadId: number;\n\n  @ApiProperty()\n  public teamDistributionId: number;\n}\n\nexport class TeamDto extends TeamInfoDto {\n  constructor(team: Team) {\n    super(team);\n    this.students = team.students.map(st => new TeamDistributionStudentDto(st)).sort((a, b) => a.rank - b.rank);\n  }\n\n  @ApiProperty({ type: [TeamDistributionStudentDto] })\n  public students: TeamDistributionStudentDto[];\n}\n\nexport class TeamsDto {\n  constructor(teams: Team[], paginationMeta: PaginationMeta) {\n    this.content = teams.map(t => new TeamDto(t));\n    this.pagination = new PaginationMetaDto(paginationMeta);\n  }\n\n  @ApiProperty({ type: [TeamDto] })\n  content: TeamDto[];\n\n  @ApiProperty({ type: PaginationMetaDto })\n  pagination: PaginationMetaDto;\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/update-team-distribution.dto.ts",
    "content": "import { CreateTeamDistributionDto } from './create-team-distribution.dto';\n\nexport class UpdateTeamDistributionDto extends CreateTeamDistributionDto {}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/dto/update-team-dto.ts",
    "content": "import { CreateTeamDto } from './create-team.dto';\n\nexport class UpdateTeamDto extends CreateTeamDto {}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/registered-student-guard.ts",
    "content": "import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';\nimport { CourseRole, CurrentRequest } from 'src/auth';\nimport { TeamDistributionStudentService } from './team-distribution-student.service';\n\n@Injectable()\nexport class RegisteredStudentOrPowerUserGuard implements CanActivate {\n  constructor(private teamDistributionStudentService: TeamDistributionStudentService) {}\n\n  async canActivate(context: ExecutionContext): Promise<boolean> {\n    const [request] = context.getArgs<[CurrentRequest]>();\n    const courseId = Number(request.params.courseId);\n    const studentId = request.user.courses[courseId]?.studentId;\n    const distributionId = Number(request.params.id);\n    const isPowerUser =\n      request.user.isAdmin ||\n      request.user.courses[courseId]?.roles.includes(CourseRole.Manager) ||\n      request.user.courses[courseId]?.roles.includes(CourseRole.Dementor);\n\n    if (isPowerUser) {\n      return true;\n    }\n\n    if (!courseId || !studentId) {\n      throw new UnauthorizedException();\n    }\n\n    const registration = await this.teamDistributionStudentService.getTeamDistributionStudent(\n      studentId,\n      distributionId,\n    );\n\n    if (!registration?.active && !registration?.distributed) {\n      throw new UnauthorizedException();\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team-distribution-student.service.test.ts",
    "content": "import { Student } from '@entities/student';\nimport { TeamDistribution } from '@entities/teamDistribution';\nimport { TeamDistributionStudent } from '@entities/teamDistributionStudent';\nimport { BadRequestException } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { TeamDistributionStudentService } from './team-distribution-student.service';\n\ndescribe('TeamDistributionStudentService', () => {\n  let service: TeamDistributionStudentService;\n\n  const repository = {\n    findOne: vi.fn(),\n    save: vi.fn(),\n    update: vi.fn(),\n  };\n\n  const studentRepository = {\n    findOneOrFail: vi.fn(),\n  };\n\n  const teamDistributionRepository = {};\n\n  const distribution: TeamDistribution = {\n    id: 1,\n    courseId: 2,\n    startDate: new Date('2026-01-01T10:00:00.000Z'),\n    endDate: new Date('2026-01-01T12:00:00.000Z'),\n    minTotalScore: 100,\n  } as TeamDistribution;\n\n  beforeEach(async () => {\n    vi.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        TeamDistributionStudentService,\n        {\n          provide: getRepositoryToken(TeamDistributionStudent),\n          useValue: repository,\n        },\n        {\n          provide: getRepositoryToken(Student),\n          useValue: studentRepository,\n        },\n        {\n          provide: getRepositoryToken(TeamDistribution),\n          useValue: teamDistributionRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<TeamDistributionStudentService>(TeamDistributionStudentService);\n  });\n\n  afterEach(() => {\n    vi.useRealTimers();\n  });\n\n  it('should throw when adding student outside distribution period', async () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T09:00:00.000Z'));\n\n    await expect(service.addStudentToTeamDistribution(10, distribution, 2)).rejects.toThrow(BadRequestException);\n    expect(repository.findOne).not.toHaveBeenCalled();\n  });\n\n  it('should allow adding student exactly at start date', async () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T10:00:00.000Z'));\n\n    repository.findOne.mockResolvedValueOnce(null);\n    studentRepository.findOneOrFail.mockResolvedValueOnce({ totalScore: 120 } as Student);\n    repository.save.mockResolvedValueOnce({});\n\n    await service.addStudentToTeamDistribution(10, distribution, 2);\n\n    expect(repository.save).toHaveBeenCalledWith({\n      studentId: 10,\n      courseId: 2,\n      teamDistributionId: 1,\n    });\n  });\n\n  it('should throw when student score is below threshold', async () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T11:00:00.000Z'));\n\n    repository.findOne.mockResolvedValueOnce(null);\n    studentRepository.findOneOrFail.mockResolvedValueOnce({ totalScore: 99 } as Student);\n\n    await expect(service.addStudentToTeamDistribution(10, distribution, 2)).rejects.toThrow(\n      'less than the input threshold',\n    );\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team-distribution-student.service.ts",
    "content": "import { Student, TeamDistributionStudent } from '@entities/index';\nimport { TeamDistribution } from '@entities/teamDistribution';\nimport { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Brackets, FindOptionsRelations, In, Repository } from 'typeorm';\nimport { paginate } from 'src/core/paginate';\n\n@Injectable()\nexport class TeamDistributionStudentService {\n  constructor(\n    @InjectRepository(TeamDistributionStudent)\n    private repository: Repository<TeamDistributionStudent>,\n    @InjectRepository(Student)\n    private studentRepository: Repository<Student>,\n    @InjectRepository(TeamDistribution)\n    private teamDistributionRepository: Repository<TeamDistribution>,\n  ) {}\n\n  public async getTeamDistributionStudent(studentId: number, teamDistributionId: number, withStudentData = false) {\n    return this.repository.findOneOrFail({\n      where: { studentId, teamDistributionId },\n      relations: withStudentData ? ['student'] : [],\n    });\n  }\n\n  private async getStudentsWithTeams(studentIds: number[]) {\n    return this.studentRepository.find({\n      where: { id: In(studentIds) },\n      relations: ['teams'],\n    });\n  }\n\n  private getDistributionById(id: number) {\n    return this.teamDistributionRepository.findOneOrFail({ where: { id } });\n  }\n\n  private verifyTeamSize(students: Student[], strictTeamSize: number) {\n    if (strictTeamSize < students.length) {\n      throw new BadRequestException('The number of students in the team has been exceeded.');\n    }\n  }\n\n  private verifyStudentTeams(students: Student[], teamDistributionId: number, teamId?: number) {\n    if (\n      students.some(student =>\n        student.teams.some(team => team.teamDistributionId === teamDistributionId && team.id !== teamId),\n      )\n    ) {\n      throw new BadRequestException('One of the students is already on the team for the current distribution');\n    }\n  }\n\n  private async verifyRegisteredStudents(\n    studentIds: number[],\n    registeredStudents: TeamDistributionStudent[],\n    teamDistributionId: number,\n    courseId: number,\n  ) {\n    if (registeredStudents.length !== studentIds.length) {\n      const notRegisteredStudentIds = studentIds.filter(id => !registeredStudents.find(el => el.studentId === id));\n      await this.addStudentsToTeamDistribution(notRegisteredStudentIds, teamDistributionId, courseId);\n    }\n  }\n\n  public async getStudentsForTeamByManager(\n    studentIds: number[],\n    teamDistributionId: number,\n    courseId: number,\n    teamId?: number,\n  ) {\n    const students = await this.getStudentsWithTeams(studentIds);\n    if (students.length === 0) {\n      throw new BadRequestException();\n    }\n    const teamDistribution = await this.getDistributionById(teamDistributionId);\n\n    this.verifyTeamSize(students, teamDistribution.strictTeamSize);\n    this.verifyStudentTeams(students, teamDistributionId, teamId);\n\n    const registeredStudents = await this.getTeamDistributionStudents(studentIds, teamDistributionId, courseId);\n\n    await this.verifyRegisteredStudents(studentIds, registeredStudents, teamDistributionId, courseId);\n    return students;\n  }\n\n  public async getTeamDistributionStudents(studentIds: number[], teamDistributionId: number, courseId: number) {\n    return this.repository.find({\n      where: { courseId, studentId: In(studentIds), teamDistributionId },\n    });\n  }\n\n  public async addStudentsToTeamDistribution(studentIds: number[], teamDistributionId: number, courseId: number) {\n    await this.repository.save(studentIds.map(id => ({ studentId: id, teamDistributionId, courseId })));\n  }\n\n  private verifyDateWithinDistributionPeriod(startDate: Date, endDate: Date) {\n    const currentDate = new Date();\n    const distributionStartDate = new Date(startDate);\n    const distributionEndDate = new Date(endDate);\n    if (currentDate < distributionStartDate || currentDate > distributionEndDate) {\n      throw new BadRequestException();\n    }\n  }\n\n  public async addStudentToTeamDistribution(\n    studentId: number,\n    teamDistribution: TeamDistribution,\n    courseId: number,\n    withVerification = true,\n  ) {\n    if (withVerification) {\n      this.verifyDateWithinDistributionPeriod(teamDistribution.startDate, teamDistribution.endDate);\n    }\n    const record = await this.repository.findOne({\n      where: { studentId, courseId, teamDistributionId: teamDistribution.id },\n    });\n\n    if (record?.active || record?.distributed) {\n      throw new BadRequestException();\n    }\n    const student = await this.studentRepository.findOneOrFail({\n      where: { id: studentId },\n    });\n\n    if (withVerification && student.totalScore < teamDistribution.minTotalScore) {\n      throw new BadRequestException('Number of points is less than the input threshold for distribution');\n    }\n\n    if (record) {\n      await this.repository.update(record.id, { active: true });\n    } else {\n      await this.repository.save({\n        studentId,\n        courseId: teamDistribution.courseId,\n        teamDistributionId: teamDistribution.id,\n      });\n    }\n  }\n\n  public async deleteStudentFromTeamDistribution(studentId: number, teamDistributionId: number) {\n    const record = await this.repository.findOne({\n      where: { studentId, teamDistributionId },\n    });\n    if (record == null) throw new NotFoundException();\n    await this.repository.update(record.id, { active: false });\n  }\n\n  public async markStudentAsDistributed(studentId: number, teamDistributionId: number) {\n    const record = await this.repository.findOne({\n      where: { studentId, teamDistributionId },\n    });\n    if (record == null) throw new NotFoundException();\n    await this.repository.update(record.id, { distributed: true });\n  }\n\n  public async findByStudentIds(studentIds: number[], teamDistributionId: number) {\n    const records = await this.repository.find({\n      where: { studentId: In(studentIds), teamDistributionId },\n    });\n    return records;\n  }\n\n  public async saveTeamDistributionStudents(teamDistributionStudents: TeamDistributionStudent[]) {\n    return this.repository.save(teamDistributionStudents);\n  }\n\n  private getUserFields(modelName = 'user') {\n    return [\n      `${modelName}.firstName`,\n      `${modelName}.lastName`,\n      `${modelName}.cvLink`,\n      `${modelName}.discord`,\n      `${modelName}.contactsTelegram`,\n      `${modelName}.contactsEmail`,\n      `${modelName}.githubId`,\n      `${modelName}.cityName`,\n      `${modelName}.countryName`,\n    ];\n  }\n\n  private getSearchString() {\n    const searchConfig = [\n      { field: '\"githubId\"' },\n      { field: '\"firstName\"' },\n      { field: '\"lastName\"' },\n      { field: '\"cityName\"' },\n      { field: '\"countryName\"' },\n    ];\n\n    return searchConfig.map(({ field }) => `\"user\".${field} ilike :search`).join(' OR ');\n  }\n\n  private getSearchConditions(search: string) {\n    return new Brackets(qb => {\n      qb.where(this.getSearchString(), { search: `${search}%` }).orWhere(\n        `CAST(user.discord AS jsonb)->>'username' ILIKE :search`,\n        { search: `${search}%` },\n      );\n    });\n  }\n\n  public async getStudentsByTeamDistributionId(distributionId: number, { search = '', page = 1, limit = 10 }) {\n    const query = this.studentRepository\n      .createQueryBuilder('student')\n      .leftJoin('student.teamDistributionStudents', 'tds')\n      .innerJoin('student.user', 'user')\n      .leftJoin('user.resume', 'resume')\n      .addSelect('resume.uuid')\n      .addSelect(this.getUserFields())\n      .where('tds.teamDistributionId = :teamDistributionId', { teamDistributionId: distributionId })\n      .andWhere('tds.active = true')\n      .andWhere('tds.distributed = false')\n      .orderBy('student.rank', 'ASC');\n\n    if (search) {\n      query.andWhere(this.getSearchConditions(search));\n    }\n    const { items: students, meta: paginationMeta } = await paginate(query, { page, limit });\n    return { students, paginationMeta };\n  }\n\n  public async getStudentWithRelations(studentId: number, relations: FindOptionsRelations<Student>) {\n    const student = await this.studentRepository.findOneOrFail({\n      where: { id: studentId },\n      relations: relations,\n    });\n    return student;\n  }\n\n  public async getStudentsForDistribute(distributionId: number) {\n    return this.repository.find({\n      where: {\n        teamDistributionId: distributionId,\n        active: true,\n        distributed: false,\n        student: {\n          isExpelled: false,\n        },\n      },\n      relations: {\n        student: true,\n      },\n      order: {\n        student: {\n          rank: 'DESC',\n        },\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team-distribution.controller.ts",
    "content": "import { Controller, Post, Body, UseGuards, ParseIntPipe, Param, Get, Delete, Put, Req, Query } from '@nestjs/common';\nimport { TeamDistributionService } from './team-distribution.service';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport {\n  TeamDistributionDetailedDto,\n  TeamDistributionDto,\n  TeamDistributionStudentDto,\n  UpdateTeamDistributionDto,\n  CreateTeamDistributionDto,\n  StudentsWithoutTeamDto,\n  TeamDto,\n} from './dto';\nimport { TeamService } from './team.service';\nimport { RegisteredStudentOrPowerUserGuard } from './registered-student-guard';\nimport { TeamDistributionStudentService } from './team-distribution-student.service';\nimport { Student } from '@entities/index';\nimport { DistributeStudentsService } from './distribute-students.service';\nimport { StudentId } from '../../core/decorators/';\n\n@Controller('courses/:courseId/team-distribution')\n@ApiTags('team distribution')\n@UseGuards(DefaultGuard, CourseGuard)\nexport class TeamDistributionController {\n  constructor(\n    private readonly teamDistributionService: TeamDistributionService,\n    private readonly teamService: TeamService,\n    private readonly teamDistributionStudentService: TeamDistributionStudentService,\n    private readonly distributeStudentsService: DistributeStudentsService,\n  ) {}\n  @Post('/')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse({ type: TeamDistributionDto })\n  @ApiOperation({ operationId: 'createTeamDistribution' })\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  public async create(@Param('courseId', ParseIntPipe) courseId: number, @Body() dto: CreateTeamDistributionDto) {\n    const data = await this.teamDistributionService.create({ courseId, ...dto });\n    return new TeamDistributionDto(data);\n  }\n\n  @Get('/')\n  @ApiOkResponse({ type: [TeamDistributionDto] })\n  @ApiOperation({ operationId: 'getCourseTeamDistributions' })\n  public async getCourseTeamDistributions(\n    @StudentId() studentId: number,\n    @Param('courseId', ParseIntPipe) courseId: number,\n  ) {\n    const data = await this.teamDistributionService.findByCourseId(courseId);\n    let student: Student | null = null;\n\n    if (studentId) {\n      student = await this.teamDistributionStudentService.getStudentWithRelations(studentId, {\n        user: true,\n        teams: true,\n        teamDistributionStudents: {\n          teamDistribution: true,\n        },\n      });\n    }\n\n    const teamDistributionsWithStatus = data.map(td =>\n      this.teamDistributionService.addStatusToDistribution(td, student),\n    );\n\n    return teamDistributionsWithStatus.map(el => new TeamDistributionDto(el));\n  }\n\n  @Delete('/:id')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse()\n  @RequiredRoles([Role.Admin, CourseRole.Manager], true)\n  @ApiOperation({ operationId: 'deleteTeamDistribution' })\n  public async delete(@Param('courseId', ParseIntPipe) _: number, @Param('id', ParseIntPipe) id: number) {\n    return this.teamDistributionService.remove(id);\n  }\n\n  @Put('/:id')\n  @UseGuards(RoleGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager], true)\n  @ApiOkResponse()\n  @ApiOperation({ operationId: 'updateTeamDistribution' })\n  public async update(\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('id', ParseIntPipe) id: number,\n    @Body() dto: UpdateTeamDistributionDto,\n  ) {\n    await this.teamDistributionService.update(id, {\n      courseId,\n      id: id,\n      ...dto,\n    });\n  }\n\n  @Post('/:id/registry')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse({ type: TeamDistributionDto })\n  @ApiOperation({ operationId: 'teamDistributionRegistry' })\n  @RequiredRoles([CourseRole.Student])\n  public async registry(\n    @StudentId() studentId: number,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    const teamDistribution = await this.teamDistributionService.getById(id);\n\n    if (studentId) {\n      await this.teamDistributionStudentService.addStudentToTeamDistribution(studentId, teamDistribution, courseId);\n    }\n  }\n\n  @Get('/:id/submit-score/:taskId')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse({ type: TeamDistributionDto })\n  @ApiOperation({ operationId: 'submitScore' })\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  public async submitScore(\n    @Req() _: CurrentRequest,\n    @Param('courseId', ParseIntPipe) _courseId: number,\n    @Param('id', ParseIntPipe) id: number,\n    @Param('taskId', ParseIntPipe) taskId: number,\n  ) {\n    await this.teamDistributionService.submitScore(id, taskId);\n  }\n\n  @Delete('/:id/registry')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse()\n  @ApiOperation({ operationId: 'teamDistributionDeleteRegistry' })\n  @RequiredRoles([CourseRole.Student])\n  public async deleteRegistry(\n    @StudentId() studentId: number,\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    if (studentId) {\n      await this.teamDistributionStudentService.deleteStudentFromTeamDistribution(studentId, id);\n    }\n  }\n\n  @Delete('/:id/students/:studentId')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse()\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  public async deleteStudentFromDistribution(\n    @Param('studentId', ParseIntPipe) studentId: number,\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    await this.teamDistributionStudentService.deleteStudentFromTeamDistribution(studentId, id);\n  }\n\n  @Get('/:id/detailed')\n  @UseGuards(RoleGuard, RegisteredStudentOrPowerUserGuard)\n  @ApiOkResponse({ type: TeamDistributionDetailedDto })\n  @ApiOperation({ operationId: 'getCourseTeamDistributionDetailed' })\n  @RequiredRoles([CourseRole.Student, CourseRole.Manager, CourseRole.Dementor, Role.Admin], true)\n  public async getCourseTeamDistributionDetailed(\n    @StudentId() studentId: number,\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    let team;\n\n    if (studentId) {\n      const student = await this.teamDistributionStudentService.getStudentWithRelations(studentId, {\n        teams: true,\n        user: true,\n      });\n      const data = student.teams.find(t => t.teamDistributionId === id);\n      if (data) {\n        team = await this.teamService.findTeamWithStudentsById(data.id);\n        team = new TeamDto(team);\n      }\n    }\n    const { teamDistribution, teamsCount, studentsWithoutTeamCount } =\n      await this.teamDistributionService.getDistributionDetailedById(id);\n    return new TeamDistributionDetailedDto(teamDistribution, teamsCount, studentsWithoutTeamCount, team);\n  }\n\n  @Get('/:id/students')\n  @UseGuards(RoleGuard, RegisteredStudentOrPowerUserGuard)\n  @ApiOkResponse({ type: [TeamDistributionStudentDto] })\n  @ApiOperation({ operationId: 'getStudentsWithoutTeam' })\n  @RequiredRoles([CourseRole.Student, CourseRole.Manager, CourseRole.Dementor, Role.Admin], true)\n  public async getStudentsWithoutTeam(\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('id', ParseIntPipe) id: number,\n    @Query('pageSize', ParseIntPipe) pageSize: number = 10,\n    @Query('current', ParseIntPipe) current: number = 1,\n    @Query('search') search: string,\n  ) {\n    const { students, paginationMeta } = await this.teamDistributionStudentService.getStudentsByTeamDistributionId(id, {\n      page: current,\n      limit: pageSize,\n      search,\n    });\n\n    return new StudentsWithoutTeamDto(students, paginationMeta);\n  }\n\n  @Post('/:id/distribution')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse()\n  @ApiOperation({ operationId: 'distributeStudentsToTeam' })\n  @RequiredRoles([CourseRole.Manager, Role.Admin], true)\n  public async distributeStudentsToTeam(\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    await this.distributeStudentsService.distributeStudents(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team-distribution.service.test.ts",
    "content": "import { Student } from '@entities/student';\nimport { TeamDistribution } from '@entities/teamDistribution';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { TeamDistributionStudent } from '@entities/teamDistributionStudent';\nimport { TeamDistributionService, registrationStatusEnum } from './team-distribution.service';\nimport { TeamService } from './team.service';\nimport { WriteScoreService } from '../score';\n\nconst buildDistribution = (data: Partial<TeamDistribution> = {}): TeamDistribution => {\n  return {\n    id: 1,\n    startDate: new Date('2026-01-01T10:00:00.000Z'),\n    endDate: new Date('2026-01-01T14:00:00.000Z'),\n    minTotalScore: 0,\n    ...data,\n  } as TeamDistribution;\n};\n\nconst buildStudent = (data: Partial<Student> = {}): Student => {\n  return {\n    id: 1,\n    isExpelled: false,\n    totalScore: 100,\n    teamDistributionStudents: [],\n    ...data,\n  } as Student;\n};\n\ndescribe('TeamDistributionService', () => {\n  let service: TeamDistributionService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        TeamDistributionService,\n        { provide: getRepositoryToken(TeamDistribution), useValue: {} },\n        { provide: getRepositoryToken(TeamDistributionStudent), useValue: {} },\n        { provide: TeamService, useValue: {} },\n        { provide: WriteScoreService, useValue: {} },\n      ],\n    }).compile();\n\n    service = module.get<TeamDistributionService>(TeamDistributionService);\n  });\n\n  afterEach(() => {\n    vi.useRealTimers();\n  });\n\n  it('should mark distribution as unavailable when student is missing', () => {\n    const distribution = buildDistribution();\n\n    const result = service.addStatusToDistribution(distribution, null);\n\n    expect(result.registrationStatus).toBe(registrationStatusEnum.Unavailable);\n  });\n\n  it('should mark distribution as future before start date', () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T09:00:00.000Z'));\n\n    const distribution = buildDistribution();\n    const student = buildStudent();\n\n    const result = service.addStatusToDistribution(distribution, student);\n\n    expect(result.registrationStatus).toBe(registrationStatusEnum.Future);\n  });\n\n  it('should mark distribution as distributed for already distributed student', () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T11:00:00.000Z'));\n\n    const distribution = buildDistribution();\n    const student = buildStudent({\n      teamDistributionStudents: [{ teamDistributionId: 1, distributed: true }] as Student['teamDistributionStudents'],\n    });\n\n    const result = service.addStatusToDistribution(distribution, student);\n\n    expect(result.registrationStatus).toBe(registrationStatusEnum.Distributed);\n  });\n\n  it('should mark distribution as completed for active registration', () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T11:00:00.000Z'));\n\n    const distribution = buildDistribution();\n    const student = buildStudent({\n      teamDistributionStudents: [{ teamDistributionId: 1, active: true }] as Student['teamDistributionStudents'],\n    });\n\n    const result = service.addStatusToDistribution(distribution, student);\n\n    expect(result.registrationStatus).toBe(registrationStatusEnum.Completed);\n  });\n\n  it('should mark distribution as available at end-date boundary', () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T14:00:00.000Z'));\n\n    const distribution = buildDistribution();\n    const student = buildStudent();\n\n    const result = service.addStatusToDistribution(distribution, student);\n\n    expect(result.registrationStatus).toBe(registrationStatusEnum.Available);\n  });\n\n  it('should mark distribution as closed after end date', () => {\n    vi.useFakeTimers();\n    vi.setSystemTime(new Date('2026-01-01T14:00:01.000Z'));\n\n    const distribution = buildDistribution();\n    const student = buildStudent();\n\n    const result = service.addStatusToDistribution(distribution, student);\n\n    expect(result.registrationStatus).toBe(registrationStatusEnum.Closed);\n  });\n});\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team-distribution.service.ts",
    "content": "import { Student, TeamDistributionStudent } from '@entities/index';\nimport { TeamDistribution } from '@entities/teamDistribution';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { TeamService } from './team.service';\nimport { SaveScoreInput, WriteScoreService } from '../score';\n\nexport enum registrationStatusEnum {\n  Available = 'available',\n  Unavailable = 'unavailable',\n  Future = 'future',\n  Completed = 'completed',\n  Distributed = 'distributed',\n  Closed = 'closed',\n}\n\n@Injectable()\nexport class TeamDistributionService {\n  constructor(\n    @InjectRepository(TeamDistribution)\n    private repository: Repository<TeamDistribution>,\n\n    @InjectRepository(TeamDistributionStudent)\n    private teamDistributionStudentRepository: Repository<TeamDistributionStudent>,\n\n    private teamService: TeamService,\n\n    private writeScoreService: WriteScoreService,\n  ) {}\n\n  public async create(data: Partial<TeamDistribution>) {\n    return this.repository.save(data);\n  }\n\n  private isStudentUnavailable(student: Student, distribution: TeamDistribution) {\n    return student.isExpelled || distribution.minTotalScore > student.totalScore;\n  }\n\n  public addStatusToDistribution(distribution: TeamDistribution, student: Student | null) {\n    if (student == null || this.isStudentUnavailable(student, distribution)) {\n      return { ...distribution, registrationStatus: registrationStatusEnum.Unavailable };\n    }\n\n    const currTimestampUTC = new Date();\n    const distributionStartDate = new Date(distribution.startDate);\n\n    if (currTimestampUTC < distributionStartDate) {\n      return { ...distribution, registrationStatus: registrationStatusEnum.Future };\n    }\n\n    if (student.teamDistributionStudents?.find(el => el.teamDistributionId === distribution.id)?.distributed) {\n      return { ...distribution, registrationStatus: registrationStatusEnum.Distributed };\n    }\n\n    if (student.teamDistributionStudents?.find(el => el.teamDistributionId === distribution.id)?.active) {\n      return { ...distribution, registrationStatus: registrationStatusEnum.Completed };\n    }\n\n    const distributionEndDate = new Date(distribution.endDate);\n    if (currTimestampUTC <= distributionEndDate && currTimestampUTC >= distributionStartDate) {\n      return { ...distribution, registrationStatus: registrationStatusEnum.Available };\n    }\n\n    if (currTimestampUTC > distributionEndDate) {\n      return { ...distribution, registrationStatus: registrationStatusEnum.Closed };\n    }\n\n    return { ...distribution, registrationStatus: registrationStatusEnum.Unavailable };\n  }\n\n  public async findByCourseId(courseId: number) {\n    return this.repository.find({\n      where: { courseId },\n      order: {\n        startDate: 'ASC',\n      },\n    });\n  }\n\n  public getById(id: number) {\n    return this.repository.findOneOrFail({ where: { id } });\n  }\n\n  public async getDistributionDetailedById(id: number) {\n    const [teamDistribution, teamsCount, studentsWithoutTeamCount] = await Promise.all([\n      this.repository.findOneOrFail({\n        where: {\n          id,\n        },\n      }),\n      this.teamService.getCountByDistributionId(id),\n      this.teamDistributionStudentRepository.count({\n        where: { teamDistributionId: id, active: true, distributed: false },\n      }),\n    ]);\n    return { teamDistribution, teamsCount, studentsWithoutTeamCount };\n  }\n\n  public async update(id: number, teamDistribution: Partial<TeamDistribution>) {\n    return this.repository.update(id, teamDistribution);\n  }\n\n  public async remove(id: number) {\n    await this.repository.delete(id);\n  }\n\n  private async getTeamDistributionStudentsWithScore(teamDistributionId: number, taskId: number) {\n    const teamDistributionStudentsWithScore = await this.teamDistributionStudentRepository\n      .createQueryBuilder('tds')\n      .leftJoinAndSelect('tds.student', 'student')\n      .leftJoinAndSelect('student.taskResults', 'tr')\n      .where('tds.teamDistributionId = :teamDistributionId', { teamDistributionId })\n      .andWhere('tr.courseTaskId = :taskId', { taskId })\n      .andWhere('tr.score > 0')\n      .getMany();\n    return teamDistributionStudentsWithScore;\n  }\n\n  public async submitScore(teamDistributionId: number, taskId: number) {\n    const allTeams = await this.teamService.findAllByDistributionId(teamDistributionId);\n    const studentsWithScore = await this.getTeamDistributionStudentsWithScore(teamDistributionId, taskId);\n\n    const newStudentTaskResults = allTeams.reduce(\n      (acc, team) => {\n        const studentsIds = team.students.map(el => el.id);\n        const studentsWithTaskResultsInTeam = studentsWithScore.filter(el => studentsIds.includes(el.studentId));\n\n        const taskResults = studentsWithTaskResultsInTeam.map(el => el.student.taskResults?.[0]);\n        const maxScore = taskResults.length ? Math.max(...taskResults.map(taskResult => taskResult?.score ?? 0)) : 0;\n        const taskResultWithMaxScore = taskResults.find(taskResult => taskResult?.score === maxScore);\n\n        const studentsWithoutMaxScore = team.students.filter(\n          student => student.id !== taskResultWithMaxScore?.studentId,\n        );\n\n        const newTaskResults = studentsWithoutMaxScore.map(student => {\n          return {\n            studentId: student.id,\n            data: {\n              score: taskResultWithMaxScore?.score ?? 0,\n              courseTaskId: taskId,\n              comment: taskResultWithMaxScore?.comment ?? 'Cross-Check score',\n            },\n          };\n        });\n        return acc.concat(newTaskResults);\n      },\n      [] as { data: SaveScoreInput; studentId: number }[],\n    );\n\n    await Promise.all(\n      newStudentTaskResults.map(taskResult =>\n        this.writeScoreService.saveScore(taskResult.studentId, taskId, taskResult.data),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team-lead-or-manager.guard.ts",
    "content": "import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';\nimport { CourseRole, CurrentRequest } from 'src/auth';\nimport { TeamService } from './team.service';\n\n@Injectable()\nexport class TeamLeadOrCourseManagerGuard implements CanActivate {\n  constructor(private teamService: TeamService) {}\n\n  async canActivate(context: ExecutionContext): Promise<boolean> {\n    const [request] = context.getArgs<[CurrentRequest]>();\n    const courseId = Number(request.params.courseId);\n    if (!courseId) {\n      throw new UnauthorizedException();\n    }\n    const isManager = request.user.isAdmin || request.user.courses[courseId]?.roles.includes(CourseRole.Manager);\n\n    if (isManager) {\n      return true;\n    }\n\n    const studentId = request.user.courses[courseId]?.studentId;\n    const teamId = Number(request.params.id);\n\n    const team = await this.teamService.findById(teamId);\n    const isTeamLead = team.teamLeadId === studentId;\n\n    if (!isTeamLead) {\n      throw new UnauthorizedException();\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team.controller.ts",
    "content": "import { Student } from '@entities/index';\nimport {\n  BadRequestException,\n  Body,\n  Controller,\n  Get,\n  Param,\n  ParseIntPipe,\n  Patch,\n  Post,\n  Query,\n  Req,\n  UseGuards,\n} from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseGuard, CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { TeamDto, TeamPasswordDto, TeamsDto, CreateTeamDto, TeamInfoDto, UpdateTeamDto, JoinTeamDto } from './dto/';\nimport { TeamDistributionStudentService } from './team-distribution-student.service';\nimport { TeamLeadOrCourseManagerGuard } from './team-lead-or-manager.guard';\nimport { TeamService } from './team.service';\nimport { StudentId } from '../../core/decorators/';\n\n@Controller('courses/:courseId/team-distribution/:distributionId/team')\n@ApiTags('team')\n@UseGuards(DefaultGuard, CourseGuard)\nexport class TeamController {\n  constructor(\n    private readonly teamService: TeamService,\n    private readonly teamDistributionStudentService: TeamDistributionStudentService,\n  ) {}\n\n  @Get('/')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse({ type: TeamsDto })\n  @ApiOperation({ operationId: 'getTeams' })\n  @RequiredRoles([CourseRole.Student, Role.Admin, CourseRole.Manager, CourseRole.Dementor], true)\n  public async getTeams(\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('distributionId', ParseIntPipe) distributionId: number,\n    @Query('pageSize', ParseIntPipe) pageSize: number = 10,\n    @Query('current', ParseIntPipe) current: number = 1,\n    @Query('search') search: string,\n  ) {\n    const { teams, paginationMeta } = await this.teamService.findByDistributionId(distributionId, {\n      page: current,\n      limit: pageSize,\n      search,\n    });\n\n    return new TeamsDto(teams, paginationMeta);\n  }\n\n  @Post('/')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse({ type: TeamDto })\n  @ApiOperation({ operationId: 'createTeam' })\n  @RequiredRoles([CourseRole.Student, Role.Admin, CourseRole.Manager], true)\n  public async create(\n    @StudentId() studentId: number,\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('distributionId', ParseIntPipe) distributionId: number,\n    @Body() dto: CreateTeamDto,\n  ) {\n    let students: Student[] = [];\n    const isManager = req.user.isAdmin || req.user.courses[courseId]?.roles.includes(CourseRole.Manager);\n    if (studentId && !isManager) {\n      const record = await this.teamDistributionStudentService.getTeamDistributionStudent(\n        studentId,\n        distributionId,\n        true,\n      );\n      if (record?.distributed) {\n        throw new BadRequestException();\n      }\n      students.push(record.student);\n    }\n\n    if (isManager) {\n      students = await this.teamDistributionStudentService.getStudentsForTeamByManager(\n        dto.studentIds ?? [],\n        distributionId,\n        courseId,\n      );\n    }\n\n    const data = await this.teamService.create({\n      teamDistributionId: distributionId,\n      students,\n      ...dto,\n    });\n    const team = await this.teamService.findTeamWithStudentsById(data.id);\n\n    if (team.students.length) {\n      const teamDistributionStudents = await this.teamDistributionStudentService.findByStudentIds(\n        team.students.map(student => student.id),\n        distributionId,\n      );\n      await this.teamDistributionStudentService.saveTeamDistributionStudents(\n        teamDistributionStudents.map(student => ({ ...student, distributed: true })),\n      );\n    }\n\n    return new TeamDto(team);\n  }\n\n  @Patch('/:id')\n  @UseGuards(RoleGuard, TeamLeadOrCourseManagerGuard)\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Student], true)\n  @ApiOperation({ operationId: 'updateTeam' })\n  @ApiOkResponse()\n  public async updateTeam(\n    @Req() req: CurrentRequest,\n    @Param('courseId', ParseIntPipe) courseId: number,\n    @Param('distributionId', ParseIntPipe) distributionId: number,\n    @Param('id', ParseIntPipe) id: number,\n    @Body() dto: UpdateTeamDto,\n  ) {\n    const isManager = req.user.isAdmin || req.user.courses[courseId]?.roles.includes(CourseRole.Manager);\n    if (isManager) {\n      await this.teamService.save(id, dto, distributionId, courseId);\n    } else {\n      await this.teamService.update(id, dto);\n    }\n  }\n\n  @Get('/:id/password')\n  @UseGuards(RoleGuard, TeamLeadOrCourseManagerGuard)\n  @ApiOkResponse({ type: TeamPasswordDto })\n  @ApiOperation({ operationId: 'getTeamPassword' })\n  @RequiredRoles([CourseRole.Student, Role.Admin, CourseRole.Manager], true)\n  public async getTeamPassword(\n    @Param('courseId', ParseIntPipe) _courseId: number,\n    @Param('distributionId', ParseIntPipe) _distributionId: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    const team = await this.teamService.findById(id);\n\n    return new TeamPasswordDto(team);\n  }\n\n  @Post('/:id/password')\n  @UseGuards(RoleGuard, TeamLeadOrCourseManagerGuard)\n  @ApiOkResponse({ type: TeamPasswordDto })\n  @ApiOperation({ operationId: 'changeTeamPassword' })\n  @RequiredRoles([CourseRole.Student, Role.Admin, CourseRole.Manager], true)\n  public async changeTeamPassword(\n    @Param('courseId', ParseIntPipe) _courseId: number,\n    @Param('distributionId', ParseIntPipe) _distributionId: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    const password = await this.teamService.generatePassword();\n    await this.teamService.updatePassword(id, password);\n\n    return new TeamPasswordDto({ id, password });\n  }\n\n  @Post('/:id/join')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse({ type: TeamInfoDto })\n  @ApiOperation({ operationId: 'joinTeam' })\n  @RequiredRoles([CourseRole.Student])\n  public async joinTeam(\n    @StudentId() studentId: number,\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('distributionId', ParseIntPipe) teamDistributionId: number,\n    @Param('id', ParseIntPipe) id: number,\n    @Body() dto: JoinTeamDto,\n  ) {\n    if (!studentId) throw new BadRequestException();\n    const team = await this.teamService.findById(id);\n    if (dto.password !== team.password) throw new BadRequestException('Invalid password');\n    if (team.teamDistribution.strictTeamSizeMode && team.teamDistribution.strictTeamSize <= team.students.length) {\n      throw new BadRequestException();\n    }\n    const teamDistributionStudent = await this.teamDistributionStudentService.getTeamDistributionStudent(\n      studentId,\n      teamDistributionId,\n      true,\n    );\n    if (teamDistributionStudent.distributed || !teamDistributionStudent.active) {\n      throw new BadRequestException();\n    }\n    await this.teamService.addStudentToTeam(team, teamDistributionStudent.student, teamDistributionId);\n    return new TeamInfoDto(team);\n  }\n\n  @Post('/:id/leave')\n  @UseGuards(RoleGuard)\n  @ApiOkResponse()\n  @ApiOperation({ operationId: 'leaveTeam' })\n  @RequiredRoles([CourseRole.Student])\n  public async leaveTeam(\n    @StudentId() studentId: number,\n    @Param('courseId', ParseIntPipe) _: number,\n    @Param('distributionId', ParseIntPipe) teamDistributionId: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    if (!studentId) throw new BadRequestException();\n    await this.teamService.deleteStudentFromTeam(id, studentId, teamDistributionId);\n    const studentsCount = await this.teamService.getStudentsCountInTeam(id);\n    if (studentsCount === 0) {\n      await this.teamService.remove(id);\n    }\n  }\n}\n"
  },
  {
    "path": "nestjs/src/courses/team-distribution/team.service.ts",
    "content": "import { Student, Team, TeamDistributionStudent } from '@entities/index';\nimport { Injectable, InternalServerErrorException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { isEqual } from 'lodash';\nimport { customAlphabet } from 'nanoid/async';\nimport { paginate } from 'src/core/paginate';\nimport { Brackets, DataSource, Repository } from 'typeorm';\nimport { UpdateTeamDto } from './dto';\nimport { TeamDistributionStudentService } from './team-distribution-student.service';\n\n@Injectable()\nexport class TeamService {\n  constructor(\n    @InjectRepository(Team)\n    private repository: Repository<Team>,\n    private teamDistributionStudentService: TeamDistributionStudentService,\n    private dataSource: DataSource,\n  ) {}\n\n  public async generatePassword(length = 6): Promise<string> {\n    const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', length);\n    const password = await nanoid();\n    return password;\n  }\n\n  public async create(data: Partial<Team>) {\n    if (data.students?.length) {\n      const [lead] = data.students.sort((a, b) => a.rank - b.rank);\n      data.teamLeadId = lead?.id;\n    }\n    const password = await this.generatePassword();\n    return this.repository.save({ ...data, password });\n  }\n\n  public async remove(id: number) {\n    await this.repository.delete(id);\n  }\n\n  public async editTeamSquad(team: Team, studentIds: number[], distributionId: number, courseId: number) {\n    const students = await this.teamDistributionStudentService.getStudentsForTeamByManager(\n      studentIds ?? [],\n      distributionId,\n      courseId,\n      team.id,\n    );\n\n    const notDistributedStudentIds = team.students\n      .filter(student => !studentIds?.includes(student.id))\n      .map(student => student.id);\n    const distributedStudentIds = studentIds?.filter(id => !team.students.find(el => el.id === id));\n\n    const studentsNeedingUpdate = await this.teamDistributionStudentService.findByStudentIds(\n      [...notDistributedStudentIds, ...distributedStudentIds],\n      distributionId,\n    );\n\n    const notDistributedStudents = studentsNeedingUpdate\n      .filter(student => notDistributedStudentIds.includes(student.studentId))\n      .map(student => ({ ...student, distributed: false }));\n    const distributedStudents = studentsNeedingUpdate\n      .filter(student => distributedStudentIds.includes(student.studentId))\n      .map(student => ({ ...student, distributed: true, active: true }));\n\n    await this.teamDistributionStudentService.saveTeamDistributionStudents([\n      ...notDistributedStudents,\n      ...distributedStudents,\n    ]);\n\n    let teamLeadId = team.teamLeadId;\n    if (notDistributedStudentIds.includes(team.teamLeadId)) {\n      teamLeadId = students.sort((a, b) => a.rank - b.rank)[0]?.id ?? team.teamLeadId;\n    }\n\n    return { students, teamLeadId };\n  }\n\n  public async findById(id: number) {\n    return this.repository.findOneOrFail({ where: { id }, relations: ['students', 'teamDistribution'] });\n  }\n\n  private getUserFields(modelName = 'user') {\n    return [\n      `${modelName}.firstName`,\n      `${modelName}.lastName`,\n      `${modelName}.cvLink`,\n      `${modelName}.discord`,\n      `${modelName}.contactsTelegram`,\n      `${modelName}.contactsEmail`,\n      `${modelName}.githubId`,\n      `${modelName}.cityName`,\n      `${modelName}.countryName`,\n    ];\n  }\n\n  private getStudentsFields(modelName = 'student') {\n    return [`${modelName}.id`, `${modelName}.rank`, `${modelName}.totalScore`];\n  }\n\n  public async getStudentsCountInTeam(id: number) {\n    const { studentsCount } = await this.repository\n      .createQueryBuilder('team')\n      .where({ id })\n      .leftJoin('team.students', 's')\n      .select('COUNT(s.id)', 'studentsCount')\n      .getRawOne();\n    return Number(studentsCount);\n  }\n\n  public async findTeamWithStudentsById(id: number) {\n    return this.repository\n      .createQueryBuilder('team')\n      .where({ id })\n      .leftJoin('team.students', 's')\n      .leftJoin('s.user', 'u')\n      .leftJoin('u.resume', 'r')\n      .addSelect(this.getStudentsFields('s'))\n      .addSelect(this.getUserFields('u'))\n      .addSelect('r.uuid')\n      .getOneOrFail();\n  }\n\n  public async save(teamId: number, dto: UpdateTeamDto, distributionId: number, courseId: number) {\n    const team = await this.findById(teamId);\n    let students = team.students;\n    let teamLeadId = team.teamLeadId;\n    if (\n      dto.studentIds &&\n      !isEqual(\n        dto.studentIds,\n        team.students.map(student => student.id),\n      )\n    ) {\n      const res = await this.editTeamSquad(team, dto.studentIds, distributionId, courseId);\n      students = res.students;\n      teamLeadId = res.teamLeadId;\n    }\n    const data = { ...team, ...dto, students, teamLeadId };\n    return this.repository.save(data);\n  }\n\n  public async update(id: number, data: UpdateTeamDto) {\n    return this.repository.update(id, data);\n  }\n\n  public async updatePassword(id: number, password: string) {\n    return this.repository.update(id, { password: password });\n  }\n\n  public async findAllByDistributionId(distributionId: number) {\n    const teams = await this.repository\n      .createQueryBuilder('team')\n      .where('team.\"teamDistributionId\" = :distributionId', { distributionId })\n      .leftJoin('team.students', 's')\n      .leftJoin('s.user', 'u')\n      .addSelect(this.getStudentsFields('s'))\n      .addSelect(this.getUserFields('u'))\n      .orderBy('team.id', 'ASC')\n      .getMany();\n\n    return teams;\n  }\n\n  public async getCountByDistributionId(distributionId: number) {\n    return this.repository.count({\n      where: { teamDistributionId: distributionId },\n    });\n  }\n\n  private getSearchString() {\n    const searchConfig = [\n      { field: '\"githubId\"' },\n      { field: '\"firstName\"' },\n      { field: '\"lastName\"' },\n      { field: '\"cityName\"' },\n      { field: '\"countryName\"' },\n    ];\n\n    return searchConfig.map(({ field }) => `\"u\".${field} ilike :search`).join(' OR ');\n  }\n\n  private getSearchConditions(search: string) {\n    return new Brackets(qb => {\n      qb.where(this.getSearchString(), { search: `${search}%` })\n        .orWhere(`CAST(u.discord AS jsonb)->>'username' ILIKE :search`, { search: `${search}%` })\n        .orWhere(`team.name ilike :search`, { search: `${search}%` });\n    });\n  }\n\n  public async findByDistributionId(distributionId: number, { search = '', page = 1, limit = 10 }) {\n    let matchingTeamIds: number[] = [];\n\n    if (search) {\n      // find all the teams that have at least one student matching the search query\n      const teamsQuery = this.repository\n        .createQueryBuilder('team')\n        .select('team.id')\n        .leftJoin('team.students', 's')\n        .leftJoin('s.user', 'u')\n        .leftJoin('u.resume', 'r')\n        .where('team.\"teamDistributionId\" = :distributionId', { distributionId })\n        .andWhere(this.getSearchConditions(search));\n\n      matchingTeamIds = (await teamsQuery.getMany()).map(team => team.id);\n    }\n\n    const query = this.repository\n      .createQueryBuilder('team')\n      .where('team.\"teamDistributionId\" = :distributionId', { distributionId })\n      .andWhere(matchingTeamIds.length > 0 ? 'team.id IN (:...matchingTeamIds)' : '1=1', { matchingTeamIds })\n      .leftJoinAndSelect('team.students', 's')\n      .leftJoinAndSelect('s.user', 'u')\n      .leftJoinAndSelect('u.resume', 'r')\n      .orderBy('team.id', 'ASC');\n\n    const { items: teams, meta: paginationMeta } = await paginate(query, { page, limit });\n    return { teams, paginationMeta };\n  }\n\n  public async deleteStudentFromTeam(teamId: number, studentId: number, teamDistributionId: number) {\n    const team = await this.findTeamWithStudentsById(teamId);\n    team.students = team.students.filter(s => s.id !== studentId);\n    if (team.teamLeadId === studentId) {\n      const [lead] = team.students.sort((a, b) => a.rank - b.rank);\n      team.teamLeadId = lead?.id ?? 0;\n    }\n    const queryRunner = this.dataSource.createQueryRunner();\n    await queryRunner.connect();\n    await queryRunner.startTransaction();\n\n    try {\n      await queryRunner.manager.save(team);\n      const record = await this.teamDistributionStudentService.getTeamDistributionStudent(\n        studentId,\n        teamDistributionId,\n      );\n      await queryRunner.manager.update(TeamDistributionStudent, record.id, { distributed: false });\n\n      await queryRunner.commitTransaction();\n    } catch {\n      await queryRunner.rollbackTransaction();\n      throw new InternalServerErrorException();\n    } finally {\n      await queryRunner.release();\n    }\n  }\n\n  public async addStudentToTeam(team: Team, student: Student, teamDistributionId: number) {\n    team.students.push(student);\n\n    const queryRunner = this.dataSource.createQueryRunner();\n    await queryRunner.connect();\n    await queryRunner.startTransaction();\n    try {\n      await queryRunner.manager.save(team);\n      const record = await this.teamDistributionStudentService.getTeamDistributionStudent(\n        student.id,\n        teamDistributionId,\n      );\n      await queryRunner.manager.update(TeamDistributionStudent, record.id, { distributed: true });\n      await queryRunner.commitTransaction();\n    } catch {\n      await queryRunner.rollbackTransaction();\n      throw new InternalServerErrorException();\n    } finally {\n      await queryRunner.release();\n    }\n  }\n\n  public async getTeamsAvailableForDistribute(teamDistributionId: number, teamSize: number) {\n    const res = await this.repository\n      .createQueryBuilder('team')\n      .where('team.\"teamDistributionId\" = :teamDistributionId', { teamDistributionId })\n      .leftJoinAndSelect('team.students', 'students')\n      .getMany();\n    return res.filter(t => t.students.length < teamSize).sort((a, b) => a.students.length - b.students.length);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/cross-check/cross-check.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { HttpModule } from '@nestjs/axios';\nimport { CourseTask } from '@entities/courseTask';\nimport { ConfigModule } from '../config/config.module';\nimport { CrossCheckService } from './cross-check.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([CourseTask]), HttpModule, ConfigModule],\n  providers: [CrossCheckService],\n})\nexport class CrossCheckModule {}\n"
  },
  {
    "path": "nestjs/src/cross-check/cross-check.service.spec.ts",
    "content": "import { HttpService } from '@nestjs/axios';\nimport { ConfigService } from '../config';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { CrossCheckService } from './cross-check.service';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { CourseTask } from '../../../server/src/models/courseTask';\n\nconst MOCK_CURRENT_TIMESTAMP = new Date('2022-03-22 00:05 UTC').getTime();\n\nconst MOCK_HOST = 'https://testhost';\n\nconst MOCK_ADMIN_USERNAME = 'TEST_USERNAME';\nconst MOCK_ADMIN_PASSWORD = 'TEST_PASSWORD';\n\nconst mockConfigService = {\n  users: {\n    root: {\n      username: MOCK_ADMIN_USERNAME,\n      password: MOCK_ADMIN_PASSWORD,\n    },\n  },\n  host: MOCK_HOST,\n};\n\nconst expectedAxiosRequestConfig = {\n  auth: {\n    username: MOCK_ADMIN_USERNAME,\n    password: MOCK_ADMIN_PASSWORD,\n  },\n};\n\nenum MockDate {\n  DateBefore = '2022-03-21T00:00:00.000Z',\n  DateAfter = '2022-03-23T00:00:00.000Z',\n}\n\nenum CrossCheckStatus {\n  Initial = 'initial',\n  Distributed = 'distributed',\n  Completed = 'completed',\n}\n\nconst tasks = [\n  {\n    id: 1,\n    courseId: 1,\n    crossCheckStatus: CrossCheckStatus.Initial,\n    studentEndDate: MockDate.DateAfter,\n    crossCheckEndDate: MockDate.DateBefore,\n  },\n  {\n    id: 2,\n    courseId: 2,\n    crossCheckStatus: CrossCheckStatus.Initial,\n    studentEndDate: MockDate.DateBefore,\n    crossCheckEndDate: MockDate.DateAfter,\n  },\n  {\n    id: 3,\n    courseId: 3,\n    crossCheckStatus: CrossCheckStatus.Initial,\n    studentEndDate: MockDate.DateBefore,\n    crossCheckEndDate: MockDate.DateBefore,\n  },\n  {\n    id: 4,\n    courseId: 4,\n    crossCheckStatus: CrossCheckStatus.Initial,\n    studentEndDate: MockDate.DateAfter,\n    crossCheckEndDate: MockDate.DateAfter,\n  },\n  {\n    id: 11,\n    courseId: 11,\n    crossCheckStatus: CrossCheckStatus.Distributed,\n    studentEndDate: MockDate.DateBefore,\n    crossCheckEndDate: MockDate.DateAfter,\n  },\n  {\n    id: 22,\n    courseId: 22,\n    crossCheckStatus: CrossCheckStatus.Distributed,\n    studentEndDate: MockDate.DateBefore,\n    crossCheckEndDate: MockDate.DateBefore,\n  },\n  {\n    id: 33,\n    courseId: 33,\n    crossCheckStatus: CrossCheckStatus.Distributed,\n    studentEndDate: MockDate.DateAfter,\n    crossCheckEndDate: MockDate.DateBefore,\n  },\n  {\n    id: 44,\n    courseId: 44,\n    crossCheckStatus: CrossCheckStatus.Distributed,\n    studentEndDate: MockDate.DateBefore,\n    crossCheckEndDate: MockDate.DateAfter,\n  },\n];\n\nconst mockPost = vi.fn(() => ({\n  pipe: vi.fn(() => []),\n}));\n\nconst mockCourseTaskRepositoryFind = vi.fn(() => tasks);\n\nconst mockCourseTaskRepositoryFactory = vi.fn(() => ({\n  find: mockCourseTaskRepositoryFind,\n}));\n\ndescribe('CrossCheckService', () => {\n  let service: CrossCheckService;\n\n  beforeAll(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        CrossCheckService,\n        {\n          provide: HttpService,\n          useValue: {\n            post: mockPost,\n          },\n        },\n        {\n          provide: ConfigService,\n          useValue: mockConfigService,\n        },\n        {\n          provide: getRepositoryToken(CourseTask),\n          useFactory: mockCourseTaskRepositoryFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<CrossCheckService>(CrossCheckService);\n  });\n\n  it('should be defined', () => {\n    expect(service).toBeDefined();\n  });\n\n  it('should have executeCronJobs method', () => {\n    expect(service).toHaveProperty('executeCronJobs');\n  });\n\n  describe('executeCronJobs should work correctly', () => {\n    beforeEach(async () => {\n      vi.spyOn(Date, 'now').mockImplementation(() => MOCK_CURRENT_TIMESTAMP);\n      await service.executeCronJobs();\n    });\n\n    it('httpService.post should be called right amout of times', () => {\n      expect(mockPost).toHaveBeenCalledTimes(4);\n    });\n\n    it('tasks with submission deadline passed should be distributed', () => {\n      expect(mockPost).toHaveBeenNthCalledWith(\n        1,\n        `${MOCK_HOST}/api/course/2/task/2/cross-check/distribution`,\n        null,\n        expectedAxiosRequestConfig,\n      );\n      expect(mockPost).toHaveBeenNthCalledWith(\n        2,\n        `${MOCK_HOST}/api/course/3/task/3/cross-check/distribution`,\n        null,\n        expectedAxiosRequestConfig,\n      );\n    });\n\n    it('tasks with cross-check deadline passed should be completed', () => {\n      expect(mockPost).toHaveBeenNthCalledWith(\n        3,\n        `${MOCK_HOST}/api/course/22/task/22/cross-check/completion`,\n        null,\n        expectedAxiosRequestConfig,\n      );\n      expect(mockPost).toHaveBeenNthCalledWith(\n        4,\n        `${MOCK_HOST}/api/course/33/task/33/cross-check/completion`,\n        null,\n        expectedAxiosRequestConfig,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/cross-check/cross-check.service.ts",
    "content": "import { Repository } from 'typeorm';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { HttpService } from '@nestjs/axios';\nimport { Cron } from '@nestjs/schedule';\nimport { ConfigService } from '../config';\nimport { isTaskNeededToStart, isTaskNeededToFinish } from './tasks-filtering';\nimport { Checker, CourseTask } from '@entities/courseTask';\nimport { from, catchError, mergeMap, EMPTY, toArray, lastValueFrom } from 'rxjs';\n\nconst ONCE_A_DAY_AT_00_05 = '5 0 * * *';\n\n@Injectable()\nexport class CrossCheckService {\n  private readonly logger = new Logger('CrossCheckService');\n\n  constructor(\n    @InjectRepository(CourseTask)\n    private readonly courseTaskRepository: Repository<CourseTask>,\n    private readonly httpService: HttpService,\n    private readonly conf: ConfigService,\n  ) {}\n\n  @Cron(ONCE_A_DAY_AT_00_05, { timeZone: 'UTC' })\n  async executeCronJobs() {\n    const { tasksToStart, tasksToFinish } = await this.getCrossCheckTasks();\n    await this.initCrossCheckAction(tasksToStart, 'distribution');\n    await this.initCrossCheckAction(tasksToFinish, 'completion');\n  }\n\n  private getInitialDataForRequest() {\n    const { username, password } = this.conf.users.root;\n    const host = this.conf.host;\n\n    return {\n      host,\n      auth: {\n        username,\n        password,\n      },\n    };\n  }\n\n  private makeCrossCheckRequest(url: string, auth: { username: string; password: string }) {\n    return this.httpService.post(url, null, {\n      auth,\n    });\n  }\n\n  private async initCrossCheckAction(courseTasks: CourseTask[], action: 'distribution' | 'completion') {\n    const { host, auth } = this.getInitialDataForRequest();\n    const baseurl = `${host}/api/course`;\n\n    const courseTaskRequests$ = from(courseTasks).pipe(\n      mergeMap(({ id, courseId }) => {\n        const requestUrl = `${baseurl}/${courseId}/task/${id}/cross-check/${action}`;\n        return this.makeCrossCheckRequest(requestUrl, auth).pipe(\n          catchError(err => {\n            this.logger.error({\n              message: `Cross-Check ${action} failed for task with id ${id}!`,\n              reason: err,\n            });\n            return EMPTY;\n          }),\n        );\n      }),\n      toArray(),\n    );\n\n    await lastValueFrom(courseTaskRequests$);\n  }\n\n  private async getCrossCheckTasks() {\n    const allCrossCheckTasks = await this.courseTaskRepository.find({\n      where: { checker: Checker.CrossCheck, disabled: false },\n    });\n\n    return {\n      tasksToStart: allCrossCheckTasks.filter(isTaskNeededToStart),\n      tasksToFinish: allCrossCheckTasks.filter(isTaskNeededToFinish),\n    };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/cross-check/tasks-filtering.ts",
    "content": "import { CourseTask, CrossCheckStatus } from '@entities/courseTask';\n\nexport const isTaskNeededToStart = ({ crossCheckStatus, studentEndDate }: CourseTask) => {\n  const currTimestampUTC = Date.now();\n  const studentEndDateTimestampUTC = Date.parse(studentEndDate as string);\n  return crossCheckStatus === CrossCheckStatus.Initial && currTimestampUTC > studentEndDateTimestampUTC;\n};\n\nexport const isTaskNeededToFinish = ({ crossCheckStatus, crossCheckEndDate }: CourseTask) => {\n  const currTimestampUTC = Date.now();\n  if (!crossCheckEndDate) return false;\n  const crossCheckEndDateTimestampUTC =\n    typeof crossCheckEndDate === 'string' ? Date.parse(crossCheckEndDate) : crossCheckEndDate.getTime();\n  return crossCheckStatus === CrossCheckStatus.Distributed && currTimestampUTC > crossCheckEndDateTimestampUTC;\n};\n"
  },
  {
    "path": "nestjs/src/devtools/devtools.controller.ts",
    "content": "import { Controller, Get, Param } from '@nestjs/common';\nimport { DevtoolsService } from './devtools.service';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DevtoolsUserDto } from './dto/devtools.users-dto';\n\n@Controller('devtools')\n@ApiTags('devtools')\nexport class DevtoolsController {\n  constructor(private readonly devtoolsService: DevtoolsService) {}\n\n  @Get('users')\n  @ApiOperation({ operationId: 'getDevUsers' })\n  @ApiOkResponse({ type: DevtoolsUserDto, isArray: true })\n  async getDevUsers() {\n    return this.devtoolsService.getUsers();\n  }\n\n  @Get('user/:githubId/login')\n  @ApiOperation({ operationId: 'getDevUserLogin' })\n  @ApiOkResponse()\n  async getDevUserLogin(@Param('githubId') githubId: string) {\n    return this.devtoolsService.getDevUserLogin({ githubId });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/devtools/devtools.module.ts",
    "content": "import { DynamicModule, Module } from '@nestjs/common';\nimport { DevtoolsController } from './devtools.controller';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { DevtoolsService } from './devtools.service';\nimport { User } from '@entities/user';\nimport { ConfigModule } from '../config';\n\n@Module({})\nexport class DevtoolsModule {\n  static forRoot(): DynamicModule {\n    const devToolsToggle = process.env.RSSCHOOL_DEV_TOOLS === 'true';\n    return {\n      module: DevtoolsModule,\n      imports: [TypeOrmModule.forFeature([User]), ConfigModule],\n      providers: devToolsToggle ? [DevtoolsService] : [],\n      controllers: devToolsToggle ? [DevtoolsController] : [],\n    };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/devtools/devtools.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { User } from '@entities/user';\nimport { ConfigService } from '../config';\n\n@Injectable()\nexport class DevtoolsService {\n  constructor(\n    @InjectRepository(User)\n    private userRepository: Repository<User>,\n    private configService: ConfigService,\n  ) {}\n\n  async getUsers() {\n    const users = await this.userRepository.find({\n      select: {\n        id: true,\n        githubId: true,\n        students: {\n          courseId: true,\n          course: {\n            alias: true,\n          },\n        },\n        mentors: {\n          courseId: true,\n          course: {\n            alias: true,\n          },\n        },\n      },\n      relations: ['students.course', 'mentors.course'],\n    });\n    return users.map(({ id, githubId, students, mentors }) => ({\n      id,\n      githubId,\n      mentor: mentors?.map(({ course }) => course?.alias) || [],\n      student: students?.map(({ course }) => course?.alias) || [],\n      students,\n    }));\n  }\n\n  async getDevUserLogin({ githubId }: { githubId: string }) {\n    this.configService.authWithDevUser(githubId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/devtools/dto/devtools.users-dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsString } from 'class-validator';\n\nexport class DevtoolsUserDto {\n  @ApiProperty()\n  @IsNumber()\n  id: number;\n\n  @ApiProperty()\n  @IsString()\n  githubId: string;\n\n  @ApiProperty({ type: Number, isArray: true })\n  @IsNumber({}, { each: true })\n  mentor: number[];\n\n  @ApiProperty({ type: Number, isArray: true })\n  @IsNumber({}, { each: true })\n  student: number[];\n}\n"
  },
  {
    "path": "nestjs/src/disciplines/disciplines.controller.test.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { DisciplinesController } from './disciplines.controller';\nimport { DisciplinesService } from './disciplines.service';\nimport { CreateDisciplineDto, DisciplineDto, UpdateDisciplineDto } from './dto';\n\nconst mockId = 1;\n\nconst mockDiscipline = {\n  id: mockId,\n  name: 'NodeJs',\n  createdDate: '1684609801766',\n  updatedDate: '1684609810052',\n  deletedDate: '1684609892046',\n};\n\nconst mockCreate = vi.fn(() => Promise.resolve(mockDiscipline));\nconst mockGetAll = vi.fn(() => Promise.resolve([mockDiscipline, mockDiscipline]));\nconst mockDelete = vi.fn(() => Promise.resolve());\nconst mockUpdate = vi.fn(() => Promise.resolve(mockDiscipline));\n\nconst mockDisciplinesServiceFactory = vi.fn(() => ({\n  create: mockCreate,\n  getAll: mockGetAll,\n  delete: mockDelete,\n  update: mockUpdate,\n}));\n\ndescribe('DisciplinesController', () => {\n  let controller: DisciplinesController;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      controllers: [DisciplinesController],\n      providers: [{ provide: DisciplinesService, useFactory: mockDisciplinesServiceFactory }],\n    }).compile();\n\n    controller = module.get<DisciplinesController>(DisciplinesController);\n  });\n\n  describe('create', () => {\n    it('should create a new discipline', async () => {\n      const mockCreateDisciplineDto = new CreateDisciplineDto();\n\n      const result = await controller.create(mockCreateDisciplineDto);\n\n      expect(result).toEqual(new DisciplineDto(mockDiscipline));\n      expect(mockCreate).toHaveBeenCalledWith(mockCreateDisciplineDto);\n    });\n  });\n\n  describe('getAll', () => {\n    it('should get all disciplines', async () => {\n      const result = await controller.getAll();\n\n      expect(result).toEqual([new DisciplineDto(mockDiscipline), new DisciplineDto(mockDiscipline)]);\n      expect(mockGetAll).toHaveBeenCalled();\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete a discipline', async () => {\n      await controller.delete(mockId);\n\n      expect(mockDelete).toHaveBeenCalledWith(mockId);\n    });\n  });\n\n  describe('update', () => {\n    it('should update a discipline', async () => {\n      const mockUpdateDisciplineDto = new UpdateDisciplineDto();\n\n      const result = await controller.update(mockId, mockUpdateDisciplineDto);\n\n      expect(result).toEqual(new DisciplineDto(mockDiscipline));\n      expect(mockUpdate).toHaveBeenCalledWith(mockId, mockUpdateDisciplineDto);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/disciplines/disciplines.controller.ts",
    "content": "import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { DisciplinesService } from './disciplines.service';\nimport { CreateDisciplineDto, DisciplineDto, DisciplineIdsDto, UpdateDisciplineDto } from './dto';\n\n@Controller('disciplines')\n@ApiTags('disciplines')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class DisciplinesController {\n  constructor(private readonly service: DisciplinesService) {}\n\n  @Post('/')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'createDiscipline' })\n  @ApiOkResponse({ type: DisciplineDto })\n  public async create(@Body() dto: CreateDisciplineDto) {\n    const data = await this.service.create(dto);\n    return new DisciplineDto(data);\n  }\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getDisciplines' })\n  @ApiOkResponse({ type: [DisciplineDto] })\n  public async getAll() {\n    const items = await this.service.getAll();\n    return items.map(item => new DisciplineDto(item));\n  }\n\n  @Post('/ids')\n  @ApiOperation({ operationId: 'getDisciplinesByIds' })\n  @ApiOkResponse({ type: [DisciplineDto] })\n  public async getByIds(@Body() dto: DisciplineIdsDto) {\n    const items = await this.service.getByIds(dto.ids);\n    return items.map(item => new DisciplineDto(item));\n  }\n\n  @Delete('/:id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'deleteDiscipline' })\n  public async delete(@Param('id', ParseIntPipe) id: number) {\n    return this.service.delete(id);\n  }\n\n  @Patch('/:id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'updateDiscipline' })\n  @ApiOkResponse({ type: DisciplineDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateDisciplineDto) {\n    const data = await this.service.update(id, dto);\n    return new DisciplineDto(data);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/disciplines/disciplines.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Discipline } from '@entities/discipline';\nimport { DisciplinesController } from './disciplines.controller';\nimport { DisciplinesService } from './disciplines.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Discipline])],\n  controllers: [DisciplinesController],\n  providers: [DisciplinesService],\n  exports: [DisciplinesService],\n})\nexport class DisciplinesModule {}\n"
  },
  {
    "path": "nestjs/src/disciplines/disciplines.service.test.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { FindOptionsWhere, In } from 'typeorm';\nimport { DisciplinesService } from './disciplines.service';\nimport { Discipline } from '@entities/discipline';\nimport { CreateDisciplineDto, UpdateDisciplineDto } from './dto';\nimport { getRepositoryToken } from '@nestjs/typeorm';\n\nconst mockDiscipline = {\n  foo: 'bar',\n} as unknown as Discipline;\n\nconst mockFindResponse = [mockDiscipline, mockDiscipline];\nconst mockFindOneByResponse = mockDiscipline;\nconst mockSaveResponse = { id: 1 };\nconst mockSoftDeleteResponse = { a: 1 };\n\nconst mockId = 1;\n\nconst mockFind = vi.fn(() => Promise.resolve(mockFindResponse));\nconst mockFindOneBy = vi.fn(() => Promise.resolve(mockFindOneByResponse));\nconst mockFindOneByOrFail = vi.fn(() => Promise.resolve(mockFindOneByResponse));\nconst mockSave = vi.fn(() => Promise.resolve(mockSaveResponse));\nconst mockUpdate = vi.fn(() => Promise.resolve(mockSaveResponse));\nconst mockSoftDelete = vi.fn(() => Promise.resolve(mockSoftDeleteResponse));\n\nconst mockDisciplinesRepositoryFactory = vi.fn(() => ({\n  find: mockFind,\n  findOneBy: mockFindOneBy,\n  findOneByOrFail: mockFindOneByOrFail,\n  save: mockSave,\n  update: mockUpdate,\n  softDelete: mockSoftDelete,\n}));\ndescribe('DisciplinesService', () => {\n  let service: DisciplinesService;\n\n  beforeEach(async () => {\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        DisciplinesService,\n        {\n          provide: getRepositoryToken(Discipline),\n          useFactory: mockDisciplinesRepositoryFactory,\n        },\n      ],\n    }).compile();\n\n    service = module.get<DisciplinesService>(DisciplinesService);\n  });\n\n  describe('getAll', () => {\n    it('should return all disciplines', async () => {\n      const result = await service.getAll();\n\n      expect(mockFind).toHaveBeenCalled();\n      expect(result).toEqual(mockFindResponse);\n    });\n  });\n\n  describe('getById', () => {\n    it('should return a discipline by id', async () => {\n      const result = await service.getById(mockId);\n\n      expect(result).toEqual(mockFindOneByResponse);\n      expect(mockFindOneBy).toHaveBeenCalledWith({ id: mockId });\n    });\n  });\n\n  describe('getByIds', () => {\n    it('should return disciplines by ids', async () => {\n      const mockIds = [1, 2];\n      const mockFilter = { bar: 'baz' } as FindOptionsWhere<Discipline>;\n\n      const result = await service.getByIds(mockIds, mockFilter);\n\n      expect(result).toEqual(mockFindResponse);\n      expect(mockFind).toHaveBeenCalledWith({\n        where: { id: In(mockIds), ...mockFilter },\n      });\n    });\n  });\n\n  describe('create', () => {\n    it('should create a new discipline', async () => {\n      const mockData = {} as CreateDisciplineDto;\n\n      const result = await service.create(mockData);\n\n      expect(result).toEqual(mockDiscipline);\n      expect(mockSave).toHaveBeenCalledWith(mockData);\n      expect(mockFindOneByOrFail).toHaveBeenCalledWith({ id: mockSaveResponse.id });\n    });\n  });\n\n  describe('update', () => {\n    it('should update a discipline', async () => {\n      const mockData = {} as UpdateDisciplineDto;\n\n      const result = await service.update(mockId, mockData);\n\n      expect(result).toEqual(mockDiscipline);\n      expect(mockUpdate).toHaveBeenCalledWith(mockId, mockData);\n      expect(mockFindOneByOrFail).toHaveBeenCalledWith({ id: mockId });\n    });\n  });\n\n  describe('delete', () => {\n    it('should delete a discipline', async () => {\n      await service.delete(mockId);\n\n      expect(mockSoftDelete).toHaveBeenCalledWith(mockId);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/disciplines/disciplines.service.ts",
    "content": "import { Discipline } from '@entities/discipline';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { FindOptionsWhere, In, Repository } from 'typeorm';\nimport { CreateDisciplineDto, UpdateDisciplineDto } from './dto';\n\n@Injectable()\nexport class DisciplinesService {\n  constructor(\n    @InjectRepository(Discipline)\n    private repository: Repository<Discipline>,\n  ) {}\n\n  public async getAll() {\n    return this.repository.find();\n  }\n\n  public async getById(id: number) {\n    return this.repository.findOneBy({ id });\n  }\n\n  public async getByIds(ids: number[], filter?: FindOptionsWhere<Discipline>) {\n    return this.repository.find({\n      where: {\n        id: In(ids),\n        ...filter,\n      },\n    });\n  }\n\n  public async create(data: CreateDisciplineDto) {\n    const { id } = await this.repository.save(data);\n    return this.repository.findOneByOrFail({ id });\n  }\n\n  public async update(id: number, data: UpdateDisciplineDto) {\n    await this.repository.update(id, data);\n    return this.repository.findOneByOrFail({ id });\n  }\n\n  public async delete(id: number): Promise<void> {\n    await this.repository.softDelete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/disciplines/dto/create-discipline.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class CreateDisciplineDto {\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  name: string;\n}\n"
  },
  {
    "path": "nestjs/src/disciplines/dto/discipline-ids.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsNumber } from 'class-validator';\n\nexport class DisciplineIdsDto {\n  @ApiProperty({\n    type: [Number],\n  })\n  @IsArray()\n  @IsNumber({}, { each: true })\n  ids: number[];\n}\n"
  },
  {
    "path": "nestjs/src/disciplines/dto/discipline.dto.ts",
    "content": "import { Discipline } from '@entities/discipline';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class DisciplineDto {\n  constructor(discipline: Discipline) {\n    this.id = discipline.id;\n    this.name = discipline.name;\n    this.createdDate = discipline.createdDate;\n    this.updatedDate = discipline.updatedDate;\n  }\n\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public createdDate: string;\n\n  @ApiProperty()\n  public updatedDate: string;\n}\n"
  },
  {
    "path": "nestjs/src/disciplines/dto/index.ts",
    "content": "export * from './create-discipline.dto';\nexport * from './update-discipline.dto';\nexport * from './discipline.dto';\nexport * from './discipline-ids.dto';\n"
  },
  {
    "path": "nestjs/src/disciplines/dto/update-discipline.dto.ts",
    "content": "import { CreateDisciplineDto } from './create-discipline.dto';\n\nexport class UpdateDisciplineDto extends CreateDisciplineDto {}\n"
  },
  {
    "path": "nestjs/src/disciplines/index.ts",
    "content": "export * from './disciplines.module';\n"
  },
  {
    "path": "nestjs/src/discord-servers/discord-servers.controller.ts",
    "content": "import { Controller, Get, Post, Put, Delete, Param, Body, ParseIntPipe, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { DiscordServersService } from './discord-servers.service';\nimport { DiscordServerDto, CreateDiscordServerDto, UpdateDiscordServerDto } from './dto';\nimport { IdNameDto } from 'src/core/dto';\n\n@Controller('discord-servers')\n@ApiTags('discord-servers')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class DiscordServersController {\n  constructor(private readonly service: DiscordServersService) {}\n\n  @Post()\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'createDiscordServer' })\n  @ApiOkResponse({ type: DiscordServerDto })\n  public async create(@Body() dto: CreateDiscordServerDto) {\n    const data = await this.service.create(dto);\n    return new DiscordServerDto(data);\n  }\n\n  @Get()\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'getDiscordServers' })\n  @ApiOkResponse({ type: [DiscordServerDto] })\n  public async getAll() {\n    const items = await this.service.getAll();\n    return items.map(item => new DiscordServerDto(item));\n  }\n\n  @Get('reduced')\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'getReducedDiscordServers' })\n  @ApiOkResponse({ type: [IdNameDto] })\n  public async getReducedAll() {\n    const items = await this.service.getAll();\n    return items.map(item => new IdNameDto(item));\n  }\n\n  @Get(':courseId/invite/:id')\n  @RequiredRoles([Role.Admin, CourseRole.Mentor], true)\n  @ApiOperation({ operationId: 'getInviteLinkByDiscordServerId' })\n  @ApiOkResponse({ type: String })\n  public async getInviteLinkById(\n    @Param('courseId', ParseIntPipe) _courseId: number,\n    @Param('id', ParseIntPipe) id: number,\n  ) {\n    const item = await this.service.getById(id);\n    return item?.mentorsChatUrl;\n  }\n\n  @Put(':id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'updateDiscordServer' })\n  @ApiOkResponse({ type: DiscordServerDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateDiscordServerDto) {\n    const data = await this.service.update(id, dto);\n    return new DiscordServerDto(data);\n  }\n\n  @Delete(':id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'deleteDiscordServer' })\n  @ApiOkResponse({ type: DiscordServerDto })\n  public async delete(@Param('id', ParseIntPipe) id: number) {\n    return this.service.delete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/discord-servers/discord-servers.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { DiscordServer } from '@entities/discordServer';\nimport { DiscordServersController } from './discord-servers.controller';\nimport { DiscordServersService } from './discord-servers.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([DiscordServer])],\n  controllers: [DiscordServersController],\n  providers: [DiscordServersService],\n})\nexport class DiscordServersModule {}\n"
  },
  {
    "path": "nestjs/src/discord-servers/discord-servers.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { DiscordServer } from '@entities/discordServer';\nimport { CreateDiscordServerDto, UpdateDiscordServerDto } from './dto';\n\n@Injectable()\nexport class DiscordServersService {\n  constructor(\n    @InjectRepository(DiscordServer)\n    private repository: Repository<DiscordServer>,\n  ) {}\n\n  public getAll() {\n    return this.repository.find();\n  }\n\n  public async getById(id: number) {\n    return this.repository.findOneBy({ id });\n  }\n\n  public create(data: CreateDiscordServerDto) {\n    return this.repository.save(data);\n  }\n\n  public update(id: number, data: UpdateDiscordServerDto) {\n    return this.repository.save({ id, ...data });\n  }\n\n  public async delete(id: number): Promise<void> {\n    await this.repository.delete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/discord-servers/dto/create-discord-server.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class CreateDiscordServerDto {\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  gratitudeUrl: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  mentorsChatUrl: string;\n}\n"
  },
  {
    "path": "nestjs/src/discord-servers/dto/discord-server.dto.ts",
    "content": "import { DiscordServer } from '@entities/discordServer';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class DiscordServerDto {\n  constructor(discordServer: DiscordServer) {\n    this.id = discordServer.id;\n    this.createdDate = discordServer.createdDate;\n    this.updatedDate = discordServer.updatedDate;\n    this.name = discordServer.name;\n    this.gratitudeUrl = discordServer.gratitudeUrl;\n    this.mentorsChatUrl = discordServer.mentorsChatUrl;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  createdDate: number;\n\n  @ApiProperty()\n  updatedDate: number;\n\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  gratitudeUrl: string;\n\n  @ApiProperty({ nullable: true, type: String })\n  mentorsChatUrl: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/discord-servers/dto/index.ts",
    "content": "export * from './discord-server.dto';\nexport * from './create-discord-server.dto';\nexport * from './update-discord-server.dto';\n"
  },
  {
    "path": "nestjs/src/discord-servers/dto/update-discord-server.dto.ts",
    "content": "import { CreateDiscordServerDto } from './create-discord-server.dto';\n\nexport class UpdateDiscordServerDto extends CreateDiscordServerDto {}\n"
  },
  {
    "path": "nestjs/src/events/dto/create-event.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';\nimport { EventType } from 'src/courses/course-events/dto/course-event.dto';\n\nexport class CreateEventDto {\n  @IsString()\n  @IsNotEmpty()\n  @ApiProperty()\n  public name: string;\n\n  @IsNotEmpty()\n  @IsEnum(EventType)\n  @ApiProperty()\n  public type: EventType;\n\n  @IsNumber()\n  @IsNotEmpty()\n  @ApiProperty()\n  public disciplineId: number;\n\n  @IsString()\n  @ApiProperty()\n  @IsOptional()\n  public descriptionUrl: string;\n\n  @IsString()\n  @ApiProperty()\n  @IsOptional()\n  public description: string;\n}\n"
  },
  {
    "path": "nestjs/src/events/dto/event.dto.ts",
    "content": "import { Event } from '@entities/event';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IdNameDto } from 'src/core/dto';\nimport { EventType } from 'src/courses/course-events/dto/course-event.dto';\n\nexport class EventDto {\n  constructor(event: Event) {\n    this.id = event.id;\n    this.name = event.name;\n    this.descriptionUrl = event.descriptionUrl;\n    this.description = event.description;\n    this.type = event.type as EventType;\n    this.discipline = event.discipline ? new IdNameDto(event.discipline) : null;\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty({ nullable: true, type: String })\n  public descriptionUrl: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public description: string | null;\n\n  @ApiProperty({ enum: EventType })\n  public type: EventType;\n\n  @ApiProperty({ type: IdNameDto, nullable: true })\n  public discipline: IdNameDto | null;\n}\n"
  },
  {
    "path": "nestjs/src/events/dto/index.ts",
    "content": "export * from './event.dto';\nexport * from './create-event.dto';\nexport * from './update-event.dto';\n"
  },
  {
    "path": "nestjs/src/events/dto/update-event.dto.ts",
    "content": "import { CreateEventDto } from './create-event.dto';\n\nexport class UpdateEventDto extends CreateEventDto {}\n"
  },
  {
    "path": "nestjs/src/events/events.controller.ts",
    "content": "import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';\nimport { EventsService } from './events.service';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { CreateEventDto, EventDto, UpdateEventDto } from './dto';\n\n@Controller('events')\n@ApiTags('events')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class EventsController {\n  constructor(private readonly eventsService: EventsService) {}\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getEvents' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOkResponse({ type: [EventDto] })\n  public async findAll() {\n    const events = await this.eventsService.findAll();\n    return events.map(event => new EventDto(event));\n  }\n\n  @Post('/')\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'createEvent' })\n  @ApiOkResponse({ type: EventDto })\n  public async create(@Body() dto: CreateEventDto) {\n    const data = await this.eventsService.create(dto);\n    return new EventDto(data);\n  }\n\n  @Patch('/:id')\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'updateEvent' })\n  @ApiOkResponse({ type: EventDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateEventDto) {\n    const data = await this.eventsService.update(id, dto);\n    return new EventDto(data);\n  }\n\n  @Delete('/:id')\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'deleteEvent' })\n  public async delete(@Param('id', ParseIntPipe) id: number) {\n    return this.eventsService.remove(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/events/events.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { EventsService } from './events.service';\nimport { EventsController } from './events.controller';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Event } from '@entities/event';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Event])],\n  controllers: [EventsController],\n  providers: [EventsService],\n})\nexport class EventsModule {}\n"
  },
  {
    "path": "nestjs/src/events/events.service.ts",
    "content": "import { Event } from '@entities/event';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CreateEventDto, UpdateEventDto } from './dto';\n\n@Injectable()\nexport class EventsService {\n  constructor(\n    @InjectRepository(Event)\n    private repository: Repository<Event>,\n  ) {}\n\n  public async findAll() {\n    return this.repository.find({ order: { updatedDate: 'DESC' }, relations: ['discipline'] });\n  }\n\n  public async create(data: CreateEventDto) {\n    return this.repository.save(data);\n  }\n\n  public async update(id: number, data: UpdateEventDto) {\n    await this.repository.update(id, data);\n    return this.repository.findOneByOrFail({ id });\n  }\n\n  public async remove(id: number) {\n    await this.repository.delete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/discord.service.ts",
    "content": "import { HttpService } from '@nestjs/axios';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { lastValueFrom } from 'rxjs';\n\nprocess.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';\nconst isProd = process.env.NODE_ENV === 'production';\n\ntype GratitudeData = {\n  fromGithubId: string;\n  toDiscordId: string | null;\n  toGithubId: string;\n  comment: string;\n  gratitudeUrl: string;\n};\n\ntype DiscordMessage = {\n  avatar_url: string;\n  content: string;\n  username: string;\n};\n\n@Injectable()\nexport class DiscordService {\n  private logger = new Logger(DiscordService.name);\n\n  constructor(private httpService: HttpService) {}\n\n  public async sendGratitudeMessage(params: GratitudeData) {\n    const mention = params.toDiscordId ? `<@${params.toDiscordId}>` : `**@${params.toGithubId}**`;\n\n    const message: DiscordMessage = {\n      avatar_url: `https://github.com/${params.fromGithubId}.png`,\n      username: params.fromGithubId,\n      content: `${mention}\\n${params.comment}`,\n    };\n    if (!isProd) {\n      this.logger.log(`Skip sending discord message in develoment: ${JSON.stringify(message)}`);\n      return;\n    }\n    await lastValueFrom(this.httpService.post(params.gratitudeUrl, message));\n  }\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/badge.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport enum Badge {\n  Congratulations = 'Congratulations',\n  ExpertHelp = 'Expert_help',\n  GreatSpeaker = 'Great_speaker',\n  GoodJob = 'Good_job',\n  HelpingHand = 'Helping_hand',\n  Hero = 'Hero',\n  ThankYou = 'Thank_you',\n  OutstandingWork = 'Outstanding_work',\n  TopPerformer = 'Top_performer',\n  JobOffer = 'Job_Offer',\n  RSActivist = 'RS_activist',\n  JuryTeam = 'Jury_Team',\n  Mentor = 'Mentor',\n  Contributor = 'Contributor',\n  Coordinator = 'Coordinator',\n  Thanks = 'Thanks',\n}\n\nexport class BadgeDto {\n  constructor(badge: { id: Badge; name: string }) {\n    this.id = badge.id;\n    this.name = badge.name;\n  }\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty({ type: Badge, enum: Badge, enumName: 'BadgeEnum' })\n  public id: Badge;\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/country.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'class-validator';\n\nexport class CountryDto {\n  constructor({ countryName }: { countryName: string }) {\n    this.countryName = countryName;\n  }\n\n  @ApiProperty({ type: String })\n  @IsString()\n  countryName: string;\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/create-gratitude.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsEnum, IsNotEmpty, IsNumber, IsString, MinLength, NotContains } from 'class-validator';\nimport { Badge } from './badge.dto';\n\nexport class CreateGratitudeDto {\n  @IsNotEmpty()\n  @IsArray()\n  @ApiProperty({ type: [Number] })\n  userIds: number[];\n\n  @IsNotEmpty()\n  @IsNumber()\n  @ApiProperty()\n  courseId: number;\n\n  @IsNotEmpty()\n  @IsString()\n  @MinLength(20)\n  @NotContains('@', {\n    message: 'The comment can not contain \"@\" symbol',\n  })\n  @ApiProperty()\n  comment: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @IsEnum(Badge)\n  @ApiProperty()\n  badgeId: string;\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/gratitude.dto.ts",
    "content": "import { Feedback } from '@entities/feedback';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { PersonDto } from 'src/core/dto';\nimport { Badge } from './badge.dto';\n\nexport class GratitudeDto {\n  constructor(feedback: Feedback) {\n    this.id = feedback.id;\n    this.user = new PersonDto(feedback.toUser);\n    this.comment = feedback.comment ?? '';\n    this.badgeId = feedback.badgeId as Badge;\n    this.courseId = feedback.courseId;\n  }\n\n  @ApiProperty()\n  public user: PersonDto;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty({ type: Badge, enum: Badge, enumName: 'BadgeEnum' })\n  public badgeId: Badge;\n\n  @ApiProperty()\n  public comment: string;\n\n  @ApiProperty()\n  public courseId: number;\n\n  @ApiProperty()\n  public date: string;\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/hero-radar.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { HeroesRadarBadge, HeroesRadarBadgeDto } from './heroes-radar-badge.dto';\nimport { PersonDto } from 'src/core/dto';\n\nexport interface HeroRadar {\n  githubId: string;\n  firstName: string;\n  lastName: string;\n  rank: number;\n  total: number;\n  badges: HeroesRadarBadge[];\n}\n\nexport class HeroRadarDto {\n  constructor(hero: HeroRadar) {\n    this.githubId = hero.githubId;\n    this.name = PersonDto.getName(hero);\n    this.rank = hero.rank;\n    this.total = hero.total;\n    this.badges = hero.badges.map(badge => new HeroesRadarBadgeDto(badge));\n  }\n\n  @ApiProperty()\n  public githubId: string;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public rank: number;\n\n  @ApiProperty()\n  public total: number;\n\n  @ApiProperty({ type: [HeroesRadarBadgeDto] })\n  badges: HeroesRadarBadgeDto[];\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/heroes-radar-badge.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Badge } from './badge.dto';\n\nexport interface HeroesRadarBadge {\n  id: string;\n  badgeId: Badge;\n  comment: string;\n  date: string;\n}\n\nexport class HeroesRadarBadgeDto {\n  constructor(badge: HeroesRadarBadge) {\n    this.id = badge.id;\n    this.badgeId = badge.badgeId;\n    this.comment = badge.comment;\n    this.date = new Date(badge.date).toISOString();\n  }\n\n  @ApiProperty()\n  public id: string;\n\n  @ApiProperty()\n  public badgeId: Badge;\n\n  @ApiProperty()\n  public comment: string;\n\n  @ApiProperty()\n  public date: string;\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/heroes-radar-query.dto.ts",
    "content": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Transform, Type } from 'class-transformer';\nimport { IsBoolean, IsDate, IsInt, IsOptional, IsString } from 'class-validator';\n\nexport class HeroesRadarQueryDto {\n  @ApiProperty()\n  @IsInt()\n  @Type(() => Number)\n  public current: number;\n\n  @ApiProperty()\n  @IsInt()\n  @Type(() => Number)\n  public pageSize: number;\n\n  @ApiPropertyOptional()\n  @IsOptional()\n  @IsInt()\n  @Type(() => Number)\n  courseId?: number;\n\n  @ApiPropertyOptional()\n  @Transform(\n    ({ value }: { value: string }) => {\n      const newValue = value.toLowerCase();\n\n      return newValue === 'true' || newValue === '1';\n    },\n    { toClassOnly: true },\n  )\n  @IsOptional()\n  @IsBoolean()\n  notActivist?: boolean;\n\n  @ApiPropertyOptional()\n  @IsOptional()\n  @IsString()\n  countryName?: string;\n\n  @ApiPropertyOptional()\n  @IsOptional()\n  @IsDate()\n  @Type(() => Date)\n  startDate?: Date;\n\n  @ApiPropertyOptional()\n  @IsOptional()\n  @IsDate()\n  @Transform(\n    ({ value }) => {\n      const date = new Date(value);\n      return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), 23, 59, 59, 999));\n    },\n    { toClassOnly: true },\n  )\n  endDate?: Date;\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/heroes-radar.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { HeroRadar, HeroRadarDto } from './hero-radar.dto';\nimport { PaginationMeta } from 'src/core/paginate';\nimport { PaginationMetaDto } from 'src/core/paginate/dto/Paginate.dto';\n\ninterface HeroesRadar {\n  heroes: HeroRadar[];\n  meta: PaginationMeta;\n}\n\nconst calculateRank = ({ heroes, meta }: HeroesRadar): HeroRadar[] => {\n  const rankedHeroes = heroes.map((hero, index) => {\n    const rank = index + 1 + meta.pageSize * (meta.current - 1);\n    return { ...hero, rank };\n  });\n  return rankedHeroes;\n};\n\nexport class HeroesRadarDto {\n  constructor(heroesRadar: HeroesRadar) {\n    this.content = calculateRank(heroesRadar).map(hero => new HeroRadarDto(hero));\n    this.pagination = new PaginationMetaDto(heroesRadar.meta);\n  }\n\n  @ApiProperty({ type: [HeroRadarDto] })\n  public content: HeroRadarDto[];\n\n  @ApiProperty({ type: PaginationMetaDto })\n  pagination: PaginationMetaDto;\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/dto/index.ts",
    "content": "export * from './create-gratitude.dto';\nexport * from './gratitude.dto';\nexport * from './badge.dto';\nexport * from './heroes-radar-query.dto';\n"
  },
  {
    "path": "nestjs/src/gratitudes/gratitudes.controller.ts",
    "content": "import {\n  Body,\n  Controller,\n  ForbiddenException,\n  Get,\n  Param,\n  ParseIntPipe,\n  Post,\n  Query,\n  Req,\n  Res,\n  UseGuards,\n} from '@nestjs/common';\nimport { ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { BadgeDto, CreateGratitudeDto, GratitudeDto, HeroesRadarQueryDto } from './dto';\nimport { GratitudesService } from './gratitudes.service';\nimport { HeroesRadarDto } from './dto/heroes-radar.dto';\nimport { CountryDto } from './dto/country.dto';\nimport { parseAsync, transforms } from 'json2csv';\nimport { Response } from 'express';\n\n@Controller('gratitudes')\n@ApiTags('gratitudes')\n@UseGuards(DefaultGuard)\nexport class GratitudesController {\n  constructor(private readonly service: GratitudesService) {}\n\n  @Post('/')\n  @ApiOperation({ operationId: 'createGratitude' })\n  @ApiOkResponse({ type: GratitudeDto })\n  public async create(@Req() req: CurrentRequest, @Body() dto: CreateGratitudeDto) {\n    await this.service.create(req.user, dto);\n  }\n\n  @Get('/badges/:courseId')\n  @ApiOperation({ operationId: 'getBadges' })\n  @ApiOkResponse({ type: [BadgeDto] })\n  public async getBadges(@Req() req: CurrentRequest, @Param('courseId', ParseIntPipe) courseId: number) {\n    const badges = this.service.getBadges(req.user, courseId);\n    return badges.map(badge => new BadgeDto(badge));\n  }\n\n  @Get('/heroes/radar')\n  @ApiOperation({ operationId: 'getHeroesRadar' })\n  @ApiOkResponse({ type: HeroesRadarDto })\n  @ApiForbiddenResponse()\n  public async getHeroesRadar(@Req() req: CurrentRequest, @Query() query: HeroesRadarQueryDto) {\n    const { isAdmin } = req.user;\n    if (query.countryName && !isAdmin) {\n      throw new ForbiddenException();\n    }\n\n    const heroes = await this.service.getHeroesRadar(query);\n\n    return new HeroesRadarDto(heroes);\n  }\n\n  @Get('/heroes/radar/csv')\n  @ApiOperation({ operationId: 'getHeroesRadarCsv' })\n  @ApiForbiddenResponse()\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin], true)\n  public async getHeroesRadarCsv(@Query() query: HeroesRadarQueryDto, @Res() res: Response) {\n    const heroes = await this.service.getHeroesRadar(query);\n\n    const parsedData = await parseAsync(new HeroesRadarDto(heroes).content, { transforms: [transforms.flatten()] });\n\n    res.setHeader('Content-Type', 'text/csv');\n    res.setHeader('Content-disposition', `filename=heroes-radar.csv`);\n\n    res.end(parsedData);\n  }\n\n  @Get('/heroes/countries')\n  @ApiOperation({ operationId: 'getHeroesCountries' })\n  @ApiOkResponse({ type: [CountryDto] })\n  public async getHeroesCountries() {\n    const countries = await this.service.getHeroesCountries();\n    return countries.map(country => new CountryDto(country));\n  }\n}\n"
  },
  {
    "path": "nestjs/src/gratitudes/gratitudes.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Feedback } from '@entities/feedback';\nimport { HttpModule } from '@nestjs/axios';\nimport { GratitudesController } from './gratitudes.controller';\nimport { GratitudesService } from './gratitudes.service';\nimport { DiscordService } from './discord.service';\n\n@Module({\n  imports: [HttpModule, TypeOrmModule.forFeature([Feedback])],\n  controllers: [GratitudesController],\n  providers: [GratitudesService, DiscordService],\n})\nexport class GratitudesModule {}\n"
  },
  {
    "path": "nestjs/src/gratitudes/gratitudes.service.ts",
    "content": "import { Feedback } from '@entities/feedback';\nimport { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { AuthUser, CourseRole } from 'src/auth';\nimport { Repository, DataSource } from 'typeorm';\nimport { DiscordService } from './discord.service';\nimport { Badge, CreateGratitudeDto, HeroesRadarQueryDto } from './dto';\n\n@Injectable()\nexport class GratitudesService {\n  private logger = new Logger(GratitudesService.name);\n\n  constructor(\n    private discordService: DiscordService,\n    @InjectRepository(Feedback)\n    private repository: Repository<Feedback>,\n    private dataSource: DataSource,\n  ) {}\n\n  public async create(authUser: AuthUser, data: CreateGratitudeDto) {\n    if (data.userIds.includes(authUser.id)) {\n      throw new BadRequestException('You cannot give feedback to yourself');\n    }\n    const badges = this.getBadges(authUser, data.courseId);\n\n    if (!badges.some(badge => badge.id === data.badgeId)) {\n      throw new BadRequestException('Badge not allowed');\n    }\n\n    await Promise.all(\n      data.userIds.map(userId =>\n        this.postUserFeedback({\n          toUserId: userId,\n          fromUserId: authUser.id,\n          comment: data.comment,\n          badgeId: data.badgeId,\n          courseId: data.courseId,\n        } as Feedback),\n      ),\n    );\n  }\n\n  public getBadges({ courses, isAdmin }: AuthUser, courseId: number) {\n    if (isAdmin) {\n      return gratitudeBadge;\n    }\n\n    const userCourseRoles = courses ? (courses[courseId]?.roles ?? []) : [];\n    return gratitudeBadge.filter((badge: GratitudeBadge) => {\n      const allowed = badge.roles?.some(role => userCourseRoles.includes(role)) ?? true;\n      return allowed;\n    });\n  }\n\n  private async getUserIdsWithActivistBadge() {\n    const activistIds = await this.repository\n      .createQueryBuilder('feedback')\n      .select('DISTINCT \"toUserId\"')\n      .where(`\"badgeId\" = 'RS_activist'`)\n      .getRawMany<{ toUserId: number }>();\n\n    return activistIds.map(({ toUserId }) => toUserId);\n  }\n\n  public async getHeroesRadar({\n    courseId,\n    current: page = 1,\n    pageSize = 20,\n    notActivist,\n    countryName,\n    startDate,\n    endDate,\n  }: HeroesRadarQueryDto) {\n    const countQuery = this.repository\n      .createQueryBuilder('feedback')\n      .select('COUNT(DISTINCT \"toUserId\") as count')\n      .innerJoin('feedback.toUser', 'user');\n\n    const countedBadgesQuery = this.repository\n      .createQueryBuilder('feedback')\n      .select([\n        'feedback.\"id\"',\n        '\"user\".\"githubId\"',\n        '\"user\".\"firstName\"',\n        '\"user\".\"lastName\"',\n        'feedback.\"badgeId\"',\n        'feedback.\"comment\"',\n        'feedback.\"createdDate\"',\n        'ROW_NUMBER() OVER (PARTITION BY feedback.\"toUserId\" ORDER BY feedback.createdDate DESC) AS \"rowNumber\"',\n        'COUNT(*) OVER (PARTITION BY feedback.\"toUserId\") AS \"total\"',\n      ])\n      .innerJoin('feedback.toUser', 'user');\n\n    if (notActivist) {\n      const ids = await this.getUserIdsWithActivistBadge();\n      [countQuery, countedBadgesQuery].forEach(query => query.where('\"feedback\".\"toUserId\" NOT IN (:...ids)', { ids }));\n    }\n\n    if (courseId) {\n      [countQuery, countedBadgesQuery].forEach(query => query.where('feedback.\"courseId\" = :courseId', { courseId }));\n    }\n\n    if (countryName) {\n      [countQuery, countedBadgesQuery].forEach(query =>\n        query.where('\"user\".\"countryName\" = :countryName', { countryName }),\n      );\n    }\n\n    if (startDate && endDate) {\n      [countQuery, countedBadgesQuery].forEach(query =>\n        query.where('feedback.\"createdDate\" BETWEEN :startDate and :endDate', {\n          startDate,\n          endDate,\n        }),\n      );\n    }\n\n    const heroesQuery = this.dataSource\n      .createQueryBuilder()\n      .select([\n        '\"badgesInfo\".\"githubId\"',\n        '\"badgesInfo\".\"firstName\"',\n        '\"badgesInfo\".\"lastName\"',\n        'total',\n        `jsonb_agg(json_build_object('id', \"id\",'badgeId', \"badgeId\",'comment', \"comment\",'date',\"createdDate\") ORDER BY \"createdDate\" DESC) as badges`,\n      ])\n      .from(\n        qb => qb.from(`(${countedBadgesQuery.getQuery()})`, 'countedBadges').where('\"countedBadges\".\"rowNumber\" <= 20'),\n        'badgesInfo',\n      )\n      .groupBy('\"githubId\"')\n      .addGroupBy('\"firstName\"')\n      .addGroupBy('\"lastName\"')\n      .addGroupBy('\"total\"')\n      .orderBy('total', 'DESC')\n      .addOrderBy('\"githubId\"', 'ASC')\n      .limit(pageSize)\n      .offset((page - 1) * pageSize)\n      .setParameters(countedBadgesQuery.getParameters());\n\n    const { count } = await countQuery.getRawOne();\n    const total = Number(count);\n    const heroes = await heroesQuery.getRawMany();\n    const totalPages = Math.ceil(total / pageSize);\n\n    return {\n      heroes,\n      meta: { itemCount: heroes.length, total, current: page, pageSize, totalPages },\n    };\n  }\n\n  public async getHeroesCountries() {\n    return await this.repository\n      .createQueryBuilder('feedback')\n      .leftJoinAndSelect('feedback.fromUser', 'user')\n      .select('DISTINCT \"countryName\"')\n      .where('\"countryName\" IS NOT NULL')\n      .orderBy('\"countryName\"', 'ASC')\n      .getRawMany();\n  }\n\n  private async postUserFeedback(data: Feedback) {\n    const feedback = await this.createFeedback(data);\n    await this.postToDiscord(feedback);\n  }\n\n  private async createFeedback(feedback: Feedback) {\n    const { id } = await this.repository.save({\n      fromUserId: feedback.fromUserId,\n      toUserId: feedback.toUserId,\n      comment: feedback.comment,\n      badgeId: feedback.badgeId,\n      courseId: feedback.courseId,\n    });\n\n    const result = await this.repository.findOneOrFail({\n      where: { id },\n      relations: ['fromUser', 'toUser', 'course', 'course.discordServer'],\n    });\n\n    return result;\n  }\n\n  private postToDiscord(feedback: Feedback) {\n    if (!feedback.course?.discordServer?.gratitudeUrl) {\n      this.logger.warn('Course do not have Discord Webhook URL');\n      return Promise.resolve(null);\n    }\n    return this.discordService.sendGratitudeMessage({\n      toGithubId: feedback.toUser.githubId,\n      toDiscordId: feedback.toUser.discord?.id ?? null,\n      fromGithubId: feedback.fromUser.githubId,\n      comment: feedback.comment ?? '',\n      gratitudeUrl: feedback.course.discordServer.gratitudeUrl,\n    });\n  }\n}\n\ntype GratitudeBadge = { id: Badge; name: string; roles?: CourseRole[] };\n\nconst gratitudeBadge: GratitudeBadge[] = [\n  { id: Badge.Congratulations, name: 'Congratulations' },\n  { id: Badge.ExpertHelp, name: 'Expert help' },\n  { id: Badge.GreatSpeaker, name: 'Great speaker' },\n  { id: Badge.GoodJob, name: 'Good job' },\n  { id: Badge.HelpingHand, name: 'Helping hand' },\n  { id: Badge.Hero, name: 'Hero' },\n  { id: Badge.ThankYou, name: 'Thank you' },\n  { id: Badge.OutstandingWork, name: 'Outstanding work', roles: [CourseRole.Manager, CourseRole.Supervisor] },\n  { id: Badge.TopPerformer, name: 'Top performer', roles: [CourseRole.Manager, CourseRole.Supervisor] },\n  { id: Badge.JobOffer, name: 'Job Offer', roles: [CourseRole.Manager, CourseRole.Supervisor] },\n  { id: Badge.RSActivist, name: 'RS activist', roles: [CourseRole.Manager, CourseRole.Supervisor] },\n  { id: Badge.JuryTeam, name: 'Jury team', roles: [CourseRole.Manager, CourseRole.Supervisor] },\n  { id: Badge.Mentor, name: 'Mentor', roles: [CourseRole.Manager] },\n  { id: Badge.Contributor, name: 'Contributor', roles: [CourseRole.Manager] },\n  { id: Badge.Coordinator, name: 'Coordinator', roles: [CourseRole.Manager] },\n  { id: Badge.Thanks, name: 'Thanks', roles: [CourseRole.Manager] },\n];\n"
  },
  {
    "path": "nestjs/src/gratitudes/index.ts",
    "content": "export * from './gratitudes.module';\n"
  },
  {
    "path": "nestjs/src/listeners/course.listener.ts",
    "content": "import { S3 } from '@aws-sdk/client-s3';\nimport { Course } from '@entities/course';\nimport { HttpService } from '@nestjs/axios';\nimport { Injectable, Logger } from '@nestjs/common';\nimport { Cron, CronExpression } from '@nestjs/schedule';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { ExportCourseDto } from 'src/courses/dto';\nimport { MoreThanOrEqual, Not, Repository } from 'typeorm';\nimport { ConfigService } from '../config';\n\n@Injectable()\nexport class CourseListener {\n  private s3: S3;\n  private logger = new Logger(CourseListener.name);\n\n  private readonly objectKey = 'app/courses.json';\n  private readonly bucket;\n\n  constructor(\n    @InjectRepository(Course)\n    private readonly repository: Repository<Course>,\n    private readonly httpService: HttpService,\n    readonly configService: ConfigService,\n  ) {\n    this.s3 = new S3(this.configService.awsClient);\n    this.bucket = this.configService.buckets.cdn;\n  }\n\n  @Cron(CronExpression.EVERY_30_MINUTES)\n  async handleCoursesChange() {\n    const courses = await this.findExportCourses();\n\n    // export courses to S3\n    const changed = await this.exportCoursesToS3Conditionally(courses);\n\n    if (changed) {\n      // trigger rs site update\n      this.triggerSiteUpdate();\n    }\n  }\n\n  /**\n   * Find courses to export to S3 for availability in rs.school\n   */\n  private async findExportCourses() {\n    const courses = await this.repository.find({\n      where: {\n        completed: false,\n        registrationEndDate: MoreThanOrEqual(new Date()),\n        alias: Not('test-course'),\n        inviteOnly: Not(true),\n      },\n      relations: ['discipline'],\n    });\n\n    return courses.map(course => new ExportCourseDto(course));\n  }\n\n  /**\n   * Trigger site update via GitHub Actions\n   */\n  private triggerSiteUpdate() {\n    if (!this.configService.auth.github.integrationSiteToken) {\n      this.logger.warn('No integration site token');\n      return;\n    }\n\n    // It dispatches event to the site repository and triggers workflow to rebuild the site\n    // rs.school site needs to be updated when courses are changed.\n    this.httpService\n      .post(\n        'https://api.github.com/repos/rolling-scopes/site/dispatches',\n        { event_type: 'course.updated' },\n        {\n          headers: {\n            'X-GitHub-Api-Version': '2022-11-28',\n            Accept: 'application/vnd.github+json',\n            Authorization: `Bearer ${this.configService.auth.github.integrationSiteToken}`,\n          },\n        },\n      )\n      .subscribe({\n        error: error => {\n          this.logger.error('Error dispatching course event to the site repository', (error as Error)?.message);\n        },\n      });\n  }\n\n  /**\n   * Write courses to S3 if they are changed\n   * @param data - courses data\n   * @returns true if courses are changed, false otherwise\n   */\n  private async exportCoursesToS3Conditionally(data: ExportCourseDto[]) {\n    if (this.configService.env !== 'prod') {\n      return false;\n    }\n\n    const current = await this.s3.getObject({ Bucket: this.bucket, Key: this.objectKey }).catch(() => null);\n    const serialized = JSON.stringify(data);\n    const stored = await current?.Body?.transformToString();\n\n    if (stored === serialized) {\n      return false;\n    }\n\n    await this.s3.putObject({\n      Bucket: this.bucket,\n      Key: this.objectKey,\n      Body: serialized,\n      ContentType: 'application/json',\n      CacheControl: 'max-age=30',\n    });\n    return true;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/listeners/index.ts",
    "content": "export { ListenersModule } from './listeners.module';\n"
  },
  {
    "path": "nestjs/src/listeners/listeners.module.ts",
    "content": "import { HttpModule } from '@nestjs/axios';\nimport { Module } from '@nestjs/common';\nimport { ConfigModule } from '../config';\nimport { CourseListener } from './course.listener';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Course } from '@entities/course';\nimport { Discipline } from '@entities/discipline';\n\n@Module({\n  imports: [ConfigModule, HttpModule, TypeOrmModule.forFeature([Course, Discipline])],\n  providers: [CourseListener],\n})\nexport class ListenersModule {}\n"
  },
  {
    "path": "nestjs/src/main.ts",
    "content": "if (process.env.NODE_ENV !== 'production') {\n  // eslint-disable-next-line @typescript-eslint/no-require-imports\n  require('dotenv').config();\n}\n\nimport { NestFactory } from '@nestjs/core';\nimport { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';\nimport { AppModule } from './app.module';\nimport { setupApp } from './setup';\nimport './core/templates';\n\nconst port = process.env.NODE_PORT || 3002;\nconst isLambda = !!process.env.AWS_LAMBDA;\n\nasync function bootstrap() {\n  const app = await NestFactory.create(AppModule, { bufferLogs: true, logger: isLambda ? console : undefined });\n\n  setupApp(app);\n\n  const config = new DocumentBuilder().setTitle('RS School API').build();\n  const document = SwaggerModule.createDocument(app, config);\n  SwaggerModule.setup('swagger', app, document);\n\n  await app.listen(port);\n}\n\nbootstrap();\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/dto/course-stats.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class MentorCourseStatsDto {\n  constructor(courseName: string, studentsCount: number) {\n    this.courseName = courseName;\n    this.studentsCount = studentsCount;\n  }\n\n  @ApiProperty({ description: 'Name of the course' })\n  public courseName: string;\n\n  @ApiProperty({ description: 'Number of certified students mentored in this course' })\n  public studentsCount: number;\n}\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/dto/index.ts",
    "content": "export { MentorCourseStatsDto } from './course-stats.dto';\nexport { TopMentorDto } from './top-mentor.dto';\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/dto/top-mentor.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { MentorCourseStatsDto } from './course-stats.dto';\n\nexport class TopMentorDto {\n  constructor(data: {\n    rank: number;\n    githubId: string;\n    name: string;\n    totalStudents: number;\n    totalGratitudes: number;\n    courseStats: MentorCourseStatsDto[];\n  }) {\n    this.rank = data.rank;\n    this.githubId = data.githubId;\n    this.name = data.name;\n    this.totalStudents = data.totalStudents;\n    this.totalGratitudes = data.totalGratitudes;\n    this.courseStats = data.courseStats;\n  }\n\n  @ApiProperty({ description: 'Position in the mentors ranking' })\n  public rank: number;\n\n  @ApiProperty({ description: 'GitHub username' })\n  public githubId: string;\n\n  @ApiProperty({ description: 'Full name of the mentor' })\n  public name: string;\n\n  @ApiProperty({ description: 'Total number of certified students mentored' })\n  public totalStudents: number;\n\n  @ApiProperty({ description: 'Total number of gratitudes received' })\n  public totalGratitudes: number;\n\n  @ApiProperty({ type: [MentorCourseStatsDto], description: 'Student counts per course' })\n  public courseStats: MentorCourseStatsDto[];\n}\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/index.ts",
    "content": "export { MentorsHallOfFameModule } from './mentors-hall-of-fame.module';\nexport { MentorsHallOfFameService } from './mentors-hall-of-fame.service';\nexport { MentorsHallOfFameController } from './mentors-hall-of-fame.controller';\nexport * from './dto';\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/mentors-hall-of-fame.controller.test.ts",
    "content": "import { BadRequestException, ParseBoolPipe } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { MentorsHallOfFameController } from './mentors-hall-of-fame.controller';\nimport { MentorsHallOfFameService } from './mentors-hall-of-fame.service';\nimport { TopMentorDto, MentorCourseStatsDto } from './dto';\n\nconst mockTopMentors = [\n  new TopMentorDto({\n    rank: 1,\n    githubId: 'mentor1',\n    name: 'John Doe',\n    totalStudents: 10,\n    totalGratitudes: 5,\n    courseStats: [new MentorCourseStatsDto('JS Course', 5), new MentorCourseStatsDto('React Course', 5)],\n  }),\n  new TopMentorDto({\n    rank: 2,\n    githubId: 'mentor2',\n    name: 'Jane Smith',\n    totalStudents: 8,\n    totalGratitudes: 3,\n    courseStats: [new MentorCourseStatsDto('JS Course', 8)],\n  }),\n];\n\nconst mockGetTopMentors = vi.fn(() => Promise.resolve(mockTopMentors));\n\nconst mockServiceFactory = vi.fn(() => ({\n  getTopMentors: mockGetTopMentors,\n}));\n\ndescribe('MentorsHallOfFameController', () => {\n  let controller: MentorsHallOfFameController;\n\n  beforeEach(async () => {\n    vi.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      controllers: [MentorsHallOfFameController],\n      providers: [{ provide: MentorsHallOfFameService, useFactory: mockServiceFactory }],\n    }).compile();\n\n    controller = module.get<MentorsHallOfFameController>(MentorsHallOfFameController);\n  });\n\n  describe('getTopMentors', () => {\n    it('returns array of top mentors', async () => {\n      const result = await controller.getTopMentors(false);\n\n      expect(Array.isArray(result)).toBe(true);\n      expect(result).toHaveLength(2);\n      expect(result[0]).toEqual(mockTopMentors[0]);\n      expect(result[1]).toEqual(mockTopMentors[1]);\n    });\n\n    it('calls service with allTime=false by default', async () => {\n      await controller.getTopMentors(false);\n\n      expect(mockGetTopMentors).toHaveBeenCalledWith(false);\n      expect(mockGetTopMentors).toHaveBeenCalledTimes(1);\n    });\n\n    it('calls service with allTime=true when provided', async () => {\n      await controller.getTopMentors(true);\n\n      expect(mockGetTopMentors).toHaveBeenCalledWith(true);\n      expect(mockGetTopMentors).toHaveBeenCalledTimes(1);\n    });\n\n    it('propagates service errors', async () => {\n      mockGetTopMentors.mockRejectedValueOnce(new Error('Service failed'));\n\n      await expect(controller.getTopMentors(false)).rejects.toThrow('Service failed');\n    });\n  });\n\n  describe('allTime query validation', () => {\n    it('throws for invalid allTime value', async () => {\n      const parseBoolPipe = new ParseBoolPipe();\n\n      await expect(\n        parseBoolPipe.transform('invalid', {\n          type: 'query',\n          data: 'allTime',\n          metatype: Boolean,\n        }),\n      ).rejects.toThrow(BadRequestException);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/mentors-hall-of-fame.controller.ts",
    "content": "import { Controller, DefaultValuePipe, Get, ParseBoolPipe, Query } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';\nimport { MentorsHallOfFameService } from './mentors-hall-of-fame.service';\nimport { TopMentorDto } from './dto';\n\n@Controller('mentors-hall-of-fame')\n@ApiTags('mentors-hall-of-fame')\nexport class MentorsHallOfFameController {\n  constructor(private readonly service: MentorsHallOfFameService) {}\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getTopMentors' })\n  @ApiOkResponse({ type: [TopMentorDto] })\n  @ApiQuery({ name: 'allTime', required: false, type: 'boolean' })\n  public async getTopMentors(\n    @Query('allTime', new DefaultValuePipe(false), ParseBoolPipe) allTime: boolean,\n  ): Promise<TopMentorDto[]> {\n    return this.service.getTopMentors(allTime);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/mentors-hall-of-fame.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Feedback } from '@entities/feedback';\nimport { User } from '@entities/user';\nimport { MentorsHallOfFameController } from './mentors-hall-of-fame.controller';\nimport { MentorsHallOfFameService } from './mentors-hall-of-fame.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Feedback, User])],\n  controllers: [MentorsHallOfFameController],\n  providers: [MentorsHallOfFameService],\n  exports: [MentorsHallOfFameService],\n})\nexport class MentorsHallOfFameModule {}\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/mentors-hall-of-fame.service.test.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { User } from '@entities/user';\nimport { MentorsHallOfFameService } from './mentors-hall-of-fame.service';\n\nconst mockMentorData = [\n  {\n    githubId: 'mentor1',\n    firstName: 'John',\n    lastName: 'Doe',\n    totalStudents: '10',\n    totalGratitudes: '5',\n    courseStatsRaw: [\n      { courseName: 'JS Course', studentId: 1 },\n      { courseName: 'JS Course', studentId: 2 },\n      { courseName: 'React Course', studentId: 3 },\n    ],\n  },\n  {\n    githubId: 'mentor2',\n    firstName: 'Jane',\n    lastName: 'Smith',\n    totalStudents: '5',\n    totalGratitudes: '3',\n    courseStatsRaw: [{ courseName: 'JS Course', studentId: 1 }],\n  },\n];\n\nconst mockGratitudesQueryBuilder = {\n  select: vi.fn().mockReturnThis(),\n  addSelect: vi.fn().mockReturnThis(),\n  from: vi.fn().mockReturnThis(),\n  groupBy: vi.fn().mockReturnThis(),\n  getQuery: vi.fn().mockReturnValue('gratitudes_subquery'),\n};\n\nconst mockQueryBuilder = {\n  innerJoin: vi.fn().mockReturnThis(),\n  leftJoin: vi.fn().mockReturnThis(),\n  where: vi.fn().mockReturnThis(),\n  andWhere: vi.fn().mockReturnThis(),\n  select: vi.fn().mockReturnThis(),\n  addSelect: vi.fn().mockReturnThis(),\n  groupBy: vi.fn().mockReturnThis(),\n  orderBy: vi.fn().mockReturnThis(),\n  addOrderBy: vi.fn().mockReturnThis(),\n  getRawMany: vi.fn(),\n};\n\nconst mockUserRepository = {\n  createQueryBuilder: vi.fn(() => mockQueryBuilder),\n  manager: {\n    createQueryBuilder: vi.fn(() => mockGratitudesQueryBuilder),\n  },\n};\n\ndescribe('MentorsHallOfFameService', () => {\n  let service: MentorsHallOfFameService;\n\n  beforeEach(async () => {\n    vi.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        MentorsHallOfFameService,\n        {\n          provide: getRepositoryToken(User),\n          useValue: mockUserRepository,\n        },\n      ],\n    }).compile();\n\n    service = module.get<MentorsHallOfFameService>(MentorsHallOfFameService);\n  });\n\n  describe('onModuleInit', () => {\n    it('calls refreshCache', async () => {\n      const refreshCacheSpy = vi.spyOn(service, 'refreshCache').mockResolvedValue();\n\n      await service.onModuleInit();\n\n      expect(refreshCacheSpy).toHaveBeenCalledTimes(1);\n    });\n  });\n\n  describe('getTopMentors', () => {\n    it('returns empty array when cache is empty', () => {\n      const result = service.getTopMentors();\n\n      expect(result).toEqual([]);\n    });\n\n    it('returns allTime mentors from allTime cache key', async () => {\n      const allTimeMentors = [\n        {\n          githubId: 'all-time-mentor',\n          firstName: 'All',\n          lastName: 'Time',\n          totalStudents: '42',\n          totalGratitudes: '9',\n          courseStatsRaw: [],\n        },\n      ];\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce([]).mockResolvedValueOnce(allTimeMentors);\n\n      await service.refreshCache();\n\n      expect(service.getTopMentors()).toEqual([]);\n      expect(service.getTopMentors(true)).toEqual(\n        expect.arrayContaining([\n          expect.objectContaining({\n            githubId: 'all-time-mentor',\n            totalStudents: 42,\n          }),\n        ]),\n      );\n    });\n  });\n\n  describe('refreshCache', () => {\n    it('returns empty array when no mentors found', async () => {\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce([]).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n      expect(result).toEqual([]);\n    });\n\n    it('returns mentors sorted by total certified students count DESC', async () => {\n      const allTimeMentors = [\n        {\n          githubId: 'mentor3',\n          firstName: 'Alex',\n          lastName: 'Brown',\n          totalStudents: '12',\n          totalGratitudes: '7',\n          courseStatsRaw: [],\n        },\n      ];\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mockMentorData).mockResolvedValueOnce(allTimeMentors);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n      const allTimeResult = service.getTopMentors(true);\n\n      expect(result.length).toBe(2);\n      expect(result[0]?.githubId).toBe('mentor1');\n      expect(result[0]?.totalStudents).toBe(10);\n      expect(result[1]?.githubId).toBe('mentor2');\n      expect(result[1]?.totalStudents).toBe(5);\n      expect(allTimeResult.length).toBe(1);\n      expect(allTimeResult[0]?.githubId).toBe('mentor3');\n    });\n\n    it('assigns sequential ranks to mentors', async () => {\n      const mentorsWithTies = [\n        {\n          githubId: 'mentor1',\n          firstName: 'John',\n          lastName: 'Doe',\n          totalStudents: '10',\n          totalGratitudes: '5',\n          courseStatsRaw: [],\n        },\n        {\n          githubId: 'mentor2',\n          firstName: 'Jane',\n          lastName: 'Smith',\n          totalStudents: '10',\n          totalGratitudes: '3',\n          courseStatsRaw: [],\n        },\n        {\n          githubId: 'mentor3',\n          firstName: 'Bob',\n          lastName: 'Johnson',\n          totalStudents: '5',\n          totalGratitudes: '2',\n          courseStatsRaw: [],\n        },\n      ];\n\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorsWithTies).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n\n      expect(result[0]?.rank).toBe(1);\n      expect(result[1]?.rank).toBe(2);\n      expect(result[2]?.rank).toBe(3);\n    });\n\n    it('returns only top 100 mentors when there are more', async () => {\n      const mentorsData = [];\n      for (let i = 1; i <= 150; i++) {\n        mentorsData.push({\n          githubId: `mentor${i}`,\n          firstName: `Name${i}`,\n          lastName: `Last${i}`,\n          totalStudents: String(200 - i),\n          totalGratitudes: String(i),\n          courseStatsRaw: [],\n        });\n      }\n\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorsData).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n\n      expect(result.length).toBe(100);\n      expect(result[0]?.rank).toBe(1);\n      expect(result[99]?.rank).toBe(100);\n    });\n\n    it('includes all mentors with same count at position 100 boundary', async () => {\n      const mentorsData = [];\n      for (let i = 1; i <= 99; i++) {\n        mentorsData.push({\n          githubId: `mentor${i}`,\n          firstName: `Name${i}`,\n          lastName: `Last${i}`,\n          totalStudents: String(200 - i),\n          totalGratitudes: String(i),\n          courseStatsRaw: [],\n        });\n      }\n      for (let i = 100; i <= 104; i++) {\n        mentorsData.push({\n          githubId: `mentor${i}`,\n          firstName: `Name${i}`,\n          lastName: `Last${i}`,\n          totalStudents: '50',\n          totalGratitudes: String(i),\n          courseStatsRaw: [],\n        });\n      }\n\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorsData).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n\n      expect(result.length).toBe(104);\n      expect(result[99]?.totalStudents).toBe(50);\n      expect(result[103]?.totalStudents).toBe(50);\n    });\n\n    it('aggregates course stats per mentor correctly', async () => {\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mockMentorData).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n\n      const mentor1 = result[0];\n      expect(mentor1?.courseStats.length).toBe(2);\n\n      const jsCourse = mentor1?.courseStats.find(s => s.courseName === 'JS Course');\n      expect(jsCourse?.studentsCount).toBe(2);\n\n      const reactCourse = mentor1?.courseStats.find(s => s.courseName === 'React Course');\n      expect(reactCourse?.studentsCount).toBe(1);\n    });\n\n    it('sorts course stats by studentsCount in descending order', async () => {\n      const mentorWithUnsortedCourses = [\n        {\n          githubId: 'mentor1',\n          firstName: 'John',\n          lastName: 'Doe',\n          totalStudents: '6',\n          totalGratitudes: '1',\n          courseStatsRaw: [\n            { courseName: 'Course C', studentId: 1 },\n            { courseName: 'Course A', studentId: 2 },\n            { courseName: 'Course A', studentId: 3 },\n            { courseName: 'Course B', studentId: 4 },\n            { courseName: 'Course B', studentId: 5 },\n            { courseName: 'Course B', studentId: 6 },\n          ],\n        },\n      ];\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorWithUnsortedCourses).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n      expect(result[0]?.courseStats.map(stat => stat.courseName)).toEqual(['Course B', 'Course A', 'Course C']);\n      expect(result[0]?.courseStats.map(stat => stat.studentsCount)).toEqual([3, 2, 1]);\n    });\n\n    it('formats mentor name from firstName and lastName', async () => {\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mockMentorData).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n\n      expect(result[0]?.name).toBe('John Doe');\n      expect(result[1]?.name).toBe('Jane Smith');\n    });\n\n    it('uses githubId as name when firstName and lastName are empty', async () => {\n      const mentorWithoutName = [\n        {\n          githubId: 'anonymousMentor',\n          firstName: null,\n          lastName: null,\n          totalStudents: '5',\n          totalGratitudes: '2',\n          courseStatsRaw: [],\n        },\n      ];\n\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorWithoutName).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n\n      expect(result[0]?.name).toBe('anonymousMentor');\n    });\n\n    it('handles null values in raw data', async () => {\n      const mentorWithNullValues = [\n        {\n          githubId: 'mentor-null',\n          firstName: null,\n          lastName: null,\n          totalStudents: '3',\n          totalGratitudes: null,\n          courseStatsRaw: null,\n        },\n      ];\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorWithNullValues).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n      expect(result[0]).toEqual(\n        expect.objectContaining({\n          githubId: 'mentor-null',\n          name: 'mentor-null',\n          totalStudents: 3,\n          totalGratitudes: 0,\n          courseStats: [],\n        }),\n      );\n    });\n\n    it('handles undefined values in raw data', async () => {\n      const mentorWithUndefinedValues = [\n        {\n          githubId: 'mentor-undefined',\n          firstName: undefined,\n          lastName: undefined,\n          totalStudents: '4',\n          totalGratitudes: undefined,\n          courseStatsRaw: undefined,\n        },\n      ];\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorWithUndefinedValues).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n      expect(result[0]).toEqual(\n        expect.objectContaining({\n          githubId: 'mentor-undefined',\n          name: 'mentor-undefined',\n          totalStudents: 4,\n          totalGratitudes: 0,\n          courseStats: [],\n        }),\n      );\n    });\n\n    it('handles duplicate student-course pairs correctly', async () => {\n      const mentorWithDuplicates = [\n        {\n          githubId: 'mentor1',\n          firstName: 'John',\n          lastName: 'Doe',\n          totalStudents: '3',\n          totalGratitudes: '4',\n          courseStatsRaw: [\n            { courseName: 'JS Course', studentId: 1 },\n            { courseName: 'JS Course', studentId: 1 },\n            { courseName: 'JS Course', studentId: 2 },\n            { courseName: 'React Course', studentId: 3 },\n          ],\n        },\n      ];\n\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mentorWithDuplicates).mockResolvedValueOnce([]);\n\n      await service.refreshCache();\n\n      const result = service.getTopMentors();\n\n      expect(result[0]?.totalStudents).toBe(3);\n      const jsCourse = result[0]?.courseStats.find(s => s.courseName === 'JS Course');\n      expect(jsCourse?.studentsCount).toBe(2);\n      const reactCourse = result[0]?.courseStats.find(s => s.courseName === 'React Course');\n      expect(reactCourse?.studentsCount).toBe(1);\n    });\n\n    it('applies date filter when allTime is false (default)', async () => {\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce(mockMentorData).mockResolvedValueOnce(mockMentorData);\n\n      await service.refreshCache();\n\n      expect(mockQueryBuilder.where).toHaveBeenCalledWith('certificate.issueDate >= :oneYearAgo', expect.any(Object));\n      expect(mockQueryBuilder.where).toHaveBeenCalledTimes(1);\n    });\n\n    it('does not apply date filter when allTime is true', async () => {\n      mockQueryBuilder.getRawMany.mockResolvedValueOnce([]).mockResolvedValueOnce(mockMentorData);\n\n      await service.refreshCache();\n\n      expect(mockQueryBuilder.where).toHaveBeenCalledTimes(1);\n    });\n\n    it('logs and keeps cache unchanged when refresh fails', async () => {\n      const loggerErrorSpy = vi.spyOn(\n        (service as unknown as { logger: { error: (message: string, error: unknown) => void } }).logger,\n        'error',\n      );\n      mockQueryBuilder.getRawMany.mockRejectedValueOnce(new Error('db failed'));\n\n      await service.refreshCache();\n\n      expect(loggerErrorSpy).toHaveBeenCalledWith('Failed to refresh cache', expect.any(Error));\n      expect(service.getTopMentors()).toEqual([]);\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/mentors-hall-of-fame/mentors-hall-of-fame.service.ts",
    "content": "import { Injectable, Logger, OnModuleInit } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Cron } from '@nestjs/schedule';\nimport { Repository } from 'typeorm';\nimport { Feedback, Mentor, Student, Certificate, Course, User } from '@entities/index';\nimport { MentorCourseStatsDto, TopMentorDto } from './dto';\n\nconst ONCE_A_DAY_AT_01_00 = '0 1 * * *';\n\n@Injectable()\nexport class MentorsHallOfFameService implements OnModuleInit {\n  private readonly logger = new Logger(MentorsHallOfFameService.name);\n\n  private readonly cache = new Map<boolean, TopMentorDto[]>();\n\n  constructor(\n    @InjectRepository(User)\n    private readonly userRepository: Repository<User>,\n  ) {}\n\n  public async onModuleInit(): Promise<void> {\n    await this.refreshCache();\n  }\n\n  @Cron(ONCE_A_DAY_AT_01_00, { timeZone: 'UTC' })\n  public async refreshCache(): Promise<void> {\n    this.logger.log('Refreshing mentors hall of fame cache...');\n\n    try {\n      const [lastYear, allTime] = await Promise.all([\n        this.fetchTopMentorsFromDb(false),\n        this.fetchTopMentorsFromDb(true),\n      ]);\n\n      this.cache.set(false, lastYear);\n      this.cache.set(true, allTime);\n\n      this.logger.log(`Cache refreshed: ${lastYear.length} mentors (last year), ${allTime.length} mentors (all time)`);\n    } catch (error) {\n      this.logger.error('Failed to refresh cache', error);\n    }\n  }\n\n  public getTopMentors(allTime = false): TopMentorDto[] {\n    return this.cache.get(allTime) ?? [];\n  }\n\n  private async fetchTopMentorsFromDb(allTime = false): Promise<TopMentorDto[]> {\n    // Get all mentors with their certified students count\n    const queryBuilder = this.userRepository\n      .createQueryBuilder('user')\n      .innerJoin(Mentor, 'mentor', 'mentor.userId = user.id')\n      .innerJoin(Student, 'student', 'student.mentorId = mentor.id')\n      .innerJoin(Certificate, 'certificate', 'certificate.studentId = student.id')\n      .innerJoin(Course, 'course', 'course.id = student.courseId');\n\n    if (!allTime) {\n      const oneYearAgo = new Date();\n      oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);\n      queryBuilder.where('certificate.issueDate >= :oneYearAgo', { oneYearAgo });\n    }\n\n    const gratitudesSubquery = this.userRepository.manager\n      .createQueryBuilder()\n      .select('feedback.toUserId', 'toUserId')\n      // TODO: Check if we need to count distinct feedback.id or not on real data\n      .addSelect('COUNT(DISTINCT feedback.id)', 'gratitudesCount')\n      .from(Feedback, 'feedback')\n      .groupBy('feedback.toUserId');\n\n    const allMentors = await queryBuilder\n      .leftJoin(`(${gratitudesSubquery.getQuery()})`, 'gratitudes', 'gratitudes.\"toUserId\" = user.id')\n      .select('user.id', 'userId')\n      .addSelect('user.githubId', 'githubId')\n      .addSelect('user.firstName', 'firstName')\n      .addSelect('user.lastName', 'lastName')\n      .addSelect('COUNT(DISTINCT student.id)', 'totalStudents')\n      .addSelect('COALESCE(MAX(gratitudes.\"gratitudesCount\"), 0)', 'totalGratitudes')\n      .addSelect(`JSON_AGG(JSON_BUILD_OBJECT('courseName', course.name, 'studentId', student.id))`, 'courseStatsRaw')\n      .groupBy('user.id')\n      .orderBy('COUNT(DISTINCT student.id)', 'DESC')\n      .addOrderBy('user.githubId', 'ASC')\n      .getRawMany();\n\n    if (allMentors.length === 0) {\n      return [];\n    }\n\n    // Filter to top 100 mentors, including all ties at the boundary\n    return this.filterTop100Mentors(allMentors);\n  }\n\n  private filterTop100Mentors(rawMentors: Record<string, unknown>[]): TopMentorDto[] {\n    const TOP_MENTORS_COUNT = 100;\n\n    if (rawMentors.length <= TOP_MENTORS_COUNT) {\n      return this.mapToTopMentorDtos(rawMentors);\n    }\n\n    // Find boundary student count (at position 100)\n    const boundaryCount = Number(rawMentors[TOP_MENTORS_COUNT - 1]!.totalStudents);\n\n    // Include all mentors until student count drops below boundary\n    const filteredMentors = rawMentors.filter((mentor, index) => {\n      return index < TOP_MENTORS_COUNT || Number(mentor.totalStudents) === boundaryCount;\n    });\n\n    return this.mapToTopMentorDtos(filteredMentors);\n  }\n\n  private mapToTopMentorDtos(rawMentors: Record<string, unknown>[]): TopMentorDto[] {\n    return rawMentors.map((raw, index) => {\n      const totalStudents = this.toNumberOrZero(raw.totalStudents);\n      const totalGratitudes = this.toNumberOrZero(raw.totalGratitudes);\n      const firstName = raw.firstName as string | null;\n      const lastName = raw.lastName as string | null;\n      const githubId = raw.githubId as string;\n      const name = [firstName, lastName].filter(Boolean).join(' ') || githubId;\n\n      // Aggregate course stats from raw JSON\n      const courseStatsRaw = Array.isArray(raw.courseStatsRaw)\n        ? (raw.courseStatsRaw as { courseName: string; studentId: number }[])\n        : [];\n      const courseStatsMap = new Map<string, Set<number>>();\n\n      for (const item of courseStatsRaw) {\n        const courseName = item.courseName;\n        if (!courseStatsMap.has(courseName)) {\n          courseStatsMap.set(courseName, new Set<number>());\n        }\n        courseStatsMap.get(courseName)!.add(item.studentId);\n      }\n\n      const courseStats: MentorCourseStatsDto[] = Array.from(courseStatsMap.entries())\n        .map(([courseName, studentIds]) => new MentorCourseStatsDto(courseName, studentIds.size))\n        .sort((a, b) => b.studentsCount - a.studentsCount);\n\n      return new TopMentorDto({\n        rank: index + 1,\n        githubId,\n        name,\n        totalStudents,\n        totalGratitudes,\n        courseStats,\n      });\n    });\n  }\n\n  private toNumberOrZero(value: unknown): number {\n    const numberValue = Number(value);\n    return Number.isFinite(numberValue) ? numberValue : 0;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/migrations.ts",
    "content": "export const migrations: any[] = [];\n"
  },
  {
    "path": "nestjs/src/notifications/dto/notification.dto.ts",
    "content": "import { Notification, NotificationType } from '@entities/notification';\nimport { NotificationChannelId } from '@entities/notificationChannel';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsEnum, IsOptional, IsString } from 'class-validator';\n\nexport class TelegramTemplate {\n  @ApiProperty()\n  body: string;\n}\n\nexport class EmailTemplate {\n  @ApiProperty()\n  subject: string;\n  @ApiProperty()\n  body: string;\n}\n\nexport class ChannelSettings {\n  @ApiProperty()\n  channelId: NotificationChannelId;\n  @ApiProperty()\n  template: EmailTemplate | TelegramTemplate;\n}\n\nexport class NotificationDto {\n  constructor(notification: Notification) {\n    this.id = notification.id;\n    this.name = notification.name;\n    this.enabled = notification.enabled;\n    this.type = notification.type;\n    this.channels = notification.channels.map(settings => ({\n      template: settings.template,\n      channelId: settings.channelId,\n    }));\n\n    this.parentId = notification.parent?.id;\n  }\n\n  @ApiProperty()\n  public id: string;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  enabled: boolean;\n\n  @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })\n  @IsEnum(NotificationType)\n  public type: NotificationType;\n\n  @ApiProperty({ type: ChannelSettings, isArray: true })\n  public channels: ChannelSettings[];\n\n  @ApiProperty()\n  @IsString()\n  @IsOptional()\n  public parentId?: string;\n}\n"
  },
  {
    "path": "nestjs/src/notifications/dto/update-notification.dto.ts",
    "content": "import { NotificationId, NotificationType } from '@entities/notification';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';\nimport { ChannelSettings } from './notification.dto';\n\nexport class UpdateNotificationDto {\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsString()\n  public id: NotificationId;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  @IsString()\n  public name: string;\n\n  @ApiProperty()\n  @IsBoolean()\n  enabled: boolean;\n\n  @ApiProperty({ type: ChannelSettings, isArray: true })\n  @Type(() => ChannelSettings)\n  @IsArray()\n  public channels: ChannelSettings[];\n\n  @ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })\n  @IsEnum(NotificationType)\n  public type: NotificationType;\n\n  @ApiProperty()\n  @IsString()\n  @IsOptional()\n  public parentId?: NotificationId;\n}\n"
  },
  {
    "path": "nestjs/src/notifications/email-template.ts",
    "content": "export const emailTemplate = `<!DOCTYPE html>\n<html>\n  <head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n    <style>\n      @media only screen and (max-width: 620px) {\n        table.body h1 {\n          font-size: 28px !important;\n          margin-bottom: 10px !important;\n        }\n\n        table.body p,\n        table.body ul,\n        table.body ol,\n        table.body td,\n        table.body span,\n        table.body a {\n          font-size: 16px !important;\n        }\n\n        table.body .wrapper,\n        table.body .article {\n          padding: 10px !important;\n        }\n\n        table.body .content {\n          padding: 0 !important;\n        }\n\n        table.body .container {\n          padding: 0 !important;\n          width: 100% !important;\n        }\n\n        table.body .main {\n          border-left-width: 0 !important;\n          border-radius: 0 !important;\n          border-right-width: 0 !important;\n        }\n\n        table.body .btn table {\n          width: 100% !important;\n        }\n\n        table.body .btn a {\n          width: 100% !important;\n        }\n\n        table.body .img-responsive {\n          height: auto !important;\n          max-width: 100% !important;\n          width: auto !important;\n        }\n      }\n      @media all {\n        .ExternalClass {\n          width: 100%;\n        }\n\n        .ExternalClass,\n        .ExternalClass p,\n        .ExternalClass span,\n        .ExternalClass font,\n        .ExternalClass td,\n        .ExternalClass div {\n          line-height: 100%;\n        }\n\n        .apple-link a {\n          color: inherit !important;\n          font-family: inherit !important;\n          font-size: inherit !important;\n          font-weight: inherit !important;\n          line-height: inherit !important;\n          text-decoration: none !important;\n        }\n\n        #MessageViewBody a {\n          color: inherit;\n          text-decoration: none;\n          font-size: inherit;\n          font-family: inherit;\n          font-weight: inherit;\n          line-height: inherit;\n        }\n\n        .btn-primary table td:hover {\n          background-color: #34495e !important;\n        }\n\n        .btn-primary a:hover {\n          background-color: #34495e !important;\n          border-color: #34495e !important;\n        }\n      }\n    </style>\n  </head>\n  <body\n    style=\"\n      background-color: #ffffff;\n      font-family: sans-serif;\n      -webkit-font-smoothing: antialiased;\n      font-size: 14px;\n      line-height: 1.4;\n      margin: 0;\n      padding: 0;\n      -ms-text-size-adjust: 100%;\n      -webkit-text-size-adjust: 100%;\n    \"\n  >\n    <table\n      role=\"presentation\"\n      border=\"0\"\n      cellpadding=\"0\"\n      cellspacing=\"0\"\n      class=\"body\"\n      style=\"\n        border-collapse: separate;\n        mso-table-lspace: 0pt;\n        mso-table-rspace: 0pt;\n        background-color: #ffffff;\n        width: 100%;\n      \"\n      width=\"100%\"\n      bgcolor=\"#ffffff\"\n    >\n      <tr>\n        <td style=\"font-family: sans-serif; font-size: 14px; vertical-align: top\" valign=\"top\">&nbsp;</td>\n        <td\n          class=\"container\"\n          style=\"\n            font-family: sans-serif;\n            font-size: 14px;\n            vertical-align: top;\n            display: block;\n            max-width: 580px;\n            padding: 10px;\n            width: 580px;\n            margin: 0 auto;\n          \"\n          width=\"580\"\n          valign=\"top\"\n        >\n          <div\n            class=\"content\"\n            style=\"box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px\"\n          >\n            <!-- START CENTERED WHITE CONTAINER -->\n            <table\n              role=\"presentation\"\n              class=\"main\"\n              style=\"\n                border-collapse: separate;\n                mso-table-lspace: 0pt;\n                mso-table-rspace: 0pt;\n                background: #ffffff;\n                border-radius: 3px;\n                width: 100%;\n              \"\n              width=\"100%\"\n            >\n              <!-- START MAIN CONTENT AREA -->\n              <tr>\n                <td\n                  class=\"wrapper\"\n                  style=\"\n                    font-family: sans-serif;\n                    font-size: 14px;\n                    vertical-align: top;\n                    box-sizing: border-box;\n                    padding: 20px;\n                  \"\n                  valign=\"top\"\n                >\n                  <table\n                    role=\"presentation\"\n                    border=\"0\"\n                    cellpadding=\"0\"\n                    cellspacing=\"0\"\n                    style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%\"\n                    width=\"100%\"\n                  >\n                    <tr>\n                      <td\n                        style=\"\n                          font-family: sans-serif;\n                          font-size: 14px;\n                          vertical-align: top;\n                          padding-bottom: 30px;\n                          text-align: center;\n                        \"\n                        valign=\"top\"\n                      >\n                        <img\n                          style=\"margin-right: 30px\"\n                          height=\"37px\"\n                          width=\"45px\"\n                          src=\"https://app.rs.school/static/images/logo_rs.png\"\n                        />\n                        <img height=\"37px\" width=\"90px\" src=\"https://app.rs.school/static/images/logo-rsschool3.png\" />\n                      </td>\n                    </tr>\n                    <tr>\n                      <td>\n                      {{emailBody}}\n                      </td>\n                    </tr>\n                  </table>\n                </td>\n              </tr>\n\n              <!-- END MAIN CONTENT AREA -->\n            </table>\n            <!-- END CENTERED WHITE CONTAINER -->\n\n            <!-- START FOOTER -->\n            <div class=\"footer\" style=\"clear: both; margin-top: 10px; text-align: center; width: 100%\">\n              <table\n                role=\"presentation\"\n                border=\"0\"\n                cellpadding=\"0\"\n                cellspacing=\"0\"\n                style=\"border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%\"\n                width=\"100%\"\n              >\n                <tr>\n                  <td\n                    class=\"content-block\"\n                    style=\"\n                      font-family: sans-serif;\n                      vertical-align: top;\n                      padding-bottom: 10px;\n                      padding-top: 10px;\n                      color: #999999;\n                      font-size: 12px;\n                      text-align: center;\n                    \"\n                    valign=\"top\"\n                    align=\"center\"\n                  >\n                    <p>© The Rolling Scopes 2024</p>\n                    <a\n                      href=\"https://app.rs.school/profile/notifications\"\n                      style=\"text-decoration: underline; color: #999999; font-size: 12px; text-align: center\"\n                      >Unsubscribe from our emails</a\n                    >\n                  </td>\n                </tr>\n              </table>\n            </div>\n            <!-- END FOOTER -->\n          </div>\n        </td>\n        <td style=\"font-family: sans-serif; font-size: 14px; vertical-align: top\" valign=\"top\">&nbsp;</td>\n      </tr>\n    </table>\n  </body>\n</html>\n`;\n"
  },
  {
    "path": "nestjs/src/notifications/notifications.controller.ts",
    "content": "import { NotificationId } from '@entities/notification';\nimport { Body, Controller, Delete, Get, Param, Post, Put, UseGuards } from '@nestjs/common';\nimport { ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { NotificationDto } from './dto/notification.dto';\nimport { UpdateNotificationDto } from './dto/update-notification.dto';\n\nimport { NotificationsService } from './notifications.service';\n\n@Controller('notifications')\n@ApiTags('notifications')\n@UseGuards(DefaultGuard, RoleGuard)\n@RequiredRoles([Role.Admin])\nexport class NotificationsController {\n  constructor(private notificationsService: NotificationsService) {}\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getNotifications' })\n  @ApiForbiddenResponse()\n  @ApiOkResponse({ type: [NotificationDto] })\n  public async getNotifications() {\n    const notifications = await this.notificationsService.getNotifications();\n    return notifications.map(notification => new NotificationDto(notification));\n  }\n\n  @Put('/')\n  @ApiOperation({ operationId: 'updateNotification' })\n  @ApiOkResponse({ type: NotificationDto })\n  @ApiForbiddenResponse()\n  public async updateNotification(@Body() dto: UpdateNotificationDto) {\n    const notification = await this.notificationsService.saveNotification(dto);\n    return new NotificationDto(notification);\n  }\n\n  @Post('/')\n  @ApiOperation({ operationId: 'createNotification' })\n  @ApiOkResponse({ type: NotificationDto })\n  @ApiForbiddenResponse()\n  public async createNotification(@Body() dto: UpdateNotificationDto) {\n    const notification = await this.notificationsService.createNotification(dto);\n    return new NotificationDto(notification);\n  }\n\n  @Delete('/:id')\n  @ApiOperation({ operationId: 'deleteNotification' })\n  @ApiOkResponse()\n  @ApiForbiddenResponse()\n  public async deleteNotification(@Param('id') id: NotificationId) {\n    await this.notificationsService.deleteNotification(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/notifications/notifications.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { NotificationsService } from './notifications.service';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { NotificationsController } from './notifications.controller';\nimport { Notification } from '@entities/notification';\nimport { ConfigModule } from 'src/config';\nimport { HttpModule } from '@nestjs/axios';\nimport { NotificationChannelSettings } from '@entities/notificationChannelSettings';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Notification, NotificationChannelSettings]), ConfigModule, HttpModule],\n  controllers: [NotificationsController],\n  providers: [NotificationsService],\n  exports: [NotificationsService],\n})\nexport class NotificationsModule {}\n"
  },
  {
    "path": "nestjs/src/notifications/notifications.service.ts",
    "content": "import { Notification, NotificationId } from '@entities/notification';\nimport { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { ConfigService } from '../config';\nimport { Repository } from 'typeorm';\nimport { UpdateNotificationDto } from './dto/update-notification.dto';\nimport { HttpService } from '@nestjs/axios';\nimport {\n  DiscordTemplate,\n  EmailTemplate,\n  NotificationChannelSettings,\n  TelegramTemplate,\n} from '@entities/notificationChannelSettings';\nimport { compile } from 'handlebars';\nimport { NotificationChannelId } from '@entities/notificationChannel';\nimport { emailTemplate } from './email-template';\nimport { lastValueFrom } from 'rxjs';\n\nconst compiledEmailTemplate = compile(emailTemplate, { noEscape: true });\n@Injectable()\nexport class NotificationsService {\n  private readonly logger = new Logger('notifications');\n\n  constructor(\n    @InjectRepository(Notification)\n    private notificationsRepository: Repository<Notification>,\n    @InjectRepository(NotificationChannelSettings)\n    private channelSettingsRepository: Repository<NotificationChannelSettings>,\n    private configService: ConfigService,\n    private httpService: HttpService,\n  ) {}\n\n  public getNotification(id: NotificationId) {\n    return this.notificationsRepository.findOneBy({ id });\n  }\n\n  public getNotifications() {\n    return this.notificationsRepository.find({ relations: ['channels', 'parent'], order: { name: 'ASC' } });\n  }\n\n  public saveNotification(notificationUpdate: UpdateNotificationDto) {\n    const { parentId, ...notification } = notificationUpdate;\n\n    return this.notificationsRepository.save({\n      ...notification,\n      parent: parentId\n        ? {\n            id: parentId,\n          }\n        : null,\n    });\n  }\n\n  public async createNotification(notification: UpdateNotificationDto) {\n    const existing = await this.notificationsRepository.findOne({ where: { id: notification.id } });\n\n    if (existing) {\n      throw new BadRequestException(`notification with id ${notification.id} already exists`);\n    }\n\n    return this.notificationsRepository.save(notification);\n  }\n\n  public deleteNotification(id: NotificationId) {\n    return this.notificationsRepository.delete({ id });\n  }\n\n  /**\n   * Messages to users regardless on user subscription status to specific channel\n   */\n  public async sendMessage(notification: {\n    notificationId: NotificationId;\n    userId: number;\n    data: object;\n    channelId: NotificationChannelId;\n    channelValue: string;\n    noEscape?: boolean;\n  }) {\n    const { userId, data, notificationId, channelId, channelValue, noEscape } = notification;\n    const channelSettings = await this.getChannelSettings(channelId, notificationId);\n\n    const message = channelSettings\n      ? this.buildChannelMessage({ ...channelSettings, externalId: channelValue, noEscape }, data)\n      : null;\n\n    if (!message) {\n      this.logger.error({\n        message: `failed to build message fo notification ${notification.notificationId} and user ${notification.userId}`,\n      });\n      return;\n    }\n\n    await this.publishNotification({\n      notificationId,\n      channelId: [channelId],\n      userId,\n      data: {\n        [channelId]: {\n          template: message.template,\n          to: message.to,\n        },\n      },\n    });\n  }\n\n  private getChannelSettings(channelId: NotificationChannelId, notificationId: NotificationId) {\n    return this.channelSettingsRepository.findOne({\n      where: {\n        notificationId,\n        channelId,\n      },\n    });\n  }\n\n  public buildChannelMessage(\n    channel: NotificationChannelSettings & { externalId?: string; noEscape?: boolean },\n    data: object,\n  ) {\n    const { channelId, externalId, template, noEscape } = channel;\n    if (!externalId || !template) return;\n\n    const body = compile(channel.template.body, { noEscape })(data);\n    const channelMessage = {\n      channelId,\n      to: externalId,\n      template: {\n        body: channel.channelId === 'email' ? compiledEmailTemplate({ emailBody: body }) : body,\n      },\n    };\n    if (channel.channelId === 'email') {\n      (channelMessage.template as EmailTemplate).subject = (channel.template as EmailTemplate).subject;\n    }\n\n    return channelMessage;\n  }\n\n  public async publishNotification(notification: NotificationPayload) {\n    if (this.configService.isDev) return;\n\n    const { restApiKey, restApiUrl } = this.configService.awsServices;\n    await lastValueFrom(\n      this.httpService.post(`${restApiUrl}/v2/notification`, notification, {\n        headers: { 'x-api-key': restApiKey },\n      }),\n    );\n\n    this.logger.log({ message: `notification ${notification.notificationId} sent to ${notification.userId}` });\n  }\n}\n\ntype NotificationPayload = {\n  notificationId: NotificationId;\n  channelId: NotificationChannelId[];\n  userId: number;\n  expireDate?: number;\n  data: Partial<Record<NotificationChannelId, NotificationData>>;\n};\n\nexport type NotificationData = {\n  to: string;\n  template: EmailTemplate | TelegramTemplate | DiscordTemplate;\n};\n"
  },
  {
    "path": "nestjs/src/openapi-spec.ts",
    "content": "import { NestFactory } from '@nestjs/core';\nimport { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';\nimport { AppModule } from './app.module';\nimport * as fs from 'fs';\nimport { exit } from 'process';\n\nconst generate = async () => {\n  const config = new DocumentBuilder().addServer('/api/v2').build();\n  const app = await NestFactory.create(AppModule, { logger: ['error'] });\n  const document = SwaggerModule.createDocument(app, config);\n\n  fs.writeFileSync('./src/spec.json', JSON.stringify(document));\n};\n\ngenerate().then(() => exit(0));\n"
  },
  {
    "path": "nestjs/src/opportunities/dto/applicant-resume.dto.ts",
    "content": "import { LanguageLevel } from '@entities/data';\nimport { Resume } from '@entities/resume';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class ApplicantResumeDto {\n  constructor(resume: Resume) {\n    this.uuid = resume.uuid;\n    this.desiredPosition = resume.desiredPosition;\n    this.email = resume.email;\n    this.englishLevel = resume.englishLevel;\n    this.fullTime = resume.fullTime;\n    this.githubId = resume.user?.githubId;\n    this.linkedin = resume.linkedin;\n    this.locations = resume.locations;\n    this.militaryService = resume.militaryService;\n    this.name = resume.name;\n    this.notes = resume.notes;\n    this.phone = resume.phone;\n    this.selfIntroLink = resume.selfIntroLink;\n    this.skype = resume.skype;\n    this.startFrom = resume.startFrom;\n    this.telegram = resume.telegram;\n    this.website = resume.website;\n    this.expires = Number(resume.expires);\n  }\n\n  @ApiProperty()\n  public uuid: string;\n\n  @ApiProperty({ nullable: true, type: String })\n  public avatarLink: string | null;\n\n  @ApiProperty({ type: [Number] })\n  public visibleCourses: number[];\n\n  @ApiProperty({ nullable: true, type: String })\n  public desiredPosition: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public email: string | null;\n\n  @ApiProperty({ enum: LanguageLevel, nullable: true })\n  public englishLevel: LanguageLevel | null;\n\n  @ApiProperty({ nullable: true, type: Number })\n  public expires: number | null;\n\n  @ApiProperty()\n  public fullTime: boolean;\n\n  @ApiProperty()\n  public githubId: string;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty({ nullable: true, type: String })\n  public linkedin: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public locations: string | null;\n\n  @ApiProperty({ enum: ['served', 'liable', 'notLiable'], nullable: true })\n  public militaryService: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public name: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public notes: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public phone: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public selfIntroLink: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public skype: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public startFrom: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public telegram: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public website: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/dto/consent.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean } from 'class-validator';\n\nexport class ConsentDto {\n  constructor(consent: boolean) {\n    this.consent = consent;\n  }\n\n  @ApiProperty()\n  @IsBoolean()\n  public consent: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/dto/form-data.dto.ts",
    "content": "import { LanguageLevel } from '@entities/data';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsBoolean, IsString, ValidateIf } from 'class-validator';\n\nexport class FormDataDto {\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public avatarLink: string | null;\n\n  @IsArray()\n  @ApiProperty({ type: [Number] })\n  public visibleCourses: number[];\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public desiredPosition: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public email: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ enum: LanguageLevel, nullable: true })\n  public englishLevel: LanguageLevel | null;\n\n  @IsBoolean()\n  @ApiProperty()\n  public fullTime: boolean;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public githubUsername: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public linkedin: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public locations: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ enum: ['served', 'liable', 'notLiable'], nullable: true })\n  public militaryService: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public name: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public notes: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public phone: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public selfIntroLink: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public skype: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public startFrom: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public telegram: string | null;\n\n  @IsString()\n  @ValidateIf(value => value === null)\n  @ApiProperty({ nullable: true, type: String })\n  public website: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/dto/give-consent-dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsNumber } from 'class-validator';\n\nexport class GiveConsentDto {\n  constructor(consent: boolean, expires: number) {\n    this.consent = consent;\n    this.expires = expires;\n  }\n\n  @ApiProperty()\n  @IsBoolean()\n  public consent: boolean;\n\n  @ApiProperty()\n  @IsNumber()\n  public expires: number;\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/dto/resume.dto.ts",
    "content": "import { Feedback } from '@entities/feedback';\nimport { Mentor } from '@entities/mentor';\nimport { Resume } from '@entities/resume';\nimport { Student } from '@entities/student';\nimport { Rate, SoftSkill, StudentFeedback } from '@entities/student-feedback';\nimport { LanguageLevel } from '@entities/data';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { PersonDto } from '../../core/dto';\n\nclass GratitudeDto {\n  constructor(gratitude: Feedback) {\n    this.date = gratitude.createdDate;\n    this.comment = gratitude.comment ?? '';\n  }\n\n  @ApiProperty()\n  public date: string;\n\n  @ApiProperty()\n  public comment: string;\n}\n\nclass ResumeCourseMentor extends PersonDto {\n  constructor(mentor: Mentor) {\n    super(mentor.user);\n    this.githubId = mentor.user.githubId;\n  }\n\n  @ApiProperty()\n  public githubId: string;\n}\n\nclass FeedbackCourseDto {\n  constructor(feedback: StudentFeedback) {\n    this.name = feedback.student.course.name;\n    this.id = feedback.student.course.id;\n  }\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public id: number;\n}\n\nclass FeedbackSoftSkill {\n  constructor(id: SoftSkill, value: Rate) {\n    this.id = id;\n    this.value = value;\n  }\n\n  @ApiProperty({ enum: Rate })\n  public value: Rate;\n\n  @ApiProperty({ enum: SoftSkill })\n  public id: SoftSkill;\n}\n\nclass FeedbackDto {\n  constructor(feedback: StudentFeedback) {\n    this.recommendation = feedback.recommendation ?? '';\n    this.recommendationComment = feedback.content?.recommendationComment ?? '';\n    this.suggestions = feedback.content?.suggestions ?? '';\n    this.englishLevel = feedback.englishLevel ?? LanguageLevel.Unkwown;\n    this.mentor = new ResumeCourseMentor(feedback.mentor);\n    this.course = new FeedbackCourseDto(feedback);\n    this.softSkills = feedback.content?.softSkills.map(({ id, value }) => new FeedbackSoftSkill(id, value)) ?? [];\n  }\n\n  @ApiProperty()\n  public date: string;\n\n  @ApiProperty()\n  public recommendation: string;\n\n  @ApiProperty()\n  public englishLevel: string;\n\n  @ApiProperty()\n  public recommendationComment: string;\n\n  @ApiProperty()\n  public suggestions: string;\n\n  @ApiProperty({ type: [FeedbackSoftSkill] })\n  public softSkills: FeedbackSoftSkill[];\n\n  @ApiProperty({ type: ResumeCourseMentor })\n  public mentor: ResumeCourseMentor;\n\n  @ApiProperty({ type: FeedbackCourseDto })\n  public course: FeedbackCourseDto;\n}\n\nclass ResumeCourseDto {\n  constructor(student: Student) {\n    this.name = student.course.name;\n    this.fullName = student.course.fullName;\n    this.rank = student.rank;\n    this.totalScore = student.totalScore;\n    this.certificateId = student.certificate?.publicId ?? null;\n    this.completed = student.course.completed;\n    this.mentor = student.mentor ? new ResumeCourseMentor(student.mentor) : null;\n    this.locationName = student.course.locationName;\n    this.id = student.course.id;\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public fullName: string;\n\n  @ApiProperty()\n  public rank: number;\n\n  @ApiProperty()\n  public totalScore: number;\n\n  @ApiProperty({ nullable: true, type: String })\n  public certificateId: string | null;\n\n  @ApiProperty()\n  public completed: boolean;\n\n  @ApiProperty({ nullable: true, type: ResumeCourseMentor })\n  public mentor: ResumeCourseMentor | null;\n\n  @ApiProperty()\n  public locationName: string;\n}\n\nexport class ResumeDto {\n  constructor(resume: Resume, students: Student[], gratitudes: Feedback[], feedbacks: StudentFeedback[]) {\n    this.uuid = resume.uuid;\n    this.avatarLink = resume.avatarLink;\n    this.desiredPosition = resume.desiredPosition;\n    this.email = resume.email;\n    this.englishLevel = resume.englishLevel;\n    this.expires = resume.expires;\n    this.fullTime = resume.fullTime;\n    this.githubUsername = resume.githubUsername;\n    this.linkedin = resume.linkedin;\n    this.locations = resume.locations;\n    this.militaryService = resume.militaryService;\n    this.name = resume.name;\n    this.notes = resume.notes;\n    this.phone = resume.phone;\n    this.selfIntroLink = resume.selfIntroLink;\n    this.skype = resume.skype;\n    this.startFrom = resume.startFrom;\n    this.telegram = resume.telegram;\n    this.website = resume.website;\n    this.visibleCourses = resume.visibleCourses ?? [];\n    this.gratitudes = gratitudes.map(item => new GratitudeDto(item));\n    this.courses = students.map(item => new ResumeCourseDto(item));\n    this.feedbacks = feedbacks.map(item => new FeedbackDto(item));\n  }\n\n  @ApiProperty()\n  public uuid: string;\n\n  @ApiProperty({ nullable: true, type: String })\n  public avatarLink: string | null;\n\n  @ApiProperty({ type: [Number] })\n  public visibleCourses: number[];\n\n  @ApiProperty({ type: [ResumeCourseDto] })\n  public courses: ResumeCourseDto[];\n\n  @ApiProperty({ nullable: true, type: String })\n  public desiredPosition: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public email: string | null;\n\n  @ApiProperty({ enum: LanguageLevel, nullable: true })\n  public englishLevel: LanguageLevel | null;\n\n  @ApiProperty({ nullable: true, type: Number })\n  public expires: number | null;\n\n  @ApiProperty({ type: [GratitudeDto] })\n  public gratitudes: GratitudeDto[];\n\n  @ApiProperty({ type: [FeedbackDto] })\n  feedbacks: FeedbackDto[];\n\n  @ApiProperty()\n  public fullTime: boolean;\n\n  @ApiProperty({ nullable: true, type: String })\n  public githubUsername: string | null;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty({ nullable: true, type: String })\n  public linkedin: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public locations: string | null;\n\n  @ApiProperty({ enum: ['served', 'liable', 'notLiable'], nullable: true })\n  public militaryService: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public name: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public notes: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public phone: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public selfIntroLink: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public skype: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public startFrom: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public telegram: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public website: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/dto/status.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class StatusDto {\n  constructor(expires: number) {\n    this.expires = expires;\n  }\n\n  @ApiProperty()\n  public expires: number;\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/dto/visibility.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class VisibilityDto {\n  constructor(isHidden: boolean) {\n    this.isHidden = isHidden;\n  }\n\n  @ApiProperty()\n  public isHidden: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/opportunities.controller.ts",
    "content": "import {\n  Controller,\n  Delete,\n  ForbiddenException,\n  Get,\n  NotFoundException,\n  Param,\n  ParseUUIDPipe,\n  Post,\n  Patch,\n  Req,\n  UseGuards,\n  Body,\n} from '@nestjs/common';\nimport {\n  ApiBadRequestResponse,\n  ApiForbiddenResponse,\n  ApiNotFoundResponse,\n  ApiOkResponse,\n  ApiOperation,\n  ApiTags,\n} from '@nestjs/swagger';\nimport { CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { ApplicantResumeDto } from './dto/applicant-resume.dto';\nimport { ConsentDto } from './dto/consent.dto';\nimport { ResumeDto } from './dto/resume.dto';\nimport { StatusDto } from './dto/status.dto';\nimport { VisibilityDto } from './dto/visibility.dto';\nimport { OpportunitiesService } from './opportunities.service';\nimport { Resume } from '@entities/resume';\nimport { FormDataDto } from './dto/form-data.dto';\nimport { GiveConsentDto } from './dto/give-consent-dto';\n\n@Controller('opportunities')\n@ApiTags('opportunities')\nexport class OpportunitiesController {\n  constructor(private opportunitiesService: OpportunitiesService) {}\n\n  @Get('/:githubId/resume')\n  @ApiOperation({ operationId: 'getResume' })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiNotFoundResponse()\n  @ApiOkResponse({ type: ResumeDto })\n  @UseGuards(DefaultGuard)\n  public async getResume(@Req() req: CurrentRequest, @Param('githubId') githubId: string) {\n    if (githubId !== req.user.githubId) {\n      // TODO: limit access to own CV for now\n      throw new ForbiddenException('No access to resume');\n    }\n    const data = await this.opportunitiesService.getResumeByGithubId(githubId);\n    if (data == null) {\n      throw new NotFoundException('Resume not found');\n    }\n    const { resume, students, gratitudes, feedbacks } = data;\n    return new ResumeDto(resume, students, gratitudes, feedbacks);\n  }\n\n  @Patch('/:githubId/resume')\n  @ApiOperation({ operationId: 'saveResume' })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiNotFoundResponse()\n  @ApiOkResponse({ type: Resume })\n  @UseGuards(DefaultGuard)\n  public async saveResume(@Req() req: CurrentRequest, @Param('githubId') githubId: string, @Body() dto: FormDataDto) {\n    if (githubId !== req.user.githubId) {\n      throw new ForbiddenException('No access to resume');\n    }\n    const data = await this.opportunitiesService.saveResume(githubId, dto);\n    if (data == null) {\n      throw new NotFoundException('Resume not found');\n    }\n    return data;\n  }\n\n  @Get('/consent')\n  @ApiOperation({ operationId: 'getConsent' })\n  @ApiOkResponse({ type: ConsentDto })\n  @UseGuards(DefaultGuard)\n  public async getConsent(@Req() req: CurrentRequest) {\n    const value = await this.opportunitiesService.getConsent(req.user.githubId);\n    if (value == null) {\n      throw new NotFoundException('Resume not found');\n    }\n    return new ConsentDto(value);\n  }\n\n  @Post('/consent')\n  @ApiOperation({ operationId: 'createConsent' })\n  @ApiOkResponse({ type: GiveConsentDto })\n  @UseGuards(DefaultGuard)\n  public async createConsent(@Req() req: CurrentRequest) {\n    const { expires, consent } = await this.opportunitiesService.createConsent(req.user.githubId);\n    return new GiveConsentDto(consent, expires);\n  }\n\n  @Delete('/consent')\n  @ApiOperation({ operationId: 'deleteConsent' })\n  @ApiOkResponse({ type: ConsentDto })\n  @UseGuards(DefaultGuard)\n  public async deleteConsent(@Req() req: CurrentRequest) {\n    const data = await this.opportunitiesService.deleteConsent(req.user.githubId);\n    return new ConsentDto(data);\n  }\n\n  @Post('/prolong')\n  @ApiOperation({ operationId: 'prolong' })\n  @ApiOkResponse({ type: StatusDto })\n  @UseGuards(DefaultGuard)\n  public async prolong(@Req() req: CurrentRequest) {\n    const data = await this.opportunitiesService.prolong(req.user.githubId);\n    return new StatusDto(data);\n  }\n\n  @Post('/visibility')\n  @ApiOperation({ operationId: 'setVisibility' })\n  @ApiOkResponse({ type: VisibilityDto })\n  @UseGuards(DefaultGuard)\n  public async setVisibility(@Req() req: CurrentRequest, @Body('isHidden') isHidden: boolean) {\n    const data = await this.opportunitiesService.setVisibility(req.user.githubId, !isHidden);\n    return new VisibilityDto(data);\n  }\n\n  @Get('/public/:uuid')\n  @ApiOperation({ operationId: 'getPublicResume' })\n  @ApiForbiddenResponse()\n  @ApiBadRequestResponse()\n  @ApiNotFoundResponse()\n  @ApiOkResponse({ type: ResumeDto })\n  public async getPublicResume(@Param('uuid', ParseUUIDPipe) uuid: string) {\n    const data = await this.opportunitiesService.getResumeByUuid(uuid);\n    if (data == null) {\n      throw new NotFoundException('Resume not found');\n    }\n    const { resume, students, gratitudes, feedbacks } = data;\n    return new ResumeDto(resume, students, gratitudes, feedbacks);\n  }\n\n  @Get('/applicants')\n  @ApiOperation({ operationId: 'getApplicants' })\n  @ApiOkResponse({ type: [ApplicantResumeDto] })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  public async getApplicants() {\n    const data = await this.opportunitiesService.getApplicantResumes();\n    return data.map(item => new ApplicantResumeDto(item));\n  }\n}\n"
  },
  {
    "path": "nestjs/src/opportunities/opportunities.module.ts",
    "content": "import { User } from '@entities/user';\nimport { Feedback } from '@entities/feedback';\nimport { Resume } from '@entities/resume';\nimport { Student } from '@entities/student';\nimport { StudentFeedback } from '@entities/student-feedback';\nimport { HttpModule } from '@nestjs/axios';\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { ConfigModule } from 'src/config';\nimport { UsersModule } from 'src/users';\nimport { OpportunitiesController } from './opportunities.controller';\nimport { OpportunitiesService } from './opportunities.service';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([Resume, User, Student, Feedback, StudentFeedback]),\n    ConfigModule,\n    HttpModule,\n    UsersModule,\n  ],\n  controllers: [OpportunitiesController],\n  providers: [OpportunitiesService],\n  exports: [OpportunitiesService],\n})\nexport class OpportunitiesModule {}\n"
  },
  {
    "path": "nestjs/src/opportunities/opportunities.service.test.ts",
    "content": "import { Feedback } from '@entities/feedback';\nimport { Resume } from '@entities/resume';\nimport { Student } from '@entities/student';\nimport { StudentFeedback } from '@entities/student-feedback';\nimport { User } from '@entities/user';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { getRepositoryToken } from '@nestjs/typeorm';\nimport { OpportunitiesService } from './opportunities.service';\n\ndescribe('OpportunitiesService', () => {\n  let service: OpportunitiesService;\n\n  const resumeRepository = {\n    findOneBy: vi.fn(),\n    save: vi.fn(),\n  };\n\n  const feedbackRepository = {};\n  const studentFeedbackRepository = {};\n\n  const userRepository = {\n    findOneOrFail: vi.fn(),\n    update: vi.fn(),\n  };\n\n  const studentRepository = {};\n\n  beforeEach(async () => {\n    vi.clearAllMocks();\n\n    const module: TestingModule = await Test.createTestingModule({\n      providers: [\n        OpportunitiesService,\n        { provide: getRepositoryToken(Resume), useValue: resumeRepository },\n        { provide: getRepositoryToken(Feedback), useValue: feedbackRepository },\n        { provide: getRepositoryToken(StudentFeedback), useValue: studentFeedbackRepository },\n        { provide: getRepositoryToken(User), useValue: userRepository },\n        { provide: getRepositoryToken(Student), useValue: studentRepository },\n      ],\n    }).compile();\n\n    service = module.get<OpportunitiesService>(OpportunitiesService);\n  });\n\n  afterEach(() => {\n    vi.useRealTimers();\n  });\n\n  it('should prolong resume expiration by 30 days', async () => {\n    const fixedNow = new Date('2026-01-01T10:00:00.000Z');\n    const expectedExpires = fixedNow.getTime() + 30 * 24 * 60 * 60 * 1000;\n\n    vi.useFakeTimers();\n    vi.setSystemTime(fixedNow);\n\n    resumeRepository.findOneBy.mockResolvedValueOnce({ id: 5, githubId: 'john' });\n    resumeRepository.save.mockImplementationOnce(async (data: { expires: number }) => ({\n      id: 5,\n      githubId: 'john',\n      expires: data.expires,\n    }));\n\n    const expires = await service.prolong('john');\n\n    expect(expires).toBe(expectedExpires);\n    expect(resumeRepository.save).toHaveBeenCalledWith({\n      id: 5,\n      githubId: 'john',\n      expires: expectedExpires,\n    });\n  });\n\n  it('should create consent and set expiration in 30 days', async () => {\n    const fixedNow = new Date('2026-01-01T10:00:00.000Z');\n    const expectedExpires = fixedNow.getTime() + 30 * 24 * 60 * 60 * 1000;\n\n    vi.useFakeTimers();\n    vi.setSystemTime(fixedNow);\n\n    userRepository.findOneOrFail.mockResolvedValueOnce({ id: 11 });\n    userRepository.update.mockResolvedValueOnce(undefined);\n    resumeRepository.findOneBy.mockResolvedValueOnce({ id: 7, githubId: 'john' });\n    resumeRepository.save.mockImplementationOnce(async (data: { expires: number }) => ({\n      expires: data.expires,\n    }));\n\n    const result = await service.createConsent('john');\n\n    expect(result).toEqual({\n      consent: true,\n      expires: expectedExpires,\n    });\n    expect(userRepository.update).toHaveBeenCalledWith(11, { opportunitiesConsent: true });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/opportunities/opportunities.service.ts",
    "content": "import { In, Repository } from 'typeorm';\nimport { addDays } from 'date-fns';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { User } from '@entities/user';\nimport { Feedback } from '@entities/feedback';\nimport { Resume } from '@entities/resume';\nimport { Recommendation, StudentFeedback } from '@entities/student-feedback';\nimport { Student } from '@entities/student';\nimport { FormDataDto } from './dto/form-data.dto';\n\nconst EXPIRATION_DAYS_PROLONGATION = 30;\n\ntype ResumeData = {\n  resume: Resume;\n  students: Student[];\n  gratitudes: Feedback[];\n  feedbacks: StudentFeedback[];\n};\n\n@Injectable()\nexport class OpportunitiesService {\n  constructor(\n    @InjectRepository(Resume)\n    private resumeRepository: Repository<Resume>,\n    @InjectRepository(Feedback)\n    private feedbackRepository: Repository<Feedback>,\n    @InjectRepository(StudentFeedback)\n    private studentFeedbackRepository: Repository<StudentFeedback>,\n    @InjectRepository(User)\n    private userRepository: Repository<User>,\n    @InjectRepository(Student)\n    private studentRepository: Repository<Student>,\n  ) {}\n\n  public async getResumeByUuid(uuid: string): Promise<ResumeData | null> {\n    const resume = await this.resumeRepository.findOneOrFail({ where: { uuid } });\n    return await this.getFullResume(resume);\n  }\n\n  public async getResumeByGithubId(githubId: string): Promise<ResumeData | null> {\n    const user = await this.userRepository.findOneOrFail({ where: { githubId } });\n    const resume = await this.resumeRepository.findOne({ where: { userId: user.id } });\n    if (resume == null) return null;\n    return await this.getFullResume(resume, false);\n  }\n\n  public async saveResume(githubId: string, dto: FormDataDto): Promise<Resume | null> {\n    const resume = await this.resumeRepository.findOneBy({ githubId });\n    if (resume == null) return null;\n    const dataToSave = { ...resume, ...dto };\n    return await this.resumeRepository.save(dataToSave);\n  }\n\n  public async getApplicantResumes(): Promise<Resume[]> {\n    const resume = await this.resumeRepository\n      .createQueryBuilder('r')\n      .innerJoin('r.user', 'u')\n      .where('u.\"opportunitiesConsent\" = true')\n      .andWhere('r.name IS NOT NULL')\n      .addSelect(['u.id', 'u.githubId'])\n      .getMany();\n\n    return resume;\n  }\n\n  public async prolong(githubId: string) {\n    const resume = await this.resumeRepository.findOneBy({ githubId });\n    const expirationTimestamp = this.getProlongedExpirationTimestamp();\n    const result = await this.resumeRepository.save({ id: resume?.id, githubId, expires: expirationTimestamp });\n    return result.expires;\n  }\n\n  public async setVisibility(githubId: string, isVisible: boolean) {\n    const resume = await this.resumeRepository.findOneBy({ githubId });\n    const isHidden = !isVisible;\n    const savedResume = await this.resumeRepository.save({ id: resume?.id, githubId, isHidden });\n    return savedResume.isHidden;\n  }\n\n  public async getConsent(githubId: string) {\n    const user = await this.userRepository.findOne({ where: { githubId } });\n    if (user == null) return false;\n    const value = user.opportunitiesConsent;\n    return Boolean(value);\n  }\n\n  public async createConsent(githubId: string): Promise<{ consent: boolean; expires: number }> {\n    const value = true;\n    const { id: userId } = await this.userRepository.findOneOrFail({ where: { githubId } });\n    await this.userRepository.update(userId, { opportunitiesConsent: value });\n    const current = await this.resumeRepository.findOneBy({ githubId });\n    const expiresIn = this.getProlongedExpirationTimestamp();\n    const { expires } = await this.resumeRepository.save({ id: current?.id, githubId, userId, expires: expiresIn });\n    return {\n      consent: value,\n      expires,\n    };\n  }\n\n  public async deleteConsent(githubId: string) {\n    const value = false;\n    const user = await this.userRepository.findOneOrFail({ where: { githubId } });\n    await this.userRepository.update(user.id, { opportunitiesConsent: value });\n    await this.resumeRepository.delete({ githubId });\n    return Boolean(value);\n  }\n\n  private async getFullResume(resume: Resume, visibleCourseOnly = true): Promise<ResumeData> {\n    const [students, gratitudes] = await Promise.all([\n      resume.userId\n        ? this.studentRepository.find({\n            relations: ['course', 'certificate', 'mentor', 'mentor.user'],\n            where: {\n              userId: resume.userId,\n              // if visibleCourses is not defined, then we show info from all courses\n              ...(visibleCourseOnly && resume.visibleCourses.length ? { courseId: In(resume.visibleCourses) } : {}),\n            },\n          })\n        : Promise.resolve([]),\n      resume.userId\n        ? this.feedbackRepository.find({ where: { toUserId: resume.userId }, order: { createdDate: 'DESC' } })\n        : Promise.resolve([]),\n    ]);\n\n    const feedbacks = await this.studentFeedbackRepository.find({\n      relations: ['student', 'student.course', 'mentor', 'mentor.user'],\n      where: { studentId: In(students.map(s => s.id)), recommendation: Recommendation.Hire },\n    });\n\n    return {\n      resume,\n      students,\n      gratitudes,\n      feedbacks,\n    };\n  }\n\n  private getProlongedExpirationTimestamp() {\n    const expirationDate = addDays(new Date(), EXPIRATION_DAYS_PROLONGATION);\n    const expirationTimestamp = expirationDate.valueOf();\n    return expirationTimestamp;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/ormconfig.ts",
    "content": "import { DataSourceOptions } from 'typeorm';\nimport * as path from 'path';\nimport { models } from '@entities/index';\nimport { migrations } from './migrations';\n\nconst config: DataSourceOptions = {\n  type: 'postgres',\n  ssl: process.env.RS_ENV\n    ? {\n        rejectUnauthorized: false,\n      }\n    : undefined, // localhost should not use ssl\n  host: process.env.RSSHCOOL_PG_HOST,\n  port: process.env.RS_ENV !== 'staging' ? 5432 : undefined,\n  username: process.env.RSSHCOOL_PG_USERNAME,\n  password: process.env.RSSHCOOL_PG_PASSWORD,\n  database: process.env.RSSHCOOL_PG_DATABASE,\n  entities: models,\n  migrations,\n  synchronize: false,\n  migrationsRun: false,\n  subscribers: [path.resolve(__dirname, '**/*.subscriber.*')],\n  logging: ['migration', 'error', 'warn'],\n};\n\nexport default config;\n"
  },
  {
    "path": "nestjs/src/profile/dto/endorsement.dto.ts",
    "content": "import { Course } from '@entities/course';\nimport { Feedback } from '@entities/feedback';\nimport { Mentor } from '@entities/mentor';\nimport { User } from '@entities/user';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { CourseDto } from 'src/courses/dto';\n\ntype Data = {\n  user: User;\n  courses: Course[];\n  mentors: Mentor[];\n  studentsCount: number;\n  interviewsCount: number;\n  feedbacks: Feedback[];\n};\n\nclass EndorsementUserDto {\n  constructor(user: User) {\n    this.id = user.id;\n    this.githubId = user.githubId;\n    this.firstName = user.firstName;\n    this.lastName = user.lastName;\n  }\n  @ApiProperty({ type: Number })\n  public readonly id: number;\n\n  @ApiProperty({ type: String })\n  public readonly githubId: string;\n\n  @ApiProperty({ type: String })\n  public readonly firstName: string;\n\n  @ApiProperty({ type: String })\n  public readonly lastName: string;\n}\n\nclass FeedbackDto {\n  constructor(feedback: Feedback) {\n    this.id = feedback.id;\n    this.comment = feedback.comment;\n  }\n\n  @ApiProperty({ type: Number })\n  public readonly id: number;\n\n  @ApiProperty({ type: String })\n  public readonly comment: string | null;\n}\n\nexport class EndorsementDto {\n  constructor(profile: { content: string; data: object } | null) {\n    this.summary = profile?.content ?? 'We do not have enough data to generate endorsement.';\n    this.data = profile?.data ?? null;\n  }\n\n  @ApiProperty({ type: String })\n  public summary: string;\n\n  @ApiProperty({ type: Object, nullable: true })\n  public data: object | null;\n}\n\nexport class EndorsementDataDto {\n  constructor(data: Data) {\n    this.user = data.user;\n    this.courses = data.courses.map(course => new CourseDto(course));\n    this.studentsCount = data.studentsCount;\n    this.interviewsCount = data.interviewsCount;\n    this.feedbacks = data.feedbacks;\n  }\n\n  @ApiProperty({ type: EndorsementUserDto })\n  public user: User;\n\n  @ApiProperty({ type: CourseDto, isArray: true, description: `User's courses` })\n  public courses: CourseDto[];\n\n  @ApiProperty({ type: Number, description: `Number of students` })\n  public studentsCount: number;\n\n  @ApiProperty({ type: Number, description: `Number of interviews` })\n  public interviewsCount: number;\n\n  public feedbacks: FeedbackDto[];\n}\n"
  },
  {
    "path": "nestjs/src/profile/dto/index.ts",
    "content": "export * from './profile-course.dto';\nexport * from './update-user.dto';\nexport * from './update-profile.dto';\n"
  },
  {
    "path": "nestjs/src/profile/dto/personal-profile.dto.ts",
    "content": "import { User } from '@entities/index';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class PersonalProfileDto {\n  constructor(user: User) {\n    this.userId = user.id;\n    this.githubId = user.githubId;\n    this.primaryEmail = user.primaryEmail ?? null;\n    this.isActiveStudent = user.students?.some(s => !s.isExpelled && !s.isFailed && s.totalScore > 0) ?? false;\n  }\n\n  @ApiProperty({ type: Number })\n  public userId: number;\n\n  @ApiProperty({ type: String })\n  public githubId: string;\n\n  @ApiProperty({ type: String, nullable: true })\n  public primaryEmail: string | null;\n\n  @ApiProperty({ type: Boolean })\n  public isActiveStudent: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/profile/dto/profile-course.dto.ts",
    "content": "import { Course } from '@entities/course';\nimport { CourseDto } from '../../courses/dto';\n\nexport class ProfileCourseDto extends CourseDto {\n  constructor(course: Course) {\n    super(course);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/profile/dto/profile.dto.ts",
    "content": "import { Resume } from '@entities/resume';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport class ProfileDto {\n  constructor(profile: { resume: Resume | null }) {\n    this.publicCvUrl = profile.resume?.uuid ? `/cv/${profile.resume.uuid}` : null;\n  }\n\n  @ApiProperty({ type: String, nullable: true })\n  public publicCvUrl: string | null;\n}\n"
  },
  {
    "path": "nestjs/src/profile/dto/update-profile.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport { IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';\nimport { Contacts, EnglishLevel } from '@common/models';\n\nclass Location {\n  @IsString()\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  cityName: string | null;\n\n  @IsString()\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  countryName: string | null;\n}\n\nclass PublicVisibilitySettings {\n  @ApiProperty()\n  @IsBoolean()\n  all: boolean;\n}\n\nclass PartialStudentVisibilitySettings extends PublicVisibilitySettings {\n  @ApiProperty()\n  @IsBoolean()\n  student: boolean;\n}\n\nclass ContactsVisibilitySettings extends PublicVisibilitySettings {\n  @ApiProperty()\n  @IsBoolean()\n  student: boolean;\n}\n\nclass VisibilitySettings extends PublicVisibilitySettings {\n  @ApiProperty()\n  @IsBoolean()\n  mentor: boolean;\n\n  @ApiProperty()\n  @IsBoolean()\n  student: boolean;\n}\n\nclass ConfigurableProfilePermissions {\n  @ApiProperty({ required: false, type: PublicVisibilitySettings })\n  @Type(() => PublicVisibilitySettings)\n  @IsOptional()\n  isProfileVisible?: PublicVisibilitySettings;\n\n  @ApiProperty({ required: false, type: VisibilitySettings })\n  @Type(() => VisibilitySettings)\n  @IsOptional()\n  isAboutVisible?: VisibilitySettings;\n\n  @ApiProperty({ required: false, type: VisibilitySettings })\n  @Type(() => VisibilitySettings)\n  @IsOptional()\n  isEducationVisible?: VisibilitySettings;\n\n  @ApiProperty({ required: false, type: PartialStudentVisibilitySettings })\n  @Type(() => PartialStudentVisibilitySettings)\n  @IsOptional()\n  isEnglishVisible?: PartialStudentVisibilitySettings;\n\n  @ApiProperty({ required: false, type: ContactsVisibilitySettings })\n  @Type(() => ContactsVisibilitySettings)\n  @IsOptional()\n  isEmailVisible?: ContactsVisibilitySettings;\n\n  @ApiProperty({ required: false, type: ContactsVisibilitySettings })\n  @Type(() => ContactsVisibilitySettings)\n  @IsOptional()\n  isTelegramVisible?: ContactsVisibilitySettings;\n\n  @ApiProperty({ required: false, type: ContactsVisibilitySettings })\n  @Type(() => ContactsVisibilitySettings)\n  @IsOptional()\n  isSkypeVisible?: ContactsVisibilitySettings;\n\n  @ApiProperty({ required: false, type: ContactsVisibilitySettings })\n  @Type(() => ContactsVisibilitySettings)\n  @IsOptional()\n  isPhoneVisible?: ContactsVisibilitySettings;\n\n  @ApiProperty({ required: false, type: ContactsVisibilitySettings })\n  @Type(() => ContactsVisibilitySettings)\n  @IsOptional()\n  isContactsNotesVisible?: ContactsVisibilitySettings;\n\n  @ApiProperty({ required: false, type: VisibilitySettings })\n  @Type(() => VisibilitySettings)\n  @IsOptional()\n  isLinkedInVisible?: VisibilitySettings;\n\n  @ApiProperty({ required: false, type: VisibilitySettings })\n  @Type(() => VisibilitySettings)\n  @IsOptional()\n  isPublicFeedbackVisible?: VisibilitySettings;\n\n  @ApiProperty({ required: false, type: VisibilitySettings })\n  @Type(() => VisibilitySettings)\n  @IsOptional()\n  isMentorStatsVisible?: VisibilitySettings;\n\n  @ApiProperty({ required: false, type: PartialStudentVisibilitySettings })\n  @Type(() => PartialStudentVisibilitySettings)\n  @IsOptional()\n  isStudentStatsVisible?: PartialStudentVisibilitySettings;\n}\n\nclass Education {\n  @ApiProperty()\n  @IsString()\n  university: string;\n\n  @ApiProperty()\n  @IsString()\n  faculty: string;\n\n  @ApiProperty()\n  @IsNumber()\n  graduationYear: number;\n}\n\nclass GeneralInfo {\n  @ApiProperty()\n  @IsString()\n  name: string;\n\n  @ApiProperty()\n  @IsString()\n  githubId: string;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  aboutMyself?: string | null;\n\n  @ApiProperty({ type: Location })\n  @Type(() => Location)\n  @ValidateNested()\n  location: Location;\n\n  @ApiProperty({ required: false, nullable: true, type: [Education] })\n  @IsOptional()\n  @Type(() => Education)\n  @IsArray()\n  educationHistory?: Education[] | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  englishLevel?: EnglishLevel | null;\n}\n\nexport class ContactsDto implements Contacts {\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  phone: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  email: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  epamEmail: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  skype: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  whatsApp: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  telegram: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  notes: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  linkedIn: string | null;\n}\n\nexport class Discord {\n  @ApiProperty()\n  @IsNotEmpty()\n  id: string;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  username: string;\n\n  @ApiProperty()\n  @IsNotEmpty()\n  discriminator: string;\n}\n\nexport class ProfileInfoDto {\n  @ApiProperty({ type: ConfigurableProfilePermissions })\n  @ValidateNested()\n  @Type(() => ConfigurableProfilePermissions)\n  permissionsSettings: ConfigurableProfilePermissions;\n\n  @ApiProperty({ type: GeneralInfo })\n  @ValidateNested()\n  @Type(() => GeneralInfo)\n  generalInfo: GeneralInfo;\n\n  @ApiProperty({ type: ContactsDto })\n  @ValidateNested()\n  @Type(() => ContactsDto)\n  contacts: ContactsDto;\n\n  @ApiProperty({ required: false, nullable: true, type: Discord })\n  @Type(() => Discord)\n  @IsOptional()\n  discord: Discord | null;\n\n  @ApiProperty()\n  @IsBoolean()\n  isPermissionsSettingsChanged: boolean;\n\n  @ApiProperty()\n  @IsBoolean()\n  isProfileSettingsChanged: boolean;\n}\n\nexport class UpdateProfileInfoDto {\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsString()\n  name?: string;\n\n  @ApiProperty({ required: false })\n  @IsOptional()\n  @IsString()\n  githubId?: string;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  aboutMyself?: string | null;\n\n  @IsString()\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  cityName?: string | null;\n\n  @IsString()\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  countryName?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: [Education] })\n  @IsOptional()\n  @IsArray()\n  educationHistory?: Education[];\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  englishLevel?: EnglishLevel | null;\n\n  @ApiProperty({ required: false, type: [String] })\n  @IsOptional()\n  @IsArray()\n  languages?: string[];\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsPhone?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsEmail?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsEpamEmail?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsSkype?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsWhatsApp?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsTelegram?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsNotes?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: String })\n  @IsOptional()\n  @IsString()\n  contactsLinkedIn?: string | null;\n\n  @ApiProperty({ required: false, nullable: true, type: Discord })\n  @ValidateNested()\n  @IsOptional()\n  @Type(() => Discord)\n  discord?: Discord | null;\n}\n"
  },
  {
    "path": "nestjs/src/profile/dto/update-user.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsEnum, IsOptional, IsString } from 'class-validator';\nimport { AvailableLanguages } from '@entities/data';\n\nexport class UpdateUserDto {\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  firstName?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  lastName?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  primaryEmail?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  cityName?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  countryName?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsNotes?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsPhone?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsEmail?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsEpamEmail?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsSkype?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsWhatsApp?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsTelegram?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  contactsLinkedIn?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  notes?: string;\n\n  @ApiProperty({ required: false, nullable: true })\n  @IsOptional()\n  @IsString()\n  aboutMyself?: string;\n\n  @ApiProperty({\n    enum: AvailableLanguages,\n    isArray: true,\n    required: false,\n  })\n  @IsOptional()\n  @IsArray()\n  @IsEnum(AvailableLanguages, { each: true })\n  languages?: AvailableLanguages[];\n}\n"
  },
  {
    "path": "nestjs/src/profile/endorsement.service.ts",
    "content": "import { Course } from '@entities/course';\nimport { Feedback, Mentor, Student, TaskInterviewResult, User } from '@entities/index';\nimport { Prompt } from '@entities/prompt';\nimport { Injectable, Logger, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { In, Repository } from 'typeorm';\nimport OpenAI from 'openai';\nimport { compile } from 'handlebars';\nimport { ConfigService } from 'src/config';\n\n@Injectable()\nexport class EndorsementService {\n  private openAI: OpenAI;\n\n  private logger = new Logger(EndorsementService.name);\n\n  constructor(\n    @InjectRepository(Course)\n    private courseRepository: Repository<Course>,\n    @InjectRepository(Mentor)\n    private mentorRepository: Repository<Mentor>,\n    @InjectRepository(Student)\n    private studentRepository: Repository<Student>,\n    @InjectRepository(Prompt)\n    private promptRepository: Repository<Prompt>,\n    @InjectRepository(Feedback)\n    private feedbackRepository: Repository<Feedback>,\n    @InjectRepository(User)\n    private userRepository: Repository<User>,\n    @InjectRepository(TaskInterviewResult)\n    private taskInterviewResultRepository: Repository<TaskInterviewResult>,\n    private readonly configService: ConfigService,\n  ) {\n    this.openAI = new OpenAI(this.configService.openai);\n  }\n\n  public async getEndorsement(githubId: string): Promise<{ content: string; data: object } | null> {\n    try {\n      const prompt = await this.getEndorsementPrompt(githubId);\n      if (!prompt) {\n        return null;\n      }\n      this.logger.log(`Endorsement prompt found`);\n      const result = await this.openAI.chat.completions.create({\n        model: 'gpt-5',\n        messages: [{ role: 'user', content: prompt.text }],\n      });\n      this.logger.log(`Open AI response received`, result);\n      const content = result.choices[0]?.message?.content ?? '';\n      return { content, data: prompt.data };\n    } catch (error) {\n      this.logger.error(error);\n      return null;\n    }\n  }\n\n  async getEndorsmentData(githubId: string) {\n    const user = await this.userRepository.findOne({ where: { githubId } });\n    if (!user) {\n      throw new NotFoundException(`User with githubId ${githubId} not found`);\n    }\n\n    const [mentors, feedbacks] = await Promise.all([\n      this.mentorRepository.find({ where: { userId: user.id } }),\n      this.feedbackRepository.find({ where: { toUserId: user.id } }),\n    ]);\n\n    const courses = await this.courseRepository.find({\n      where: { id: In(mentors.map(m => m.courseId)) },\n      relations: ['discipline'],\n    });\n    const mentorIds = mentors.map(m => m.id);\n    const [studentsCount, interviewsCount] = await Promise.all([\n      this.studentRepository.count({ where: { mentorId: In(mentorIds) } }),\n      this.taskInterviewResultRepository.count({ where: { mentorId: In(mentorIds) } }),\n    ]);\n\n    const data = {\n      user,\n      courses,\n      mentors,\n      studentsCount,\n      interviewsCount,\n      feedbacks,\n    };\n    return data;\n  }\n\n  async getEndorsementPrompt(githubId: string) {\n    const [prompt, data] = await Promise.all([\n      this.promptRepository.findOne({ where: { type: 'endorsement' } }),\n      this.getEndorsmentData(githubId),\n    ]);\n\n    if (!prompt?.text || data.mentors.length === 0) {\n      this.logger.warn(`No prompt text or mentors found for githubId: ${githubId}`);\n      return null;\n    }\n\n    return { text: compile(prompt.text)(data), temperature: prompt.temperature, data };\n  }\n}\n"
  },
  {
    "path": "nestjs/src/profile/index.ts",
    "content": "export * from './profile.module';\n"
  },
  {
    "path": "nestjs/src/profile/profile.controller.ts",
    "content": "import { Body, Controller, Delete, ForbiddenException, Get, Param, Patch, Post, Req, UseGuards } from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { CoursesService } from 'src/courses/courses.service';\nimport { CurrentRequest } from '../auth/auth.service';\nimport { ProfileCourseDto, UpdateUserDto, UpdateProfileInfoDto } from './dto';\nimport { ProfileDto } from './dto/profile.dto';\nimport { ProfileService } from './profile.service';\nimport { PersonalProfileDto } from './dto/personal-profile.dto';\nimport { EndorsementService } from './endorsement.service';\nimport { EndorsementDataDto, EndorsementDto } from './dto/endorsement.dto';\n\n@Controller('profile')\n@ApiTags('profile')\nexport class ProfileController {\n  constructor(\n    private readonly profileService: ProfileService,\n    private readonly endormentService: EndorsementService,\n    private readonly coursesService: CoursesService,\n  ) {}\n\n  @Get(':username/courses')\n  @ApiOperation({ operationId: 'getUserCourses' })\n  @ApiOkResponse({ type: [ProfileCourseDto] })\n  @UseGuards(DefaultGuard)\n  public async getCourses(\n    @Req() req: CurrentRequest,\n    @Param('username') username: string,\n  ): Promise<ProfileCourseDto[]> {\n    const user = req.user;\n    if (user.isAdmin) {\n      const data = await this.coursesService.getAll();\n      return data.map(course => new ProfileCourseDto(course));\n    }\n    if (username !== user.githubId && username !== 'me') {\n      throw new ForbiddenException();\n    }\n    const data = await this.profileService.getCourses(user);\n    return data.map(course => new ProfileCourseDto(course));\n  }\n\n  @Post('/user')\n  @ApiOperation({ operationId: 'updateUser' })\n  @ApiBody({ type: UpdateUserDto })\n  @UseGuards(DefaultGuard)\n  public async updateUser(@Req() req: CurrentRequest, @Body() dto: UpdateUserDto) {\n    const { user } = req;\n\n    await this.profileService.updateUser(user.id, dto);\n  }\n\n  @Patch('/info')\n  @ApiOperation({ operationId: 'updateProfileInfoFlat' })\n  @ApiBody({ type: UpdateProfileInfoDto })\n  @UseGuards(DefaultGuard)\n  public async updateProfileFlatInfo(@Req() req: CurrentRequest, @Body() dto: UpdateProfileInfoDto) {\n    const { user } = req;\n\n    await this.profileService.updateProfileFlat(user.id, dto);\n  }\n\n  @Get(':username')\n  @ApiOperation({ operationId: 'getProfile' })\n  @ApiResponse({ type: ProfileDto })\n  @UseGuards(DefaultGuard)\n  public async getProfileInfo(@Param('username') githubId: string) {\n    const profile = await this.profileService.getProfile(githubId);\n\n    return new ProfileDto(profile);\n  }\n\n  @Get(':username/personal')\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'getPersonalProfile' })\n  @ApiResponse({ type: PersonalProfileDto })\n  public async getPersonalProfile(@Param('username') githubId: string) {\n    const user = await this.profileService.getPersonalProfile(githubId);\n\n    return new PersonalProfileDto(user);\n  }\n\n  @Get(':username/endorsement')\n  @ApiOperation({ operationId: 'getEndorsement' })\n  @ApiResponse({ type: EndorsementDto })\n  @UseGuards(DefaultGuard, RoleGuard)\n  @RequiredRoles([Role.Admin])\n  public async getEndorsement(@Param('username') githubId: string) {\n    const endorsement = await this.endormentService.getEndorsement(githubId);\n    return new EndorsementDto(endorsement);\n  }\n\n  @Get(':username/endorsement-data')\n  @ApiOperation({ operationId: 'getEndorsementData' })\n  @ApiResponse({ type: EndorsementDataDto })\n  @UseGuards(DefaultGuard)\n  public async getEndorsementData(@Param('username') githubId: string) {\n    const data = await this.endormentService.getEndorsmentData(githubId);\n    return new EndorsementDataDto(data);\n  }\n\n  @Delete(':username')\n  @ApiOperation({ operationId: 'obfuscateProfile' })\n  @UseGuards(DefaultGuard, RoleGuard)\n  public async obfuscateProfile(@Param('username') githubId: string) {\n    await this.profileService.obfuscateProfile(githubId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/profile/profile.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ProfileService } from './profile.service';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Course } from '@entities/course';\nimport { ProfileController } from './profile.controller';\nimport { CoursesModule } from '../courses/courses.module';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { User } from '@entities/user';\nimport { ProfilePermissions } from '@entities/profilePermissions';\nimport { UsersNotificationsModule } from 'src/users-notifications/users-notifications.module';\nimport { Resume } from '@entities/resume';\nimport { EndorsementService } from './endorsement.service';\nimport { Certificate, Feedback, Mentor, Prompt, Student, TaskInterviewResult } from '@entities/index';\nimport { ConfigModule } from 'src/config';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([\n      Certificate,\n      Course,\n      Feedback,\n      Mentor,\n      NotificationUserConnection,\n      ProfilePermissions,\n      Prompt,\n      Resume,\n      Student,\n      TaskInterviewResult,\n      User,\n    ]),\n    UsersNotificationsModule,\n    CoursesModule,\n    ConfigModule,\n  ],\n  controllers: [ProfileController],\n  providers: [ProfileService, EndorsementService],\n  exports: [ProfileService],\n})\nexport class ProfileModule {}\n"
  },
  {
    "path": "nestjs/src/profile/profile.service.ts",
    "content": "import { Course } from '@entities/course';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { ProfilePermissions } from '@entities/profilePermissions';\nimport { User } from '@entities/user';\nimport { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { AuthUser } from 'src/auth';\nimport { In, IsNull, Not, Repository, UpdateResult } from 'typeorm';\nimport { UserNotificationsService } from '../users-notifications';\nimport { ProfileInfoDto, UpdateProfileInfoDto, UpdateUserDto } from './dto';\nimport { isEmail } from 'class-validator';\nimport { Resume } from '@entities/resume';\nimport { Discord } from '../../../common/models';\nimport { omitBy, isUndefined } from 'lodash';\nimport { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';\nimport { nanoid } from 'nanoid';\nimport { Certificate } from '@entities/certificate';\n\n@Injectable()\nexport class ProfileService {\n  constructor(\n    @InjectRepository(Course)\n    private courseRepository: Repository<Course>,\n    @InjectRepository(NotificationUserConnection)\n    private notificationConnectionsRepository: Repository<NotificationUserConnection>,\n    @InjectRepository(User)\n    private userRepository: Repository<User>,\n    @InjectRepository(Certificate)\n    private certificateRepository: Repository<Certificate>,\n    @InjectRepository(Resume)\n    private resumeRepository: Repository<Resume>,\n    @InjectRepository(ProfilePermissions)\n    private profilePermissionsRepository: Repository<ProfilePermissions>,\n    private userNotificationsService: UserNotificationsService,\n  ) {}\n\n  public async getCourses(authUser: AuthUser): Promise<Course[]> {\n    const courseIds = Object.keys(authUser.courses).map(Number);\n\n    return this.courseRepository.find({\n      cache: 120 * 1000,\n      where: { id: In(courseIds) },\n      order: {\n        startDate: 'DESC',\n      },\n      relations: ['discipline'],\n    });\n  }\n\n  public async updateUser(userId: number, userDto: UpdateUserDto) {\n    const {\n      firstName,\n      lastName,\n      countryName,\n      cityName,\n      contactsTelegram,\n      contactsPhone,\n      contactsEmail,\n      contactsNotes,\n      contactsSkype,\n      contactsWhatsApp,\n      contactsLinkedIn,\n      contactsEpamEmail,\n      aboutMyself,\n      languages,\n    } = userDto;\n\n    await this.userRepository\n      .createQueryBuilder()\n      .update(User)\n      .set({\n        firstName,\n        lastName,\n        countryName,\n        cityName,\n        contactsTelegram,\n        contactsPhone,\n        contactsEmail,\n        contactsNotes,\n        contactsSkype,\n        contactsWhatsApp,\n        contactsLinkedIn,\n        contactsEpamEmail,\n        aboutMyself,\n        languages,\n      })\n      .returning('*')\n      .where('id = :id', { id: userId })\n      .execute();\n  }\n\n  public async updateProfile(userId: number, profileInfo: ProfileInfoDto) {\n    const {\n      isPermissionsSettingsChanged,\n      isProfileSettingsChanged,\n      permissionsSettings,\n      contacts,\n      discord,\n      generalInfo,\n    } = profileInfo;\n\n    if (isPermissionsSettingsChanged) {\n      const userPermissions = await this.profilePermissionsRepository.findOne({ where: { userId } });\n\n      await this.profilePermissionsRepository.save({\n        id: userPermissions?.id,\n        userId,\n        ...permissionsSettings,\n      });\n    }\n\n    if (isProfileSettingsChanged) {\n      const [firstName, lastName = ''] = generalInfo.name.split(' ');\n      const { location, aboutMyself, educationHistory, englishLevel } = generalInfo;\n      const { skype, whatsApp, phone, email, epamEmail, telegram, notes, linkedIn } = contacts;\n      const { countryName, cityName } = location;\n      if (email && !isEmail(email)) {\n        throw new BadRequestException('Email is invalid.');\n      }\n      if (epamEmail && !isEmail(epamEmail)) {\n        throw new BadRequestException('Epam email is invalid.');\n      }\n\n      const user = await this.userRepository\n        .createQueryBuilder()\n        .update(User)\n        .set({\n          firstName,\n          lastName,\n          countryName,\n          cityName,\n          educationHistory: educationHistory ?? [],\n          discord,\n          englishLevel: englishLevel || 'a0',\n          aboutMyself: aboutMyself || '',\n          contactsTelegram: telegram || '',\n          contactsPhone: phone || '',\n          contactsEmail: email || '',\n          contactsNotes: notes || '',\n          contactsSkype: skype || '',\n          contactsWhatsApp: whatsApp || '',\n          contactsLinkedIn: linkedIn || '',\n          contactsEpamEmail: epamEmail || '',\n        })\n        .returning('*')\n        .where('id = :id', { id: userId })\n        .execute();\n\n      await Promise.all([this.updateEmailChannel(userId, user), this.updateDiscordChannel(userId, user)]);\n    }\n  }\n\n  public async updateProfileFlat(userId: number, profileInfo: UpdateProfileInfoDto) {\n    const {\n      name,\n      countryName,\n      cityName,\n      educationHistory,\n      discord,\n      englishLevel,\n      aboutMyself,\n      contactsTelegram,\n      contactsPhone,\n      contactsEmail,\n      contactsNotes,\n      contactsSkype,\n      contactsWhatsApp,\n      contactsLinkedIn,\n      contactsEpamEmail,\n      languages,\n    } = profileInfo;\n\n    if (contactsEmail && !isEmail(contactsEmail)) {\n      throw new BadRequestException('Email is invalid.');\n    }\n    if (contactsEpamEmail && !isEmail(contactsEpamEmail)) {\n      throw new BadRequestException('Epam email is invalid.');\n    }\n\n    const [firstName, lastName] = name?.split(' ') ?? [];\n    const user = await this.userRepository\n      .createQueryBuilder()\n      .update(User)\n      .set(\n        omitBy<QueryDeepPartialEntity<User>>(\n          {\n            firstName,\n            lastName: firstName ? (lastName ?? '') : undefined,\n            countryName,\n            cityName,\n            educationHistory,\n            discord,\n            englishLevel,\n            aboutMyself,\n            contactsTelegram,\n            contactsPhone,\n            contactsEmail,\n            contactsNotes,\n            contactsSkype,\n            contactsWhatsApp,\n            contactsLinkedIn,\n            contactsEpamEmail,\n            languages,\n          },\n          isUndefined,\n        ),\n      )\n      .returning('*')\n      .where('id = :id', { id: userId })\n      .execute();\n\n    await Promise.all([this.updateEmailChannel(userId, user), this.updateDiscordChannel(userId, user)]);\n  }\n\n  public async getProfile(githubId: string) {\n    const user = await this.userRepository.findOneOrFail({ where: { githubId } });\n    const resume = await this.resumeRepository.findOne({\n      where: { userId: user.id, name: Not(IsNull()) },\n    });\n\n    return { resume: resume ?? null };\n  }\n\n  public async getPersonalProfile(githubId: string) {\n    return this.userRepository.findOneOrFail({ where: { githubId }, relations: ['students'] });\n  }\n\n  private async updateEmailChannel(userId: number, user: UpdateResult) {\n    const newEmail = user.raw[0]?.contactsEmail;\n    const channelId = 'email';\n\n    if (!newEmail) {\n      await this.notificationConnectionsRepository.delete({\n        channelId,\n        userId,\n      });\n    } else {\n      const connection = await this.notificationConnectionsRepository.findOne({\n        where: {\n          channelId,\n          userId,\n        },\n      });\n      const shouldSendEmailConfirmation = !connection || connection.externalId !== newEmail;\n      if (shouldSendEmailConfirmation) {\n        await this.userNotificationsService.sendEmailConfirmation(userId, false);\n      }\n\n      const isConfirmed = connection?.enabled && connection?.externalId === newEmail ? true : false;\n      await this.notificationConnectionsRepository.save({\n        channelId,\n        userId,\n        externalId: newEmail,\n        enabled: isConfirmed,\n      });\n    }\n  }\n\n  private async updateDiscordChannel(userId: number, user: UpdateResult) {\n    const newDiscord: Discord = user.raw[0]?.discord;\n    const channelId = 'discord';\n    if (!newDiscord) {\n      await this.notificationConnectionsRepository.delete({\n        channelId,\n        userId,\n      });\n    } else {\n      const connection = await this.notificationConnectionsRepository.findOne({\n        where: {\n          channelId,\n          userId,\n        },\n      });\n\n      if (!connection || connection.externalId !== `${newDiscord.id}`) {\n        await this.notificationConnectionsRepository.save({\n          channelId,\n          userId,\n          externalId: `${newDiscord.id}`,\n          enabled: true,\n        });\n      }\n    }\n  }\n\n  public async obfuscateProfile(githubId: string) {\n    const user = await this.userRepository.findOneOrFail({ where: { githubId }, relations: ['students'] });\n\n    await this.userRepository.update(user.id, {\n      obfuscated: true,\n      aboutMyself: null,\n      activist: false,\n      cityName: null,\n      contactsEmail: null,\n      contactsEpamEmail: null,\n      contactsLinkedIn: null,\n      contactsNotes: null,\n      contactsPhone: null,\n      contactsSkype: null,\n      contactsTelegram: null,\n      contactsWhatsApp: null,\n      countryName: null,\n      cvLink: null,\n      dateOfBirth: null,\n      discord: null,\n      educationHistory: [],\n      employmentHistory: [],\n      englishLevel: null,\n      epamApplicantId: null,\n      externalAccounts: [],\n      firstName: 'Removed',\n      firstNameNative: null,\n      githubId: `gdpr-${nanoid(10)}`.toLowerCase(),\n      languages: [],\n      lastName: 'Removed',\n      lastNameNative: null,\n      locationId: null,\n      locationName: null,\n      militaryService: null,\n      opportunitiesConsent: false,\n      primaryEmail: null,\n      providerUserId: null,\n      tshirtFashion: null,\n      tshirtSize: null,\n    });\n\n    await this.notificationConnectionsRepository.delete({ userId: user.id });\n    await this.resumeRepository.delete({ userId: user.id });\n\n    for (const student of user.students ?? []) {\n      await this.certificateRepository.delete({ studentId: student.id });\n    }\n  }\n}\n"
  },
  {
    "path": "nestjs/src/prompts/dto/create-prompt.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsString } from 'class-validator';\n\nexport class CreatePromptDto {\n  @IsString()\n  @ApiProperty()\n  type: string;\n\n  @IsString()\n  @ApiProperty()\n  text: string;\n\n  @IsNumber()\n  @ApiProperty()\n  temperature: number;\n}\n"
  },
  {
    "path": "nestjs/src/prompts/dto/index.ts",
    "content": "export * from './prompt.dto';\nexport * from './create-prompt.dto';\nexport * from './update-prompt.dto';\n"
  },
  {
    "path": "nestjs/src/prompts/dto/prompt.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { Prompt } from '@entities/prompt';\n\nexport class PromptDto {\n  constructor(prompt: Prompt) {\n    this.id = prompt.id;\n    this.type = prompt.type;\n    this.text = prompt.text;\n    this.temperature = prompt.temperature;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  type: string;\n\n  @ApiProperty()\n  text: string;\n\n  @ApiProperty()\n  temperature: number;\n}\n"
  },
  {
    "path": "nestjs/src/prompts/dto/update-prompt.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class UpdatePromptDto {\n  @IsOptional()\n  @IsNumber()\n  @ApiProperty()\n  temperature?: number;\n\n  @IsOptional()\n  @IsString()\n  @ApiProperty()\n  type?: string;\n\n  @IsOptional()\n  @IsString()\n  @ApiProperty()\n  text?: string;\n}\n"
  },
  {
    "path": "nestjs/src/prompts/prompts.controller.ts",
    "content": "import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { CreatePromptDto, PromptDto, UpdatePromptDto } from './dto';\nimport { PromptsService } from './prompts.service';\n\n@Controller('prompts')\n@ApiTags('prompts')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class PromptsController {\n  constructor(private readonly promptsService: PromptsService) {}\n\n  @Post('/')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'createPrompt' })\n  @ApiOkResponse({ type: PromptDto })\n  public async create(@Body() dto: CreatePromptDto) {\n    const result = await this.promptsService.create(dto);\n    return new PromptDto(result);\n  }\n\n  @Get('/')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'getPrompts' })\n  @ApiOkResponse({ type: [PromptDto] })\n  public async getAll(): Promise<PromptDto[]> {\n    const data = await this.promptsService.findAll();\n    return data.map(item => new PromptDto(item));\n  }\n\n  @Delete('/:id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'deletePrompt' })\n  @ApiOkResponse({})\n  public async remove(@Param('id', ParseIntPipe) id: number) {\n    return this.promptsService.remove(id);\n  }\n\n  @Patch('/:id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'updatePrompt' })\n  @ApiOkResponse({ type: PromptDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() alert: UpdatePromptDto) {\n    const result = await this.promptsService.update(id, alert);\n    return new PromptDto(result);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/prompts/prompts.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { PromptsController } from './prompts.controller';\nimport { PromptsService } from './prompts.service';\nimport { Prompt } from '@entities/prompt';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Prompt])],\n  controllers: [PromptsController],\n  providers: [PromptsService],\n})\nexport class PromptsModule {}\n"
  },
  {
    "path": "nestjs/src/prompts/prompts.service.ts",
    "content": "import { Prompt } from '@entities/prompt';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CreatePromptDto } from './dto/create-prompt.dto';\nimport { UpdatePromptDto } from './dto/update-prompt.dto';\n\n@Injectable()\nexport class PromptsService {\n  constructor(\n    @InjectRepository(Prompt)\n    private promptsRepository: Repository<Prompt>,\n  ) {}\n\n  public async create(dto: CreatePromptDto) {\n    const { text, type, temperature } = dto;\n    const { id } = await this.promptsRepository.save({ text, type, temperature });\n    return this.promptsRepository.findOneByOrFail({ id });\n  }\n\n  public async findAll(): Promise<Prompt[]> {\n    const items = await this.promptsRepository.find();\n    return items;\n  }\n\n  public async update(id: number, dto: UpdatePromptDto) {\n    await this.promptsRepository.update(id, dto);\n    return this.promptsRepository.findOneByOrFail({ id });\n  }\n\n  public async remove(id: number) {\n    await this.promptsRepository.delete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/registry/constants.ts",
    "content": "export const DEFAULT_PAGE_SIZE = 200;\nexport const DEFAULT_PAGE_NUMBER = 1;\n"
  },
  {
    "path": "nestjs/src/registry/dto/approve-mentor.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsArray } from 'class-validator';\n\nexport class ApproveMentorDto {\n  @ApiProperty()\n  @IsArray()\n  preselectedCourses!: string[];\n}\n"
  },
  {
    "path": "nestjs/src/registry/dto/comment-mentor-registry.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'class-validator';\n\nexport class CommentMentorRegistryDto {\n  @IsString()\n  @ApiProperty({ nullable: true, type: String })\n  public comment: string;\n}\n"
  },
  {
    "path": "nestjs/src/registry/dto/invite-mentors.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsBoolean, IsOptional, IsString } from 'class-validator';\n\nexport class InviteMentorsDto {\n  @ApiProperty()\n  @IsArray()\n  disciplines: string[];\n\n  @ApiProperty()\n  @IsBoolean()\n  @IsOptional()\n  isMentor?: boolean;\n\n  @ApiProperty()\n  @IsString()\n  text: string;\n}\n"
  },
  {
    "path": "nestjs/src/registry/dto/mentor-registry.dto.ts",
    "content": "import { MentorRegistry } from '@entities/mentorRegistry';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { PersonDto } from 'src/core/dto';\n\nexport class MentorRegistryDto {\n  constructor(mentorRegistry: MentorRegistry) {\n    this.id = mentorRegistry.id;\n    this.englishMentoring = mentorRegistry.englishMentoring;\n    this.githubId = mentorRegistry.user.githubId;\n    this.cityName = mentorRegistry.user.cityName;\n    this.preferedCourses = mentorRegistry.preferedCourses.map(id => Number(id));\n    this.preselectedCourses = mentorRegistry.preselectedCourses.map(id => Number(id));\n    this.maxStudentsLimit = mentorRegistry.maxStudentsLimit;\n    this.preferedStudentsLocation = mentorRegistry.preferedStudentsLocation;\n    this.name = PersonDto.getName({ firstName: mentorRegistry.user.firstName, lastName: mentorRegistry.user.lastName });\n    this.technicalMentoring = mentorRegistry.technicalMentoring;\n    this.courses = mentorRegistry.user.mentors?.map(m => m.courseId);\n    this.sendDate = mentorRegistry.sendDate ?? mentorRegistry.updatedDate;\n    this.hasCertificate = mentorRegistry.user.students?.some(s => s.certificate?.id);\n    this.primaryEmail = mentorRegistry.user.primaryEmail ?? null;\n    this.languagesMentoring = mentorRegistry.languagesMentoring;\n    this.contactsEpamEmail = mentorRegistry.user.contactsEpamEmail;\n    this.receivedDate = mentorRegistry.createdDate;\n    this.comment = mentorRegistry.comment;\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public githubId: string;\n\n  @ApiProperty({ type: String, nullable: true })\n  public cityName: string | null;\n\n  @ApiProperty({ type: [Number] })\n  preferedCourses: number[];\n\n  @ApiProperty({ type: [Number] })\n  preselectedCourses: number[];\n\n  @ApiProperty()\n  public maxStudentsLimit: number;\n\n  @ApiProperty()\n  public preferedStudentsLocation: string;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public technicalMentoring: string[];\n\n  @ApiProperty({ type: [Number] })\n  public courses?: number[];\n\n  @ApiProperty()\n  public sendDate: Date;\n\n  @ApiProperty()\n  public receivedDate: Date;\n\n  @ApiProperty()\n  public hasCertificate?: boolean;\n\n  @ApiProperty()\n  public englishMentoring: boolean;\n\n  @ApiProperty()\n  public primaryEmail: string | null;\n\n  @ApiProperty({ type: [String] })\n  public languagesMentoring: string[];\n\n  @ApiProperty({ type: String, nullable: true })\n  public contactsEpamEmail: string | null;\n\n  @ApiProperty({ type: String, nullable: true })\n  public comment: string | null;\n}\n\nexport class FilterMentorRegistryResponse {\n  constructor(mentors: MentorRegistryDto[], total: number) {\n    this.mentors = mentors;\n    this.total = total;\n  }\n  @ApiProperty({ type: [MentorRegistryDto] })\n  mentors: MentorRegistryDto[];\n\n  @ApiProperty()\n  total: number;\n}\n"
  },
  {
    "path": "nestjs/src/registry/registry.controller.ts",
    "content": "import { Body, Controller, Delete, Get, Param, Put, Req, UseGuards, Query, ParseArrayPipe, Post } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';\nimport { uniq } from 'lodash';\nimport { CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { UserNotificationsService } from 'src/users-notifications/users.notifications.service';\nimport { ApproveMentorDto } from './dto/approve-mentor.dto';\nimport { MentorRegistryDto } from './dto/mentor-registry.dto';\nimport { RegistryService } from './registry.service';\nimport { CoursesService } from 'src/courses/courses.service';\nimport { DisciplinesService } from 'src/disciplines/disciplines.service';\nimport { CommentMentorRegistryDto } from './dto/comment-mentor-registry.dto';\nimport { FilterMentorRegistryResponse } from './dto/mentor-registry.dto';\nimport { DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE } from './constants';\nimport { CourseInfo } from '@entities/session';\nimport { InviteMentorsDto } from './dto/invite-mentors.dto';\n\nexport enum MentorRegistryTabsMode {\n  New = 'new',\n  All = 'all',\n}\n\n@Controller('registry')\n@ApiTags('registry')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class RegistryController {\n  constructor(\n    private registryService: RegistryService,\n    private notificationService: UserNotificationsService,\n    private coursesService: CoursesService,\n    private disciplinesService: DisciplinesService,\n  ) {}\n\n  @Put('mentor/:githubId')\n  @ApiOperation({ operationId: 'approveMentor' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor])\n  @ApiOkResponse()\n  public async approveMentor(@Param('githubId') githubId: string, @Body() body: ApproveMentorDto) {\n    const { preselectedCourses } = body;\n\n    const [user, notificationData] = await Promise.all([\n      this.registryService.approveMentor(githubId, preselectedCourses),\n      this.registryService.buildMentorApprovalData(preselectedCourses),\n    ]);\n\n    await this.notificationService.sendEventNotification({\n      data: notificationData,\n      notificationId: 'mentorRegistrationApproval',\n      userId: user.id,\n    });\n  }\n\n  @Delete('mentor/:githubId')\n  @ApiOperation({ operationId: 'cancelMentorRegistry' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor])\n  @ApiOkResponse()\n  public async cancelMentorRegistry(@Param('githubId') githubId: string) {\n    await this.registryService.cancelMentorRegistry(githubId);\n  }\n\n  @Put('mentor/:githubId/comment')\n  @ApiOperation({ operationId: 'commentMentorRegistry' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor])\n  @ApiOkResponse()\n  public async commentMentorRegistry(@Param('githubId') githubId: string, @Body() body: CommentMentorRegistryDto) {\n    const { comment } = body;\n    await this.registryService.commentMentorRegistry(githubId, comment);\n  }\n\n  @Get('mentors')\n  @ApiOperation({ operationId: 'getMentorRegistries' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager, CourseRole.Supervisor])\n  @ApiOkResponse({ type: FilterMentorRegistryResponse })\n  @ApiQuery({ name: 'status', required: false, enum: MentorRegistryTabsMode })\n  @ApiQuery({ name: 'pageSize', required: false, type: 'number' })\n  @ApiQuery({ name: 'currentPage', required: false, type: 'number' })\n  @ApiQuery({ name: 'githubId', required: false, type: 'string' })\n  @ApiQuery({ name: 'cityName', required: false, type: 'string' })\n  @ApiQuery({ name: 'preferedCourses', required: false, type: 'number', isArray: true })\n  @ApiQuery({ name: 'preselectedCourses', required: false, type: 'number', isArray: true })\n  @ApiQuery({ name: 'technicalMentoring', required: false, type: 'string', isArray: true })\n  public async getMentorRegistries(\n    @Req() req: CurrentRequest,\n    @Query('status') status: MentorRegistryTabsMode = MentorRegistryTabsMode.All,\n    @Query('pageSize') pageSize?: number,\n    @Query('currentPage') currentPage?: number,\n    @Query('githubId') githubId?: string,\n    @Query('cityName') cityName?: string,\n    @Query('preferedCourses', new ParseArrayPipe({ items: Number, optional: true })) preferedCourses?: number[],\n    @Query('preselectedCourses', new ParseArrayPipe({ items: Number, optional: true })) preselectedCourses?: number[],\n    @Query('technicalMentoring', new ParseArrayPipe({ items: String, optional: true })) technicalMentoring?: string[],\n  ) {\n    if (req.user.isAdmin && !req.query) {\n      const data = await this.registryService.findAllMentorRegistries();\n      return {\n        total: data.length,\n        mentors: data.map(el => new MentorRegistryDto(el)),\n      };\n    }\n    const data = await this.registryService.filterMentorRegistries({\n      page: currentPage || DEFAULT_PAGE_NUMBER,\n      limit: pageSize || DEFAULT_PAGE_SIZE,\n      githubId,\n      cityName,\n      preferedCourses,\n      preselectedCourses,\n      technicalMentoring,\n      coursesIds: req.user.isAdmin\n        ? undefined\n        : Object.entries(req.user.courses)\n            .filter(\n              ([_, value]) => value.roles.includes(CourseRole.Manager) || value.roles.includes(CourseRole.Supervisor),\n            )\n            .map(([key]) => Number(key)),\n      disciplineNames: req.user.isAdmin ? undefined : await this.getDisciplineNamesByCourseIds(req.user.courses),\n      status,\n    });\n    return {\n      total: data.total,\n      mentors: data.mentors.map(el => new MentorRegistryDto(el)),\n    };\n  }\n\n  private async getDisciplineNamesByCourseIds(userCourses: Record<number, CourseInfo>): Promise<string[]> {\n    const coursesIds = Object.entries(userCourses)\n      .filter(([_, value]) => value.roles.includes(CourseRole.Manager) || value.roles.includes(CourseRole.Supervisor))\n      .map(([key]) => Number(key));\n    const courses = await this.coursesService.getByIds(coursesIds);\n    const disciplineIds = uniq(courses.map(course => course.disciplineId).filter(Boolean)) as number[];\n    const disciplines = await this.disciplinesService.getByIds(disciplineIds);\n    return disciplines.map(discipline => discipline.name);\n  }\n\n  @Post('mentors/invite')\n  @ApiOperation({ operationId: 'inviteMentors' })\n  @RequiredRoles([Role.Admin])\n  public async inviteMentors(@Body() body: InviteMentorsDto) {\n    await this.registryService.sendInvitationsToMentors(body);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/registry/registry.module.ts",
    "content": "import { MentorRegistry } from '@entities/mentorRegistry';\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { CoursesModule } from 'src/courses/courses.module';\nimport { UsersModule } from 'src/users';\nimport { UsersNotificationsModule } from 'src/users-notifications/users-notifications.module';\nimport { RegistryController } from './registry.controller';\nimport { RegistryService } from './registry.service';\nimport { DisciplinesModule } from 'src/disciplines';\nimport { NotificationsModule } from 'src/notifications/notifications.module';\nimport { Student } from '@entities/student';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([MentorRegistry]),\n    UsersModule,\n    UsersNotificationsModule,\n    CoursesModule,\n    DisciplinesModule,\n    NotificationsModule,\n    TypeOrmModule.forFeature([Student]),\n  ],\n  controllers: [RegistryController],\n  providers: [RegistryService],\n})\nexport class RegistryModule {}\n"
  },
  {
    "path": "nestjs/src/registry/registry.service.ts",
    "content": "import { User } from '@entities/user';\nimport { MentorRegistry } from '@entities/mentorRegistry';\nimport { BadRequestException, Injectable, Logger } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { CoursesService } from 'src/courses/courses.service';\nimport { UsersService } from 'src/users/users.service';\nimport { Brackets, Repository } from 'typeorm';\nimport { paginate } from 'src/core/paginate';\nimport { InviteMentorsDto } from './dto/invite-mentors.dto';\nimport { NotificationsService } from 'src/notifications/notifications.service';\nimport { Student } from '@entities/student';\nimport { MentorRegistryTabsMode } from './registry.controller';\n\n@Injectable()\nexport class RegistryService {\n  private readonly logger = new Logger('registry');\n\n  constructor(\n    @InjectRepository(MentorRegistry)\n    private mentorsRegistryRepository: Repository<MentorRegistry>,\n    @InjectRepository(Student)\n    readonly studentRepository: Repository<Student>,\n    private usersService: UsersService,\n    private coursesService: CoursesService,\n    private notificationsService: NotificationsService,\n  ) {}\n\n  public async approveMentor(githubId: string, preselectedCourses: string[]): Promise<User> {\n    const user = await this.usersService.getByGithubId(githubId);\n    if (!user) {\n      throw new BadRequestException('User not found');\n    }\n\n    await this.mentorsRegistryRepository.update({ userId: user.id }, { preselectedCourses, sendDate: new Date() });\n    return user;\n  }\n\n  public async cancelMentorRegistry(githubId: string) {\n    const user = await this.usersService.getByGithubId(githubId);\n    if (user == null) {\n      throw new BadRequestException('User not found');\n    }\n    await this.mentorsRegistryRepository.update({ userId: user.id }, { canceled: true });\n  }\n\n  private getPreparedMentorRegistriesQuery() {\n    return this.mentorsRegistryRepository\n      .createQueryBuilder('mentorRegistry')\n      .innerJoin('mentorRegistry.user', 'user')\n      .addSelect([\n        'user.id',\n        'user.firstName',\n        'user.lastName',\n        'user.githubId',\n        'user.primaryEmail',\n        'user.cityName',\n        'user.contactsEpamEmail',\n      ])\n      .leftJoin('user.mentors', 'mentor')\n      .leftJoin('user.students', 'student')\n      .leftJoin('student.certificate', 'certificate')\n      .addSelect(['mentor.id', 'mentor.courseId', 'student.id', 'certificate.id'])\n      .orderBy('mentorRegistry.updatedDate', 'DESC');\n  }\n\n  public async findAllMentorRegistries() {\n    const mentorRegistries = await this.getPreparedMentorRegistriesQuery()\n      .andWhere('mentorRegistry.canceled = false')\n      .getMany();\n\n    return mentorRegistries;\n  }\n\n  public async buildMentorApprovalData(preselectedCourses: string[]) {\n    const courses = await this.coursesService.getByIds(preselectedCourses.map(id => parseInt(id)));\n\n    return {\n      courses,\n    };\n  }\n\n  public async commentMentorRegistry(githubId: string, comment: string | null) {\n    const user = await this.usersService.getByGithubId(githubId);\n    if (user == null) {\n      throw new BadRequestException('User not found');\n    }\n    await this.mentorsRegistryRepository.update({ userId: user.id }, { comment: comment ?? undefined });\n  }\n\n  public async filterMentorRegistries({\n    githubId,\n    page,\n    limit,\n    cityName,\n    preselectedCourses,\n    preferedCourses,\n    technicalMentoring,\n    coursesIds,\n    disciplineNames,\n    status,\n  }: {\n    githubId?: string;\n    cityName?: string;\n    page: number;\n    limit: number;\n    preselectedCourses?: number[];\n    preferedCourses?: number[];\n    technicalMentoring?: string[];\n    coursesIds?: number[];\n    disciplineNames?: string[];\n    status: MentorRegistryTabsMode;\n  }) {\n    const req = this.getPreparedMentorRegistriesQuery().andWhere('mentorRegistry.canceled = false');\n\n    if (githubId) {\n      req.andWhere(`\"user\".\"githubId\" ILIKE :githubId`, { githubId: `%${githubId}%` });\n    }\n    if (cityName) {\n      req.andWhere(`\"user\".\"cityName\" ILIKE :cityName`, { cityName: `%${cityName}%` });\n    }\n    if (preselectedCourses?.length) {\n      req.andWhere(\n        `EXISTS (\n        SELECT\n        FROM unnest(string_to_array(mentorRegistry.preselectedCourses, ',')) course\n        WHERE course = ANY(:preselectedCourses)\n      )`,\n        { preselectedCourses },\n      );\n    }\n    if (preferedCourses?.length) {\n      req.andWhere(\n        `EXISTS (\n        SELECT\n        FROM unnest(string_to_array(mentorRegistry.preferedCourses, ',')) course\n        WHERE course = ANY(:preferedCourses)\n      )`,\n        { preferedCourses },\n      );\n    }\n    if (technicalMentoring?.length) {\n      req.andWhere(\n        `EXISTS (\n        SELECT\n        FROM unnest(string_to_array(mentorRegistry.technicalMentoring, ',')) course\n        WHERE course = ANY(:technicalMentoring)\n      )`,\n        { technicalMentoring },\n      );\n    }\n\n    if (coursesIds?.length || disciplineNames?.length) {\n      req.andWhere(\n        new Brackets(qb => {\n          if (coursesIds?.length) {\n            qb.where(`string_to_array(mentorRegistry.preferedCourses, ',') && :coursesIds`, { coursesIds });\n          }\n          if (disciplineNames?.length) {\n            qb.orWhere(\n              `mentorRegistry.preferedCourses = ''\n              OR string_to_array(mentorRegistry.technicalMentoring, ',') && :disciplineNames`,\n              {\n                disciplineNames,\n              },\n            );\n          }\n        }),\n      );\n    }\n    if (status === MentorRegistryTabsMode.New) {\n      req.andWhere(\n        new Brackets(qb => {\n          qb.where(`mentorRegistry.preselectedCourses = ''`).orWhere(\n            `(SELECT COUNT(*) FROM mentor WHERE mentor.userId = mentorRegistry.userId AND mentor.courseId = ANY(string_to_array(mentorRegistry.preselectedCourses, ',')::int[]))\n              < cardinality(string_to_array(mentorRegistry.preselectedCourses, ','))`,\n          );\n        }),\n      );\n    }\n\n    const response = await paginate(req, {\n      page,\n      limit,\n    });\n\n    return {\n      total: response.meta.total,\n      mentors: response.items,\n    };\n  }\n\n  public async sendInvitationsToMentors(data: InviteMentorsDto) {\n    const { text, disciplines, isMentor } = data;\n\n    const query = await this.studentRepository\n      .createQueryBuilder('student')\n      .innerJoin('student.course', 'course')\n      .innerJoin('course.discipline', 'discipline')\n      .innerJoin(\n        'notification_user_connection',\n        'notification',\n        'notification.userId = student.userId and notification.channelId = :channelId and notification.enabled = :enabled',\n        {\n          channelId: 'email',\n          enabled: true,\n        },\n      )\n      .innerJoin('student.certificate', 'certificate')\n      .where('discipline.id IN (:...ids)', { ids: disciplines })\n      .select(['student.userId', 'notification.externalId'])\n      .distinct(true);\n\n    if (isMentor) {\n      query.innerJoin('mentor', 'mentor', 'mentor.userId = student.userId');\n    }\n\n    const users = await query.getRawMany();\n\n    Promise.resolve().then(\n      () =>\n        // eslint-disable-next-line no-async-promise-executor\n        new Promise(async () => {\n          this.logger.log({ message: 'processing invitations...' });\n\n          const batchSize = 10;\n\n          for (let i = 0; i < users.length; i += batchSize) {\n            const batch = users.slice(i, i + batchSize);\n\n            const promises = batch.map(async user => {\n              const userId = user.student_userId;\n              const email = user.notification_externalId;\n\n              try {\n                await this.notificationsService.sendMessage({\n                  notificationId: 'mentorsInvitation',\n                  userId,\n                  data: {\n                    text,\n                  },\n                  channelId: 'email',\n                  channelValue: email,\n                  noEscape: true,\n                });\n              } catch (e) {\n                this.logger.log({ message: (e as Error).message, userId });\n              }\n            });\n\n            await Promise.all(promises);\n          }\n        }),\n    );\n  }\n}\n"
  },
  {
    "path": "nestjs/src/repositories/dto/create-repository-event.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class CreateRepositoryEventDto {\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  action: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  githubId: string;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  repositoryUrl: string;\n}\n"
  },
  {
    "path": "nestjs/src/repositories/dto/index.ts",
    "content": "export * from './create-repository-event.dto';\n"
  },
  {
    "path": "nestjs/src/repositories/index.ts",
    "content": "export * from './repositories.module';\n"
  },
  {
    "path": "nestjs/src/repositories/repositories.controller.ts",
    "content": "import { Body, Controller, HttpCode, ParseArrayPipe, Post, UseGuards } from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { AuthGuard } from '@nestjs/passport';\nimport { CreateRepositoryEventDto } from './dto';\nimport { RepositoriesService } from './repositories.service';\n\n@Controller('repositories')\n@ApiTags('repositories')\n@UseGuards(AuthGuard('basic'))\nexport class RepositoriesController {\n  constructor(private readonly repositoriesService: RepositoriesService) {}\n\n  @Post('/event')\n  @HttpCode(200)\n  @ApiOperation({ operationId: 'createRepositoryEvent' })\n  @ApiBody({ type: [CreateRepositoryEventDto] })\n  @ApiOkResponse()\n  public async createRepositoryEvent(\n    @Body(new ParseArrayPipe({ items: CreateRepositoryEventDto })) dto: CreateRepositoryEventDto[],\n  ): Promise<void> {\n    await this.repositoriesService.createEvents(dto);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/repositories/repositories.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { RepositoryEvent } from '@entities/repositoryEvent';\nimport { Student } from '@entities/student';\nimport { User } from '@entities/user';\nimport { RepositoriesController } from './repositories.controller';\nimport { RepositoriesService } from './repositories.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([RepositoryEvent, Student, User])],\n  controllers: [RepositoriesController],\n  providers: [RepositoriesService],\n})\nexport class RepositoriesModule {}\n"
  },
  {
    "path": "nestjs/src/repositories/repositories.service.ts",
    "content": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { In, Repository } from 'typeorm';\nimport { RepositoryEvent } from '@entities/repositoryEvent';\nimport { Student } from '@entities/student';\nimport { User } from '@entities/user';\nimport { CreateRepositoryEventDto } from './dto';\n\n@Injectable()\nexport class RepositoriesService {\n  constructor(\n    @InjectRepository(RepositoryEvent)\n    private readonly repositoryEventRepository: Repository<RepositoryEvent>,\n    @InjectRepository(Student)\n    private readonly studentRepository: Repository<Student>,\n    @InjectRepository(User)\n    private readonly userRepository: Repository<User>,\n  ) {}\n\n  public async createEvents(events: CreateRepositoryEventDto[]): Promise<void> {\n    if (events.length === 0) {\n      return;\n    }\n\n    const uniqueGithubIds = [...new Set(events.map(e => e.githubId))];\n    const users = await this.userRepository.find({\n      select: ['id', 'githubId'],\n      where: { githubId: In(uniqueGithubIds) },\n    });\n    const githubIdToUserId = new Map(users.map(u => [u.githubId, u.id]));\n\n    const eventsToSave = events.map(event => ({ ...event, userId: githubIdToUserId.get(event.githubId) }));\n    await this.repositoryEventRepository.save(eventsToSave);\n\n    const uniqueUrls = [...new Set(events.map(e => e.repositoryUrl))];\n    await this.studentRepository.update({ repository: In(uniqueUrls) }, { repositoryLastActivityDate: new Date() });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/reset.d.ts",
    "content": "import '@total-typescript/ts-reset';\n"
  },
  {
    "path": "nestjs/src/schedule/dto/check-schedule-changes.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsOptional } from 'class-validator';\n\nexport class CheckScheduleChangesDto {\n  @ApiProperty()\n  @IsNumber()\n  @IsOptional()\n  public lastHours?: number;\n}\n"
  },
  {
    "path": "nestjs/src/schedule/schedule.controller.ts",
    "content": "import { Body, Controller, Logger, Post, UseGuards } from '@nestjs/common';\nimport { ApiForbiddenResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { UserNotificationsService } from 'src/users-notifications/users.notifications.service';\nimport { CheckScheduleChangesDto } from './dto/check-schedule-changes';\nimport { ScheduleService } from './schedule.service';\n\n@Controller('schedule')\n@ApiTags('schedule')\n@UseGuards(DefaultGuard, RoleGuard)\n@RequiredRoles([Role.Admin])\nexport class ScheduleController {\n  private readonly logger = new Logger('schedule');\n\n  constructor(\n    private scheduleService: ScheduleService,\n    private notificationService: UserNotificationsService,\n  ) {}\n\n  @Post('/notify/changes')\n  @ApiOperation({ operationId: 'notifyScheduleChanges' })\n  @ApiForbiddenResponse()\n  public async notifyScheduleChanges(@Body() dto: CheckScheduleChangesDto) {\n    const recipients = await this.scheduleService.getChangedCoursesRecipients(dto.lastHours);\n    Promise.resolve().then(\n      () =>\n        // eslint-disable-next-line no-async-promise-executor\n        new Promise(async () => {\n          this.logger.log({ message: 'processing recipients notifications...' });\n\n          for (const [userId, courses] of recipients) {\n            try {\n              await this.notificationService.sendEventNotification({\n                data: { courses },\n                notificationId: 'courseScheduleChange',\n                userId,\n              });\n            } catch (e) {\n              this.logger.log({ message: (e as Error).message, userId });\n            }\n          }\n        }),\n    );\n  }\n}\n"
  },
  {
    "path": "nestjs/src/schedule/schedule.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { ScheduleService } from './schedule.service';\nimport { ScheduleController } from './schedule.controller';\nimport { CoursesModule } from 'src/courses/courses.module';\nimport { UsersNotificationsModule } from 'src/users-notifications/users-notifications.module';\nimport { User } from '@entities/user';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { History } from '@entities/history';\nimport { CourseEvent } from '@entities/courseEvent';\n\n@Module({\n  imports: [UsersNotificationsModule, CoursesModule, TypeOrmModule.forFeature([User, History, CourseEvent])],\n  controllers: [ScheduleController],\n  providers: [ScheduleService],\n})\nexport class ScheduleModule {}\n"
  },
  {
    "path": "nestjs/src/schedule/schedule.service.ts",
    "content": "import { Injectable, Logger } from '@nestjs/common';\nimport { CoursesService } from 'src/courses/courses.service';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { User } from '@entities/user';\nimport { In, LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm';\nimport { isEqual, subHours } from 'date-fns';\nimport { Course } from '@entities/course';\nimport { Student } from '@entities/student';\nimport { Mentor } from '@entities/mentor';\nimport { CourseUser } from '@entities/courseUser';\nimport { History } from '@entities/history';\nimport { CourseEvent } from '@entities/courseEvent';\nimport { CourseTask } from '@entities/courseTask';\n\n@Injectable()\nexport class ScheduleService {\n  private readonly logger = new Logger('schedule');\n  constructor(\n    private courseService: CoursesService,\n    @InjectRepository(User)\n    readonly userRepository: Repository<User>,\n    @InjectRepository(History)\n    private readonly historyRepository: Repository<History>,\n    @InjectRepository(CourseEvent)\n    private readonly courseRepository: Repository<CourseEvent>,\n  ) {}\n\n  public async getChangedCoursesRecipients(lastHours: number = 2): Promise<Recipients> {\n    const courseChangesMap = await this.getCourseChangesMap(lastHours);\n\n    const updatedCourses = await this.courseService.getByIds([...courseChangesMap.keys()], {\n      startDate: LessThanOrEqual(new Date()),\n      endDate: MoreThanOrEqual(new Date()),\n    });\n\n    const aliasMap = new Map(updatedCourses.map(course => [course.alias, course]));\n\n    this.logger.log({ message: `updated courses: ${updatedCourses.map(course => course.name)} ` });\n    if (!updatedCourses.length) return [];\n\n    const users = await this.getUsersCourses(updatedCourses.map(c => c.id));\n\n    return users\n      .filter(user => user.aliases.length > 0)\n      .map(user => [\n        user.id,\n        user.aliases.map((courseAlias: string) => {\n          const course = aliasMap.get(courseAlias) as Course;\n          const changes = courseChangesMap.get(course.id) as Map<string, ChangeEvent>;\n\n          return {\n            course,\n            changes: [...changes.values()],\n          };\n        }),\n      ]);\n  }\n\n  private async getUsersCourses(courseIds: number[]) {\n    const users: {\n      id: number;\n      aliases: string[];\n    }[] = await this.userRepository\n      .createQueryBuilder('user')\n      .select('user.id', 'id')\n      .addSelect(`array_remove(array_agg(DISTINCT course.\"alias\"), NULL)`, 'aliases')\n      .leftJoin(\n        Student,\n        'student',\n        'user.id = student.userId and student.isExpelled = false and student.courseId In (:...courseIds)',\n        {\n          courseIds,\n        },\n      )\n      .leftJoin(\n        Mentor,\n        'mentor',\n        'user.id = mentor.userId and mentor.isExpelled = false and mentor.courseId In (:...courseIds)',\n        {\n          courseIds,\n        },\n      )\n      .leftJoin(\n        CourseUser,\n        'courseUser',\n        'user.id = courseUser.userId and courseUser.courseId In (:...courseIds) and courseUser.isManager = true',\n        {\n          courseIds,\n        },\n      )\n      .innerJoin(\n        Course,\n        'course',\n        '(mentor.courseId = course.id or student.courseId = course.id or courseUser.courseId = course.id)',\n      )\n\n      .groupBy('user.id')\n      .getRawMany();\n\n    return users;\n  }\n\n  private async getCourseChangesMap(lastHours: number) {\n    const records = await this.getScheduleUpdatedRecords(lastHours);\n\n    const { courseMap, fetchEvents, fetchTasks } = this.buildChangesMaps(records);\n    const taskEventMap = await this.buildTaskEventMap(fetchEvents, fetchTasks);\n\n    courseMap.forEach(recordsMap => {\n      recordsMap.forEach((value, key) => {\n        const [type, _, id] = key.split('-');\n        recordsMap.set(key, {\n          ...value,\n          name: taskEventMap.get(`${type}-${id}`)?.name ?? '',\n        });\n      });\n    });\n\n    return courseMap;\n  }\n\n  private async buildTaskEventMap(fetchEvents: Set<number>, fetchTasks: Set<number>) {\n    const courseEntries: {\n      name: string;\n      id: number;\n      type: 'task' | 'event';\n    }[] = await this.courseRepository.query(\n      `select \"name\", \"id\", 'event' as \"type\"  from \"event\" where \"id\" = ANY($1)\n        union\n       select \"name\", \"id\", 'task' as \"type\"  from \"task\" where \"id\" = ANY($2)`,\n      [fetchEvents.size > 0 ? [...fetchEvents] : null, [...fetchTasks]],\n    );\n    const entryMap = new Map(courseEntries.map(entry => [`${entry.type}-${entry.id}`, entry]));\n\n    return entryMap;\n  }\n\n  private buildChangesMaps(records: History[]) {\n    const courseMap = new Map<number, Map<string, ChangeEvent>>();\n    const fetchTasks = new Set<number>();\n    const fetchEvents = new Set<number>();\n\n    for (const record of records) {\n      const { operation, entityId, previous, update } = record;\n      const event = record.event as ScheduleEvent;\n      const entryUpdate = update as CourseTask | CourseEvent;\n      const entryPrevious = previous as CourseTask | CourseEvent;\n\n      const courseId = entryUpdate.courseId || entryPrevious.courseId;\n      let recordsMap = courseMap.get(courseId);\n\n      if (!recordsMap) {\n        recordsMap = new Map<string, ChangeEvent>();\n        courseMap.set(courseId, recordsMap);\n      }\n\n      const type = event === 'course_task' ? 'task' : 'event';\n      const parentId = this.getParentId(entryUpdate, entryPrevious);\n      const entryKey = `${type}-${entityId}-${parentId}`;\n      const prevEntry = recordsMap.get(entryKey);\n      if (type === 'event') {\n        fetchEvents.add(parentId);\n      } else {\n        fetchTasks.add(parentId);\n      }\n\n      if (operation === 'insert') {\n        recordsMap.set(entryKey, {\n          isNew: true,\n          type,\n          ...this.getInsertFields(event, entryUpdate),\n        });\n      } else if (operation === 'remove' || (entryUpdate as CourseTask).disabled === true) {\n        recordsMap.set(entryKey, {\n          isRemoved: true,\n          type,\n          ...this.getInsertFields(event, entryPrevious),\n        });\n      } else if (operation === 'update') {\n        recordsMap.set(entryKey, {\n          ...prevEntry,\n          type,\n          ...this.getUpdatedFields(event, entryUpdate, entryPrevious),\n        });\n      }\n    }\n\n    return {\n      courseMap,\n      fetchTasks,\n      fetchEvents,\n    };\n  }\n\n  private getParentId(updatedEntry: CourseTask | CourseEvent, previousEntry: CourseTask | CourseEvent) {\n    if (previousEntry) {\n      return (previousEntry as CourseEvent).eventId || (previousEntry as CourseTask).taskId;\n    }\n\n    return (updatedEntry as CourseEvent).eventId || (updatedEntry as CourseTask).taskId;\n  }\n\n  private async getScheduleUpdatedRecords(lastHours: number) {\n    const date = subHours(new Date(), lastHours);\n    const records = await this.historyRepository\n      .createQueryBuilder('entry')\n      .where({\n        event: In(['course_task', 'course_event']),\n        updatedDate: MoreThanOrEqual(date.toISOString()),\n      })\n      .orderBy('entry.\"updatedDate\"', 'DESC')\n      .getMany();\n\n    return records;\n  }\n\n  getInsertFields(tableName: ScheduleEvent, entryUpdate: CourseTask | CourseEvent) {\n    if (tableName === 'course_event') {\n      const event = entryUpdate as CourseEvent;\n      const fields: Partial<CourseEvent> = {\n        dateTime: event.dateTime,\n      };\n      if (event.place) {\n        fields.place = event.place;\n      }\n\n      return fields;\n    }\n\n    const task = entryUpdate as CourseTask;\n    const fields: Partial<CourseTask> = {\n      studentStartDate: task.studentStartDate,\n      studentEndDate: task.studentEndDate,\n    };\n    if (task.crossCheckEndDate) {\n      fields.crossCheckEndDate = task.crossCheckEndDate;\n    }\n\n    return fields;\n  }\n\n  getUpdatedFields(\n    tableName: ScheduleEvent,\n    entryUpdate: CourseTask | CourseEvent,\n    previous: CourseTask | CourseEvent,\n  ) {\n    if (tableName === 'course_event') {\n      const fields: Partial<\n        CourseEvent & {\n          placeOld: string;\n          dateTimeOld: string;\n        }\n      > = {};\n\n      const event = entryUpdate as CourseEvent;\n      const previousEvent = previous as CourseEvent;\n\n      if (event.place !== previousEvent.place) {\n        fields.place = event.place;\n        fields.placeOld = previousEvent.place;\n      }\n      if (!this.isDateEqual(event.dateTime, previousEvent.dateTime)) {\n        fields.dateTime = event.dateTime;\n        fields.dateTimeOld = previousEvent.dateTime as string;\n      }\n\n      return Object.keys(fields).length > 0 ? fields : undefined;\n    }\n\n    const task = entryUpdate as CourseTask;\n    const previousTask = previous as CourseTask;\n    const fields: Partial<\n      CourseTask & {\n        studentStartDateOld: string | Date;\n        studentEndDateOld: string | Date;\n        crossCheckEndDateOld: string | Date;\n      }\n    > = {};\n\n    if (!this.isDateEqual(task.studentStartDate, previousTask.studentStartDate)) {\n      fields.studentStartDate = task.studentStartDate;\n      fields.studentStartDateOld = previousTask.studentStartDate ?? undefined;\n    }\n    if (!this.isDateEqual(task.studentEndDate, previousTask.studentEndDate)) {\n      fields.studentEndDate = task.studentEndDate;\n      fields.studentEndDateOld = previousTask.studentEndDate ?? undefined;\n    }\n\n    if (!this.isDateEqual(task.crossCheckEndDate, previousTask.crossCheckEndDate)) {\n      fields.crossCheckEndDate = task.crossCheckEndDate;\n      fields.crossCheckEndDateOld = previousTask.crossCheckEndDate ?? undefined;\n    }\n\n    return fields;\n  }\n\n  private isDateEqual(date1: string | Date | null, date2: string | Date | null) {\n    if (!date1 && !date2) return true;\n    if (!date1 || !date2) return false;\n    return isEqual(new Date(date1 as string | Date), new Date(date2 as string | Date));\n  }\n}\n\ntype UserCourses = [number, { course: Course; changes: ChangeEvent[] }[]];\ntype Recipients = UserCourses[];\n\ntype ChangeEvent = {\n  isNew?: boolean;\n  isRemoved?: boolean;\n  type: string;\n  name?: string;\n} & Partial<CourseEvent | CourseTask>;\n\ntype ScheduleEvent = 'course_task' | 'course_event';\n"
  },
  {
    "path": "nestjs/src/session/dto/auth-user.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { AuthUser } from 'src/auth';\nimport { CourseRole } from '@entities/session';\n\nexport class AuthUserDto {\n  constructor(readonly authUser: AuthUser) {\n    this.id = authUser.id;\n    this.githubId = authUser.githubId;\n    this.roles = authUser.roles;\n    this.isAdmin = authUser.isAdmin;\n    this.isHirer = authUser.isHirer;\n    this.appRoles = authUser.appRoles;\n    this.courses = authUser.courses;\n  }\n\n  @ApiProperty()\n  id: number;\n\n  @ApiProperty()\n  githubId: string;\n\n  @ApiProperty({\n    type: 'object',\n    additionalProperties: { type: 'string', enum: ['mentor', 'student'] },\n  })\n  roles: Record<string, 'mentor' | 'student'>;\n\n  @ApiProperty()\n  isAdmin: boolean;\n\n  @ApiProperty()\n  isHirer: boolean;\n\n  @ApiProperty()\n  appRoles: string[];\n\n  @ApiProperty({\n    type: 'object',\n    additionalProperties: {\n      type: 'object',\n      properties: {\n        roles: {\n          type: 'array',\n          items: {\n            type: 'string',\n            enum: [\n              CourseRole.Manager,\n              CourseRole.Supervisor,\n              CourseRole.Student,\n              CourseRole.Mentor,\n              CourseRole.Dementor,\n              CourseRole.Activist,\n            ],\n          },\n        },\n      },\n      required: ['roles'],\n    },\n  })\n  courses: Record<string, { roles: string[] }>;\n}\n"
  },
  {
    "path": "nestjs/src/session/session.controller.ts",
    "content": "import { Controller, Get, Req, UseGuards } from '@nestjs/common';\nimport { CurrentRequest, DefaultGuard } from 'src/auth';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { AuthUserDto } from './dto/auth-user.dto';\n\n@Controller('session')\n@ApiTags('session')\n@UseGuards(DefaultGuard)\nexport class SessionController {\n  // constructor(private readonly authService: AuthService) {}\n\n  @Get()\n  @ApiOperation({ operationId: 'getSession' })\n  @ApiOkResponse({ type: AuthUserDto })\n  getSession(@Req() req: CurrentRequest) {\n    return req.user;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/session/session.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { SessionController } from './session.controller';\nimport { AuthModule } from 'src/auth/auth.module';\n\n@Module({\n  imports: [AuthModule],\n  controllers: [SessionController],\n})\nexport class SessionModule {}\n"
  },
  {
    "path": "nestjs/src/setup.ts",
    "content": "import { BadRequestException, INestApplication, ValidationError, ValidationPipe } from '@nestjs/common';\nimport * as Sentry from '@sentry/node';\nimport cookieParser from 'cookie-parser';\nimport { Logger } from 'nestjs-pino';\nimport { EntityNotFoundFilter, SentryFilter } from './core/filters';\nimport { ValidationFilter } from './core/validation';\nimport { HttpAdapterHost } from '@nestjs/core';\n\nexport function setupApp(app: INestApplication) {\n  const logger = app.get(Logger);\n  app.enableCors();\n  app.useLogger(logger);\n  app.use(cookieParser());\n\n  if (process.env.SENTRY_DSN) {\n    const ignoredExceptions = ['UnauthorizedException', 'TokenExpiredError', 'NotFoundException'];\n\n    Sentry.init({\n      dsn: process.env.SENTRY_DSN,\n      enableTracing: false,\n      defaultIntegrations: false,\n      debug: false,\n      beforeSend(event) {\n        const [value] = event.exception?.values ?? [];\n        if (value?.type && ignoredExceptions.includes(value.type)) {\n          return null;\n        }\n        return event;\n      },\n    });\n  }\n\n  const httpAdapterHost = app.get(HttpAdapterHost);\n  app.useGlobalFilters(new SentryFilter(httpAdapterHost.httpAdapter), new EntityNotFoundFilter());\n  app.useGlobalPipes(\n    new ValidationPipe({\n      whitelist: true,\n      skipMissingProperties: false,\n      forbidUnknownValues: true,\n      forbidNonWhitelisted: true,\n      exceptionFactory: (errors: ValidationError[]) => {\n        const message = errors.map(error => Object.values(error?.constraints ?? {}).join('\\n')).join('\\n');\n        logger.warn('Validation Pipe Error', errors);\n        return new BadRequestException(message);\n      },\n    }),\n  );\n  app.useGlobalFilters(new ValidationFilter());\n}\n"
  },
  {
    "path": "nestjs/src/spec.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"paths\": {\n    \"/activity\": {\n      \"get\": {\n        \"operationId\": \"getActivity\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ActivityDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"activity\"]\n      },\n      \"post\": {\n        \"operationId\": \"createActivity\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateActivityDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ActivityDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"activity\"]\n      }\n    },\n    \"/activity/webhook\": {\n      \"post\": {\n        \"operationId\": \"createActivityWebhook\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateActivityWebhookDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ActivityDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"activity\"]\n      }\n    },\n    \"/users/search\": {\n      \"get\": {\n        \"operationId\": \"searchUsers\",\n        \"parameters\": [{ \"name\": \"query\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/UserSearchDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"users\"]\n      }\n    },\n    \"/alerts\": {\n      \"post\": {\n        \"operationId\": \"createAlert\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateAlertDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/AlertDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"alerts\"]\n      },\n      \"get\": {\n        \"operationId\": \"getAlerts\",\n        \"parameters\": [{ \"name\": \"enabled\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"boolean\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/AlertDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"alerts\"]\n      }\n    },\n    \"/alerts/{id}\": {\n      \"delete\": {\n        \"operationId\": \"deleteAlert\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"alerts\"]\n      },\n      \"patch\": {\n        \"operationId\": \"updateAlert\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateAlertDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/AlertDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"alerts\"]\n      }\n    },\n    \"/students/{studentId}/feedbacks\": {\n      \"post\": {\n        \"operationId\": \"createStudentFeedback\",\n        \"parameters\": [{ \"name\": \"studentId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateStudentFeedbackDto\" } } }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/StudentFeedbackDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students feedbacks\"]\n      }\n    },\n    \"/students/{studentId}/feedbacks/{id}\": {\n      \"patch\": {\n        \"operationId\": \"updateStudentFeedback\",\n        \"parameters\": [\n          { \"name\": \"studentId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateStudentFeedbackDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/StudentFeedbackDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students feedbacks\"]\n      },\n      \"get\": {\n        \"operationId\": \"getStudentFeedback\",\n        \"parameters\": [\n          { \"name\": \"studentId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/StudentFeedbackDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students feedbacks\"]\n      }\n    },\n    \"/courses\": {\n      \"get\": {\n        \"operationId\": \"getCourses\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses\"]\n      },\n      \"post\": {\n        \"operationId\": \"createCourse\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateCourseDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses\"]\n      }\n    },\n    \"/courses/{courseId}\": {\n      \"get\": {\n        \"operationId\": \"getCourse\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseDto\" } } }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses\"]\n      },\n      \"put\": {\n        \"operationId\": \"updateCourse\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateCourseDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseDto\" } } }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses\"]\n      }\n    },\n    \"/courses/{courseId}/leave\": {\n      \"post\": {\n        \"operationId\": \"leaveCourse\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": false,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/LeaveCourseRequestDto\" } } }\n        },\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses\"]\n      }\n    },\n    \"/courses/{courseId}/rejoin\": {\n      \"post\": {\n        \"operationId\": \"rejoinCourse\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses\"]\n      }\n    },\n    \"/courses/{courseId}/copy\": {\n      \"post\": {\n        \"operationId\": \"copyCourse\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateCourseDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses\"]\n      }\n    },\n    \"/students\": {\n      \"get\": {\n        \"operationId\": \"getUserStudents\",\n        \"parameters\": [\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"student\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"country\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"city\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"ongoingCourses\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"previousCourses\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UserStudentsDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students\"]\n      }\n    },\n    \"/students/{studentId}\": {\n      \"get\": {\n        \"operationId\": \"getStudent\",\n        \"parameters\": [{ \"name\": \"studentId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/StudentDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students\"]\n      }\n    },\n    \"/mentors/{mentorId}/course/{courseId}/options\": {\n      \"get\": {\n        \"operationId\": \"getMentorOptions\",\n        \"parameters\": [\n          { \"name\": \"mentorId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/MentorOptionsDto\" } } }\n          },\n          \"403\": { \"description\": \"\" },\n          \"404\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"mentors\"]\n      }\n    },\n    \"/mentors/{mentorId}/students\": {\n      \"get\": {\n        \"operationId\": \"getMentorStudents\",\n        \"parameters\": [{ \"name\": \"mentorId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/MentorStudentDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"mentors\"]\n      }\n    },\n    \"/mentors/{mentorId}/course/{courseId}/students\": {\n      \"get\": {\n        \"operationId\": \"getCourseStudentsCount\",\n        \"parameters\": [\n          { \"name\": \"mentorId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": { \"description\": \"\", \"content\": { \"application/json\": { \"schema\": { \"type\": \"number\" } } } },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"mentors\"]\n      }\n    },\n    \"/mentors/{mentorId}/course/{courseId}/dashboard\": {\n      \"get\": {\n        \"operationId\": \"getMentorDashboardData\",\n        \"parameters\": [\n          { \"name\": \"mentorId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/MentorDashboardDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"mentors\"]\n      }\n    },\n    \"/mentors/{mentorId}/course/{courseId}/random-task\": {\n      \"get\": {\n        \"operationId\": \"getRandomTask\",\n        \"parameters\": [\n          { \"name\": \"mentorId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" }, \"400\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"mentors\"]\n      }\n    },\n    \"/courses/{courseId}/tasks\": {\n      \"get\": {\n        \"operationId\": \"getCourseTasks\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          {\n            \"name\": \"status\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"enum\": [\"started\", \"inprogress\", \"finished\"], \"type\": \"string\" }\n          },\n          {\n            \"name\": \"checker\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"enum\": [\"auto-test\", \"assigned\", \"mentor\", \"taskOwner\", \"crossCheck\"], \"type\": \"string\" }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseTaskDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      },\n      \"post\": {\n        \"operationId\": \"createCourseTask\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateCourseTaskDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseTaskDetailedDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/{courseId}/tasks/solutions\": {\n      \"get\": {\n        \"operationId\": \"getCourseTasksWithStudentSolution\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          {\n            \"name\": \"status\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"enum\": [\"started\", \"inprogress\", \"finished\"], \"type\": \"string\" }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseTaskDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/{courseId}/tasks/detailed\": {\n      \"get\": {\n        \"operationId\": \"getCourseTasksDetailed\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseTaskDetailedDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/{courseId}/tasks/{courseTaskId}\": {\n      \"get\": {\n        \"operationId\": \"getCourseTask\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseTaskDetailedDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      },\n      \"put\": {\n        \"operationId\": \"updateCourseTask\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateCourseTaskDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseTaskDetailedDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      },\n      \"delete\": {\n        \"operationId\": \"deleteCourseTask\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" }, \"400\": { \"description\": \"\" }, \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/{courseId}/events\": {\n      \"post\": {\n        \"operationId\": \"createCourseEvent\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateCourseEventDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseEventDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses events\"]\n      }\n    },\n    \"/courses/{courseId}/events/{courseEventId}\": {\n      \"put\": {\n        \"operationId\": \"updateCourseEvent\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseEventId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateCourseEventDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" }, \"400\": { \"description\": \"\" }, \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses events\"]\n      },\n      \"delete\": {\n        \"operationId\": \"deleteCourseEvent\",\n        \"parameters\": [\n          { \"name\": \"courseEventId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": {} }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" }, \"400\": { \"description\": \"\" }, \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses events\"]\n      }\n    },\n    \"/courses/{courseId}/interviews\": {\n      \"get\": {\n        \"operationId\": \"getInterviews\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"disabled\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"boolean\" } },\n          {\n            \"name\": \"types\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/InterviewDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/courses/{courseId}/interviews/comments\": {\n      \"get\": {\n        \"operationId\": \"getStageInterviewsCommentToStudent\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/InterviewCommentDto\" } }\n              }\n            }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/courses/{courseId}/interviews/{interviewId}\": {\n      \"get\": {\n        \"operationId\": \"getInterview\",\n        \"parameters\": [\n          { \"name\": \"interviewId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/InterviewDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/courses/{courseId}/interviews/{interviewId}/pairs\": {\n      \"get\": {\n        \"operationId\": \"getInterviewPairs\",\n        \"parameters\": [\n          { \"name\": \"interviewId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/InterviewPairDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/courses/{courseId}/interviews/{interviewId}/register\": {\n      \"post\": {\n        \"operationId\": \"registerToInterview\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"interviewId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" }, \"400\": { \"description\": \"\" }, \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/courses/{courseId}/interviews/{courseTaskId}/auto-distribute\": {\n      \"post\": {\n        \"operationId\": \"distributeInterviewPairs\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/InterviewDistributeDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": { \"$ref\": \"#/components/schemas/InterviewDistributeResponseDto\" }\n                }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" },\n          \"409\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/courses/{courseId}/interviews/{interviewId}/students/available\": {\n      \"get\": {\n        \"operationId\": \"getAvailableStudents\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"interviewId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/AvailableStudentDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/courses/{courseId}/interviews/{interviewId}/{type}/feedback\": {\n      \"get\": {\n        \"operationId\": \"getInterviewFeedback\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"interviewId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"type\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/InterviewFeedbackDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      },\n      \"post\": {\n        \"operationId\": \"createInterviewFeedback\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"interviewId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"type\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/PutInterviewFeedbackDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" }, \"400\": { \"description\": \"\" }, \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses interviews\"]\n      }\n    },\n    \"/tasks/notify/changes\": {\n      \"post\": {\n        \"operationId\": \"notifyTasksDeadlines\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CheckTasksDeadlineDto\" } } }\n        },\n        \"responses\": { \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/stats/expelled\": {\n      \"get\": {\n        \"operationId\": \"getExpelledStats\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ExpelledStatsDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/stats/expelled\": {\n      \"get\": {\n        \"operationId\": \"getCourseExpelledStats\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ExpelledStatsDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/stats/expelled/{id}\": {\n      \"delete\": {\n        \"operationId\": \"deleteExpelledStat\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"200\": { \"description\": \"\", \"content\": { \"application/json\": { \"schema\": { \"type\": \"string\" } } } }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/aggregate/stats\": {\n      \"get\": {\n        \"operationId\": \"getCoursesStats\",\n        \"parameters\": [\n          {\n            \"name\": \"ids\",\n            \"required\": true,\n            \"in\": \"query\",\n            \"description\": \"List of course IDs\",\n            \"schema\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n          },\n          {\n            \"name\": \"year\",\n            \"required\": true,\n            \"in\": \"query\",\n            \"description\": \"Year for which stats are fetched\",\n            \"schema\": { \"type\": \"number\" }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseAggregateStatsDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/stats\": {\n      \"get\": {\n        \"operationId\": \"getCourseStats\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseStatsDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/stats/mentors\": {\n      \"get\": {\n        \"operationId\": \"getCourseMentors\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseMentorsStatsDto\" } } }\n          },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/stats/mentors/countries\": {\n      \"get\": {\n        \"operationId\": \"getCourseMentorCountries\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CountriesStatsDto\" } } }\n          },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/stats/students/countries\": {\n      \"get\": {\n        \"operationId\": \"getCourseStudentCountries\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CountriesStatsDto\" } } }\n          },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/stats/students/certificates/countries\": {\n      \"get\": {\n        \"operationId\": \"getCourseStudentCertificatesCountries\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CountriesStatsDto\" } } }\n          },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/stats/task/{taskId}/performance\": {\n      \"get\": {\n        \"operationId\": \"getTaskPerformance\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"taskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskPerformanceStatsDto\" } } }\n          },\n          \"400\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course stats\"]\n      }\n    },\n    \"/courses/{courseId}/cross-checks/pairs\": {\n      \"get\": {\n        \"operationId\": \"getCrossCheckPairs\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"orderBy\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"orderDirection\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"checker\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"student\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"url\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"task\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"403\": { \"description\": \"\" },\n          \"default\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CrossCheckPairResponseDto\" } }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/{courseId}/cross-checks/available-review-stats\": {\n      \"get\": {\n        \"operationId\": \"getAvailableCrossCheckReviewStats\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"403\": { \"description\": \"\" },\n          \"default\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/AvailableReviewStatsDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/{courseId}/cross-checks/{courseTaskId}/csv\": {\n      \"get\": {\n        \"operationId\": \"getCrossCheckCsv\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/courses/{courseId}/cross-checks/{courseTaskId}/feedbacks/my\": {\n      \"get\": {\n        \"operationId\": \"getMyCrossCheckFeedbacks\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"403\": { \"description\": \"\" },\n          \"default\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CrossCheckFeedbackDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses tasks\"]\n      }\n    },\n    \"/course/{courseId}/students/score\": {\n      \"get\": {\n        \"operationId\": \"getScore\",\n        \"parameters\": [\n          { \"name\": \"activeOnly\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          {\n            \"name\": \"orderBy\",\n            \"required\": true,\n            \"in\": \"query\",\n            \"schema\": {\n              \"enum\": [\n                \"rank\",\n                \"totalScore\",\n                \"crossCheckScore\",\n                \"githubId\",\n                \"name\",\n                \"cityName\",\n                \"mentor\",\n                \"totalScoreChangeDate\",\n                \"repositoryLastActivityDate\"\n              ],\n              \"type\": \"string\"\n            }\n          },\n          {\n            \"name\": \"orderDirection\",\n            \"required\": true,\n            \"in\": \"query\",\n            \"schema\": { \"enum\": [\"asc\", \"desc\"], \"type\": \"string\" }\n          },\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"githubId\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"name\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"mentor.githubId\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"cityName\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ScoreDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students score\"]\n      }\n    },\n    \"/course/{courseId}/students/score/{githubId}\": {\n      \"get\": {\n        \"operationId\": \"getStudentScore\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ScoreStudentDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students score\"]\n      }\n    },\n    \"/courses/{courseId}/tasks/{courseTaskId}/solutions\": {\n      \"post\": {\n        \"operationId\": \"createTaskSolution\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/SaveTaskSolutionDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskSolutionDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses task solutions\"]\n      }\n    },\n    \"/courses/{courseId}/schedule\": {\n      \"get\": {\n        \"operationId\": \"getSchedule\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseScheduleItemDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses schedule\"]\n      }\n    },\n    \"/courses/{courseId}/schedule/copy\": {\n      \"post\": {\n        \"operationId\": \"copySchedule\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseCopyFromDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"courses schedule\"]\n      }\n    },\n    \"/courses/{courseId}/icalendar/token\": {\n      \"get\": {\n        \"operationId\": \"getScheduleICalendarToken\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseScheduleTokenDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses schedule ical\"]\n      }\n    },\n    \"/courses/{courseId}/icalendar/{token}\": {\n      \"get\": {\n        \"operationId\": \"getScheduleICalendar\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"token\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"timezone\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": { \"description\": \"\", \"content\": { \"application/json\": { \"schema\": { \"type\": \"string\" } } } }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"courses schedule ical\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution\": {\n      \"post\": {\n        \"operationId\": \"createTeamDistribution\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateTeamDistributionDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamDistributionDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      },\n      \"get\": {\n        \"operationId\": \"getCourseTeamDistributions\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TeamDistributionDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{id}\": {\n      \"delete\": {\n        \"operationId\": \"deleteTeamDistribution\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      },\n      \"put\": {\n        \"operationId\": \"updateTeamDistribution\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateTeamDistributionDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{id}/registry\": {\n      \"post\": {\n        \"operationId\": \"teamDistributionRegistry\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamDistributionDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      },\n      \"delete\": {\n        \"operationId\": \"teamDistributionDeleteRegistry\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{id}/submit-score/{taskId}\": {\n      \"get\": {\n        \"operationId\": \"submitScore\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"taskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamDistributionDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{id}/students/{studentId}\": {\n      \"delete\": {\n        \"operationId\": \"TeamDistributionController_deleteStudentFromDistribution\",\n        \"parameters\": [\n          { \"name\": \"studentId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{id}/detailed\": {\n      \"get\": {\n        \"operationId\": \"getCourseTeamDistributionDetailed\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamDistributionDetailedDto\" } }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{id}/students\": {\n      \"get\": {\n        \"operationId\": \"getStudentsWithoutTeam\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"search\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TeamDistributionStudentDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{id}/distribution\": {\n      \"post\": {\n        \"operationId\": \"distributeStudentsToTeam\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"team distribution\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{distributionId}/team\": {\n      \"get\": {\n        \"operationId\": \"getTeams\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"distributionId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"search\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamsDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team\"]\n      },\n      \"post\": {\n        \"operationId\": \"createTeam\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"distributionId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateTeamDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{distributionId}/team/{id}\": {\n      \"patch\": {\n        \"operationId\": \"updateTeam\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"distributionId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateTeamDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"team\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{distributionId}/team/{id}/password\": {\n      \"get\": {\n        \"operationId\": \"getTeamPassword\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"distributionId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamPasswordDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team\"]\n      },\n      \"post\": {\n        \"operationId\": \"changeTeamPassword\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"distributionId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamPasswordDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{distributionId}/team/{id}/join\": {\n      \"post\": {\n        \"operationId\": \"joinTeam\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"distributionId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/JoinTeamDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TeamInfoDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"team\"]\n      }\n    },\n    \"/courses/{courseId}/team-distribution/{distributionId}/team/{id}/leave\": {\n      \"post\": {\n        \"operationId\": \"leaveTeam\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"distributionId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"team\"]\n      }\n    },\n    \"/courses/{courseId}/tasks/{courseTaskId}/verifications/answers\": {\n      \"get\": {\n        \"operationId\": \"getAnswers\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TaskVerificationAttemptDto\" } }\n              }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course task verifications\"]\n      }\n    },\n    \"/courses/{courseId}/tasks/{courseTaskId}/verifications\": {\n      \"post\": {\n        \"operationId\": \"createTaskVerification\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseTaskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/Object\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateTaskVerificationDto\" } }\n            }\n          },\n          \"400\": { \"description\": \"\" },\n          \"429\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course task verifications\"]\n      }\n    },\n    \"/courses/{courseId}/users\": {\n      \"get\": {\n        \"operationId\": \"getCourseUsers\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseUserDto\" } }\n              }\n            }\n          },\n          \"404\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course users\"]\n      },\n      \"put\": {\n        \"operationId\": \"putCourseUsers\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/UpdateCourseUserDto\" } }\n            }\n          }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"course users\"]\n      }\n    },\n    \"/courses/{courseId}/users/{githubId}\": {\n      \"put\": {\n        \"operationId\": \"putCourseUser\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CourseRolesDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" }, \"400\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"course users\"]\n      }\n    },\n    \"/course/{courseId}/mentors/details\": {\n      \"get\": {\n        \"operationId\": \"getMentorsDetails\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/MentorDetailsDto\" } }\n              }\n            }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course mentors\"]\n      }\n    },\n    \"/course/{courseId}/mentors/details/csv\": {\n      \"get\": {\n        \"operationId\": \"getMentorsDetailsCsv\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"course mentors\"]\n      }\n    },\n    \"/course/{courseId}/mentors/search/{searchText}\": {\n      \"get\": {\n        \"operationId\": \"searchMentors\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"searchText\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/SearchMentorDto\" } }\n              }\n            }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"course mentors\"]\n      }\n    },\n    \"/courses/{courseId}/students/{githubId}/summary\": {\n      \"get\": {\n        \"operationId\": \"getStudentSummary\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/StudentSummaryDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"students\"]\n      }\n    },\n    \"/courses/{courseId}/students/expel\": {\n      \"post\": {\n        \"operationId\": \"expelStudents\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ExpelStatusDto\" } } }\n        },\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"students\"]\n      }\n    },\n    \"/course/{courseId}/mentor-reviews\": {\n      \"get\": {\n        \"operationId\": \"getMentorReviews\",\n        \"parameters\": [\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"tasks\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"student\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"checker\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"sortField\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"sortOrder\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/MentorReviewsDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"mentor-reviews\"]\n      },\n      \"post\": {\n        \"operationId\": \"assignReviewer\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/MentorReviewAssignDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"mentor-reviews\"]\n      }\n    },\n    \"/users/notifications\": {\n      \"get\": {\n        \"operationId\": \"getUserNotifications\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UserNotificationsDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"users notifications\"]\n      },\n      \"put\": {\n        \"operationId\": \"updateUserNotifications\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"array\",\n                \"items\": { \"$ref\": \"#/components/schemas/UpdateNotificationUserSettingsDto\" }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": { \"$ref\": \"#/components/schemas/UpdateNotificationUserSettingsDto\" }\n                }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"users notifications\"]\n      }\n    },\n    \"/users/notifications/connections\": {\n      \"get\": {\n        \"operationId\": \"getUserNotificationConnections\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/NotificationUserConnectionsDto\" } }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"users notifications\"]\n      }\n    },\n    \"/users/notifications/confirmation/email\": {\n      \"post\": {\n        \"operationId\": \"sendEmailConfirmationLink\",\n        \"parameters\": [],\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"users notifications\"]\n      }\n    },\n    \"/users/notifications/connection/find\": {\n      \"post\": {\n        \"operationId\": \"UsersNotificationsController_findConnection\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/NotificationConnectionExistsDto\" } }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/NotificationConnectionDto\" } }\n            }\n          }\n        },\n        \"tags\": [\"users notifications\"]\n      }\n    },\n    \"/users/notifications/connection\": {\n      \"post\": {\n        \"operationId\": \"UsersNotificationsController_createUserConnection\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpsertNotificationConnectionDto\" } }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/NotificationConnectionDto\" } }\n            }\n          }\n        },\n        \"tags\": [\"users notifications\"]\n      }\n    },\n    \"/users/notifications/send\": {\n      \"post\": {\n        \"operationId\": \"sendNotification\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/SendUserNotificationDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" }, \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"users notifications\"]\n      }\n    },\n    \"/notifications\": {\n      \"get\": {\n        \"operationId\": \"getNotifications\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/NotificationDto\" } }\n              }\n            }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"notifications\"]\n      },\n      \"put\": {\n        \"operationId\": \"updateNotification\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateNotificationDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/NotificationDto\" } } }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"notifications\"]\n      },\n      \"post\": {\n        \"operationId\": \"createNotification\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateNotificationDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/NotificationDto\" } } }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"notifications\"]\n      }\n    },\n    \"/notifications/{id}\": {\n      \"delete\": {\n        \"operationId\": \"deleteNotification\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" }, \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"notifications\"]\n      }\n    },\n    \"/auth/github/login\": {\n      \"get\": {\n        \"operationId\": \"githubLogin\",\n        \"parameters\": [],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"auth\"]\n      }\n    },\n    \"/auth/github/callback\": {\n      \"get\": {\n        \"operationId\": \"githubCallback\",\n        \"parameters\": [],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"auth\"]\n      }\n    },\n    \"/auth/github/logout\": {\n      \"get\": {\n        \"operationId\": \"githubLogout\",\n        \"parameters\": [],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"auth\"]\n      }\n    },\n    \"/auth/github/connect\": {\n      \"post\": {\n        \"operationId\": \"AuthController_createConnectLinkViaGithub\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/AuthConnectionDto\" } } }\n        },\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"tags\": [\"auth\"]\n      }\n    },\n    \"/auth/cache/{userId}\": {\n      \"delete\": {\n        \"operationId\": \"clearAuthUserSessionCache\",\n        \"parameters\": [{ \"name\": \"userId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"auth\"]\n      }\n    },\n    \"/profile/{username}/courses\": {\n      \"get\": {\n        \"operationId\": \"getUserCourses\",\n        \"parameters\": [{ \"name\": \"username\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ProfileCourseDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      }\n    },\n    \"/profile/user\": {\n      \"post\": {\n        \"operationId\": \"updateUser\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateUserDto\" } } }\n        },\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      }\n    },\n    \"/profile/info\": {\n      \"patch\": {\n        \"operationId\": \"updateProfileInfoFlat\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateProfileInfoDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      }\n    },\n    \"/profile/{username}\": {\n      \"get\": {\n        \"operationId\": \"getProfile\",\n        \"parameters\": [{ \"name\": \"username\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ProfileDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      },\n      \"delete\": {\n        \"operationId\": \"obfuscateProfile\",\n        \"parameters\": [{ \"name\": \"username\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      }\n    },\n    \"/profile/{username}/personal\": {\n      \"get\": {\n        \"operationId\": \"getPersonalProfile\",\n        \"parameters\": [{ \"name\": \"username\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/PersonalProfileDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      }\n    },\n    \"/profile/{username}/endorsement\": {\n      \"get\": {\n        \"operationId\": \"getEndorsement\",\n        \"parameters\": [{ \"name\": \"username\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/EndorsementDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      }\n    },\n    \"/profile/{username}/endorsement-data\": {\n      \"get\": {\n        \"operationId\": \"getEndorsementData\",\n        \"parameters\": [{ \"name\": \"username\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/EndorsementDataDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"profile\"]\n      }\n    },\n    \"/disciplines\": {\n      \"post\": {\n        \"operationId\": \"createDiscipline\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateDisciplineDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/DisciplineDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"disciplines\"]\n      },\n      \"get\": {\n        \"operationId\": \"getDisciplines\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DisciplineDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"disciplines\"]\n      }\n    },\n    \"/disciplines/ids\": {\n      \"post\": {\n        \"operationId\": \"getDisciplinesByIds\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/DisciplineIdsDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DisciplineDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"disciplines\"]\n      }\n    },\n    \"/disciplines/{id}\": {\n      \"delete\": {\n        \"operationId\": \"deleteDiscipline\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"disciplines\"]\n      },\n      \"patch\": {\n        \"operationId\": \"updateDiscipline\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateDisciplineDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/DisciplineDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"disciplines\"]\n      }\n    },\n    \"/registry/mentor/{githubId}\": {\n      \"put\": {\n        \"operationId\": \"approveMentor\",\n        \"parameters\": [{ \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ApproveMentorDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"registry\"]\n      },\n      \"delete\": {\n        \"operationId\": \"cancelMentorRegistry\",\n        \"parameters\": [{ \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"registry\"]\n      }\n    },\n    \"/registry/mentor/{githubId}/comment\": {\n      \"put\": {\n        \"operationId\": \"commentMentorRegistry\",\n        \"parameters\": [{ \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CommentMentorRegistryDto\" } } }\n        },\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"registry\"]\n      }\n    },\n    \"/registry/mentors\": {\n      \"get\": {\n        \"operationId\": \"getMentorRegistries\",\n        \"parameters\": [\n          {\n            \"name\": \"status\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"enum\": [\"new\", \"all\"], \"type\": \"string\" }\n          },\n          { \"name\": \"pageSize\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"currentPage\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"githubId\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          { \"name\": \"cityName\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          {\n            \"name\": \"preferedCourses\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n          },\n          {\n            \"name\": \"preselectedCourses\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n          },\n          {\n            \"name\": \"technicalMentoring\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/FilterMentorRegistryResponse\" } }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"registry\"]\n      }\n    },\n    \"/registry/mentors/invite\": {\n      \"post\": {\n        \"operationId\": \"inviteMentors\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/InviteMentorsDto\" } } }\n        },\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"registry\"]\n      }\n    },\n    \"/certificate/{publicId}\": {\n      \"get\": {\n        \"operationId\": \"getCertificate\",\n        \"parameters\": [{ \"name\": \"publicId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"certificate\"]\n      }\n    },\n    \"/certificate\": {\n      \"post\": {\n        \"operationId\": \"saveCertificate\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/SaveCertificateDto\" } } }\n        },\n        \"responses\": { \"201\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"certificate\"]\n      }\n    },\n    \"/certificate/{studentId}\": {\n      \"delete\": {\n        \"operationId\": \"removeCertificate\",\n        \"parameters\": [{ \"name\": \"studentId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"certificate\"]\n      }\n    },\n    \"/discord-servers\": {\n      \"post\": {\n        \"operationId\": \"createDiscordServer\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateDiscordServerDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/DiscordServerDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"discord-servers\"]\n      },\n      \"get\": {\n        \"operationId\": \"getDiscordServers\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DiscordServerDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"discord-servers\"]\n      }\n    },\n    \"/discord-servers/reduced\": {\n      \"get\": {\n        \"operationId\": \"getReducedDiscordServers\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/IdNameDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"discord-servers\"]\n      }\n    },\n    \"/discord-servers/{courseId}/invite/{id}\": {\n      \"get\": {\n        \"operationId\": \"getInviteLinkByDiscordServerId\",\n        \"parameters\": [\n          { \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }\n        ],\n        \"responses\": {\n          \"200\": { \"description\": \"\", \"content\": { \"application/json\": { \"schema\": { \"type\": \"string\" } } } }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"discord-servers\"]\n      }\n    },\n    \"/discord-servers/{id}\": {\n      \"put\": {\n        \"operationId\": \"updateDiscordServer\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateDiscordServerDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/DiscordServerDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"discord-servers\"]\n      },\n      \"delete\": {\n        \"operationId\": \"deleteDiscordServer\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/DiscordServerDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"discord-servers\"]\n      }\n    },\n    \"/opportunities/{githubId}/resume\": {\n      \"get\": {\n        \"operationId\": \"getResume\",\n        \"parameters\": [{ \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ResumeDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" },\n          \"404\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      },\n      \"patch\": {\n        \"operationId\": \"saveResume\",\n        \"parameters\": [{ \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/FormDataDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/Resume\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" },\n          \"404\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      }\n    },\n    \"/opportunities/consent\": {\n      \"get\": {\n        \"operationId\": \"getConsent\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ConsentDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      },\n      \"post\": {\n        \"operationId\": \"createConsent\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/GiveConsentDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      },\n      \"delete\": {\n        \"operationId\": \"deleteConsent\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ConsentDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      }\n    },\n    \"/opportunities/prolong\": {\n      \"post\": {\n        \"operationId\": \"prolong\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/StatusDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      }\n    },\n    \"/opportunities/visibility\": {\n      \"post\": {\n        \"operationId\": \"setVisibility\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/VisibilityDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      }\n    },\n    \"/opportunities/public/{uuid}\": {\n      \"get\": {\n        \"operationId\": \"getPublicResume\",\n        \"parameters\": [{ \"name\": \"uuid\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ResumeDto\" } } }\n          },\n          \"400\": { \"description\": \"\" },\n          \"403\": { \"description\": \"\" },\n          \"404\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      }\n    },\n    \"/opportunities/applicants\": {\n      \"get\": {\n        \"operationId\": \"getApplicants\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ApplicantResumeDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"opportunities\"]\n      }\n    },\n    \"/user-group\": {\n      \"post\": {\n        \"operationId\": \"createUserGroup\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateUserGroupDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UserGroupDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"user-group\"]\n      },\n      \"get\": {\n        \"operationId\": \"getUserGroups\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/UserGroupDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"user-group\"]\n      }\n    },\n    \"/user-group/{id}\": {\n      \"put\": {\n        \"operationId\": \"updateUserGroup\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateUserGroupDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UserGroupDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"user-group\"]\n      },\n      \"delete\": {\n        \"operationId\": \"deleteUserGroup\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UserGroupDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"user-group\"]\n      }\n    },\n    \"/schedule/notify/changes\": {\n      \"post\": {\n        \"operationId\": \"notifyScheduleChanges\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CheckScheduleChangesDto\" } } }\n        },\n        \"responses\": { \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"schedule\"]\n      }\n    },\n    \"/gratitudes\": {\n      \"post\": {\n        \"operationId\": \"createGratitude\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateGratitudeDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/GratitudeDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"gratitudes\"]\n      }\n    },\n    \"/gratitudes/badges/{courseId}\": {\n      \"get\": {\n        \"operationId\": \"getBadges\",\n        \"parameters\": [{ \"name\": \"courseId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/BadgeDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"gratitudes\"]\n      }\n    },\n    \"/gratitudes/heroes/radar\": {\n      \"get\": {\n        \"operationId\": \"getHeroesRadar\",\n        \"parameters\": [\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"notActivist\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"boolean\" } },\n          { \"name\": \"countryName\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          {\n            \"name\": \"startDate\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"format\": \"date-time\", \"type\": \"string\" }\n          },\n          { \"name\": \"endDate\", \"required\": false, \"in\": \"query\", \"schema\": { \"format\": \"date-time\", \"type\": \"string\" } }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/HeroesRadarDto\" } } }\n          },\n          \"403\": { \"description\": \"\" }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"gratitudes\"]\n      }\n    },\n    \"/gratitudes/heroes/radar/csv\": {\n      \"get\": {\n        \"operationId\": \"getHeroesRadarCsv\",\n        \"parameters\": [\n          { \"name\": \"current\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"pageSize\", \"required\": true, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"courseId\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"number\" } },\n          { \"name\": \"notActivist\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"boolean\" } },\n          { \"name\": \"countryName\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"string\" } },\n          {\n            \"name\": \"startDate\",\n            \"required\": false,\n            \"in\": \"query\",\n            \"schema\": { \"format\": \"date-time\", \"type\": \"string\" }\n          },\n          { \"name\": \"endDate\", \"required\": false, \"in\": \"query\", \"schema\": { \"format\": \"date-time\", \"type\": \"string\" } }\n        ],\n        \"responses\": { \"403\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"gratitudes\"]\n      }\n    },\n    \"/gratitudes/heroes/countries\": {\n      \"get\": {\n        \"operationId\": \"getHeroesCountries\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CountryDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"gratitudes\"]\n      }\n    },\n    \"/events\": {\n      \"get\": {\n        \"operationId\": \"getEvents\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/EventDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"events\"]\n      },\n      \"post\": {\n        \"operationId\": \"createEvent\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateEventDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/EventDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"events\"]\n      }\n    },\n    \"/events/{id}\": {\n      \"patch\": {\n        \"operationId\": \"updateEvent\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateEventDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/EventDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"events\"]\n      },\n      \"delete\": {\n        \"operationId\": \"deleteEvent\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"events\"]\n      }\n    },\n    \"/tasks\": {\n      \"post\": {\n        \"operationId\": \"createTask\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateTaskDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"tasks\"]\n      },\n      \"get\": {\n        \"operationId\": \"getTasks\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": { \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TaskDto\" } } }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"tasks\"]\n      }\n    },\n    \"/tasks/{id}\": {\n      \"delete\": {\n        \"operationId\": \"deleteTask\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"tasks\"]\n      },\n      \"patch\": {\n        \"operationId\": \"updateTask\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateTaskDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"tasks\"]\n      }\n    },\n    \"/tasks/{taskId}/criteria\": {\n      \"get\": {\n        \"operationId\": \"getTaskCriteria\",\n        \"parameters\": [{ \"name\": \"taskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskCriteriaDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"tasks-criteria\"]\n      },\n      \"post\": {\n        \"operationId\": \"createTaskCriteria\",\n        \"parameters\": [{ \"name\": \"taskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskCriteriaDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskCriteriaDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"tasks-criteria\"]\n      },\n      \"patch\": {\n        \"operationId\": \"updateTaskCriteria\",\n        \"parameters\": [{ \"name\": \"taskId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskCriteriaDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/TaskCriteriaDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"tasks-criteria\"]\n      }\n    },\n    \"/prompts\": {\n      \"post\": {\n        \"operationId\": \"createPrompt\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreatePromptDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/PromptDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"prompts\"]\n      },\n      \"get\": {\n        \"operationId\": \"getPrompts\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/PromptDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"prompts\"]\n      }\n    },\n    \"/prompts/{id}\": {\n      \"delete\": {\n        \"operationId\": \"deletePrompt\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"prompts\"]\n      },\n      \"patch\": {\n        \"operationId\": \"updatePrompt\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdatePromptDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/PromptDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"prompts\"]\n      }\n    },\n    \"/auto-test\": {\n      \"get\": {\n        \"operationId\": \"getBasicAutoTests\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/BasicAutoTestTaskDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"auto-tests\"]\n      }\n    },\n    \"/auto-test/{id}\": {\n      \"get\": {\n        \"operationId\": \"getAutoTest\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/AutoTestTaskDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"auto-tests\"]\n      }\n    },\n    \"/contributors\": {\n      \"post\": {\n        \"operationId\": \"createContributor\",\n        \"parameters\": [],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/CreateContributorDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ContributorDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"contributors\"]\n      },\n      \"get\": {\n        \"operationId\": \"getContributors\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ContributorDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"contributors\"]\n      }\n    },\n    \"/contributors/{id}\": {\n      \"get\": {\n        \"operationId\": \"getContributor\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ContributorDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"contributors\"]\n      },\n      \"delete\": {\n        \"operationId\": \"deleteContributor\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"contributors\"]\n      },\n      \"patch\": {\n        \"operationId\": \"updateContributor\",\n        \"parameters\": [{ \"name\": \"id\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"number\" } }],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/UpdateContributorDto\" } } }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/ContributorDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"contributors\"]\n      }\n    },\n    \"/mentors-hall-of-fame\": {\n      \"get\": {\n        \"operationId\": \"getTopMentors\",\n        \"parameters\": [{ \"name\": \"allTime\", \"required\": false, \"in\": \"query\", \"schema\": { \"type\": \"boolean\" } }],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TopMentorDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"mentors-hall-of-fame\"]\n      }\n    },\n    \"/session\": {\n      \"get\": {\n        \"operationId\": \"getSession\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": { \"application/json\": { \"schema\": { \"$ref\": \"#/components/schemas/AuthUserDto\" } } }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"session\"]\n      }\n    },\n    \"/devtools/users\": {\n      \"get\": {\n        \"operationId\": \"getDevUsers\",\n        \"parameters\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/DevtoolsUserDto\" } }\n              }\n            }\n          }\n        },\n        \"summary\": \"\",\n        \"tags\": [\"devtools\"]\n      }\n    },\n    \"/devtools/user/{githubId}/login\": {\n      \"get\": {\n        \"operationId\": \"getDevUserLogin\",\n        \"parameters\": [{ \"name\": \"githubId\", \"required\": true, \"in\": \"path\", \"schema\": { \"type\": \"string\" } }],\n        \"responses\": { \"200\": { \"description\": \"\" } },\n        \"summary\": \"\",\n        \"tags\": [\"devtools\"]\n      }\n    }\n  },\n  \"info\": { \"title\": \"\", \"description\": \"\", \"version\": \"1.0.0\", \"contact\": {} },\n  \"tags\": [],\n  \"servers\": [{ \"url\": \"/api/v2\" }],\n  \"components\": {\n    \"schemas\": {\n      \"ActivityDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"lastActivityTime\": { \"type\": \"number\" }, \"isActive\": { \"type\": \"boolean\" } },\n        \"required\": [\"lastActivityTime\", \"isActive\"]\n      },\n      \"CreateActivityDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"isActive\": { \"type\": \"boolean\" } },\n        \"required\": [\"isActive\"]\n      },\n      \"SenderLoginDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"githubId\": { \"type\": \"string\" } },\n        \"required\": [\"githubId\"]\n      },\n      \"SenderDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"login\": { \"$ref\": \"#/components/schemas/SenderLoginDto\" } },\n        \"required\": [\"login\"]\n      },\n      \"CreateActivityWebhookDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"sender\": { \"$ref\": \"#/components/schemas/SenderDto\" } },\n        \"required\": [\"sender\"]\n      },\n      \"CourseRecord\": {\n        \"type\": \"object\",\n        \"properties\": { \"courseName\": { \"type\": \"string\" }, \"id\": { \"type\": \"number\" } },\n        \"required\": [\"courseName\", \"id\"]\n      },\n      \"UserSearchDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"countryName\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsEpamEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"primaryEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsDiscord\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsTelegram\": { \"type\": \"string\", \"nullable\": true },\n          \"mentors\": { \"nullable\": true, \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseRecord\" } },\n          \"students\": { \"nullable\": true, \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseRecord\" } }\n        },\n        \"required\": [\n          \"id\",\n          \"githubId\",\n          \"name\",\n          \"cityName\",\n          \"countryName\",\n          \"contactsEmail\",\n          \"contactsEpamEmail\",\n          \"primaryEmail\",\n          \"contactsDiscord\",\n          \"contactsTelegram\",\n          \"mentors\",\n          \"students\"\n        ]\n      },\n      \"CreateAlertDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": { \"type\": \"string\" },\n          \"text\": { \"type\": \"string\" },\n          \"enabled\": { \"type\": \"boolean\" },\n          \"courseId\": { \"type\": \"number\" }\n        },\n        \"required\": [\"type\", \"text\"]\n      },\n      \"AlertDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"type\": { \"type\": \"string\" },\n          \"text\": { \"type\": \"string\" },\n          \"enabled\": { \"type\": \"boolean\" },\n          \"courseId\": { \"type\": \"number\", \"nullable\": true },\n          \"updatedDate\": { \"type\": \"string\" },\n          \"createdDate\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"type\", \"text\", \"enabled\", \"courseId\", \"updatedDate\", \"createdDate\"]\n      },\n      \"UpdateAlertDto\": { \"type\": \"object\", \"properties\": {} },\n      \"SoftSkillEntry\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\",\n            \"enum\": [\"skill.soft.responsible\", \"skill.soft.team-player\", \"skill.soft.communicable\"]\n          },\n          \"value\": { \"type\": \"string\", \"enum\": [\"None\", \"Poor\", \"Fair\", \"Good\", \"Great\", \"Excellent\"] }\n        },\n        \"required\": [\"id\", \"value\"]\n      },\n      \"StudentFeedbackContentDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"suggestions\": { \"type\": \"string\" },\n          \"recommendationComment\": { \"type\": \"string\" },\n          \"softSkills\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/SoftSkillEntry\" } }\n        },\n        \"required\": [\"suggestions\", \"recommendationComment\", \"softSkills\"]\n      },\n      \"CreateStudentFeedbackDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": { \"$ref\": \"#/components/schemas/StudentFeedbackContentDto\" },\n          \"recommendation\": { \"type\": \"string\", \"enum\": [\"hire\", \"not-hire\"] },\n          \"englishLevel\": { \"type\": \"string\", \"enum\": [\"unknown\", \"a0\", \"a1\", \"a2\", \"b1\", \"b2\", \"c1\", \"c2\"] }\n        },\n        \"required\": [\"content\", \"recommendation\", \"englishLevel\"]\n      },\n      \"PersonDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"name\": { \"type\": \"string\" }, \"githubId\": { \"type\": \"string\" }, \"id\": { \"type\": \"number\" } },\n        \"required\": [\"name\", \"githubId\", \"id\"]\n      },\n      \"StudentFeedbackDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" },\n          \"content\": { \"$ref\": \"#/components/schemas/StudentFeedbackContentDto\" },\n          \"recommendation\": { \"type\": \"string\", \"enum\": [\"hire\", \"not-hire\"] },\n          \"author\": { \"$ref\": \"#/components/schemas/PersonDto\" },\n          \"mentor\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/PersonDto\" }] },\n          \"englishLevel\": { \"type\": \"string\", \"enum\": [\"unknown\", \"a0\", \"a1\", \"a2\", \"b1\", \"b2\", \"c1\", \"c2\"] }\n        },\n        \"required\": [\n          \"id\",\n          \"createdDate\",\n          \"updatedDate\",\n          \"content\",\n          \"recommendation\",\n          \"author\",\n          \"mentor\",\n          \"englishLevel\"\n        ]\n      },\n      \"UpdateStudentFeedbackDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": { \"$ref\": \"#/components/schemas/StudentFeedbackContentDto\" },\n          \"recommendation\": { \"type\": \"string\", \"enum\": [\"hire\", \"not-hire\"] },\n          \"englishLevel\": { \"type\": \"string\", \"enum\": [\"unknown\", \"a0\", \"a1\", \"a2\", \"b1\", \"b2\", \"c1\", \"c2\"] }\n        },\n        \"required\": [\"content\", \"recommendation\", \"englishLevel\"]\n      },\n      \"IdNameDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"name\": { \"type\": \"string\" }, \"id\": { \"type\": \"number\" } },\n        \"required\": [\"name\", \"id\"]\n      },\n      \"CourseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"fullName\": { \"type\": \"string\" },\n          \"alias\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"year\": { \"type\": \"number\" },\n          \"startDate\": { \"type\": \"string\" },\n          \"endDate\": { \"type\": \"string\" },\n          \"registrationEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"primarySkillId\": { \"type\": \"string\" },\n          \"primarySkillName\": { \"type\": \"string\" },\n          \"locationName\": { \"type\": \"string\" },\n          \"discordServerId\": { \"type\": \"number\" },\n          \"completed\": { \"type\": \"boolean\" },\n          \"planned\": { \"type\": \"boolean\" },\n          \"inviteOnly\": { \"type\": \"boolean\" },\n          \"certificateIssuer\": { \"type\": \"string\" },\n          \"usePrivateRepositories\": { \"type\": \"boolean\" },\n          \"personalMentoring\": { \"type\": \"boolean\" },\n          \"personalMentoringStartDate\": { \"type\": \"string\", \"nullable\": true },\n          \"personalMentoringEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"logo\": { \"type\": \"string\" },\n          \"discipline\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/IdNameDto\" }] },\n          \"minStudentsPerMentor\": { \"type\": \"number\" },\n          \"certificateThreshold\": { \"type\": \"number\" },\n          \"wearecommunityUrl\": { \"type\": \"string\", \"nullable\": true },\n          \"certificateDisciplines\": { \"nullable\": true, \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n        },\n        \"required\": [\n          \"id\",\n          \"createdDate\",\n          \"updatedDate\",\n          \"name\",\n          \"fullName\",\n          \"alias\",\n          \"description\",\n          \"descriptionUrl\",\n          \"year\",\n          \"startDate\",\n          \"endDate\",\n          \"registrationEndDate\",\n          \"primarySkillId\",\n          \"primarySkillName\",\n          \"locationName\",\n          \"discordServerId\",\n          \"completed\",\n          \"planned\",\n          \"inviteOnly\",\n          \"certificateIssuer\",\n          \"usePrivateRepositories\",\n          \"personalMentoring\",\n          \"personalMentoringStartDate\",\n          \"personalMentoringEndDate\",\n          \"logo\",\n          \"discipline\",\n          \"minStudentsPerMentor\",\n          \"certificateThreshold\",\n          \"wearecommunityUrl\",\n          \"certificateDisciplines\"\n        ]\n      },\n      \"CreateCourseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"startDate\": { \"type\": \"string\" },\n          \"endDate\": { \"type\": \"string\" },\n          \"fullName\": { \"type\": \"string\" },\n          \"alias\": { \"type\": \"string\" },\n          \"registrationEndDate\": { \"type\": \"string\" },\n          \"completed\": { \"type\": \"boolean\" },\n          \"planned\": { \"type\": \"boolean\" },\n          \"inviteOnly\": { \"type\": \"boolean\" },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"disciplineId\": { \"type\": \"number\" },\n          \"discordServerId\": { \"type\": \"number\" },\n          \"usePrivateRepositories\": { \"type\": \"boolean\" },\n          \"certificateIssuer\": { \"type\": \"string\" },\n          \"personalMentoring\": { \"type\": \"boolean\" },\n          \"personalMentoringStartDate\": { \"type\": \"string\" },\n          \"personalMentoringEndDate\": { \"type\": \"string\" },\n          \"logo\": { \"type\": \"string\" },\n          \"minStudentsPerMentor\": { \"type\": \"number\" },\n          \"certificateThreshold\": { \"type\": \"number\" },\n          \"wearecommunityUrl\": { \"type\": \"string\" },\n          \"certificateDisciplines\": { \"nullable\": true, \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n        },\n        \"required\": [\n          \"name\",\n          \"startDate\",\n          \"endDate\",\n          \"fullName\",\n          \"alias\",\n          \"certificateThreshold\",\n          \"wearecommunityUrl\",\n          \"certificateDisciplines\"\n        ]\n      },\n      \"UpdateCourseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"fullName\": { \"type\": \"string\" },\n          \"alias\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"year\": { \"type\": \"number\" },\n          \"startDate\": { \"type\": \"string\" },\n          \"endDate\": { \"type\": \"string\" },\n          \"registrationEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"locationName\": { \"type\": \"string\" },\n          \"discordServerId\": { \"type\": \"number\" },\n          \"completed\": { \"type\": \"boolean\" },\n          \"planned\": { \"type\": \"boolean\" },\n          \"inviteOnly\": { \"type\": \"boolean\" },\n          \"certificateIssuer\": { \"type\": \"string\" },\n          \"usePrivateRepositories\": { \"type\": \"boolean\" },\n          \"personalMentoring\": { \"type\": \"boolean\" },\n          \"personalMentoringStartDate\": { \"type\": \"string\", \"nullable\": true },\n          \"personalMentoringEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"logo\": { \"type\": \"string\" },\n          \"disciplineId\": { \"type\": \"number\" },\n          \"minStudentsPerMentor\": { \"type\": \"number\" },\n          \"certificateThreshold\": { \"type\": \"number\" },\n          \"wearecommunityUrl\": { \"type\": \"string\", \"nullable\": true },\n          \"certificateDisciplines\": { \"nullable\": true, \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n        },\n        \"required\": [\"certificateThreshold\"]\n      },\n      \"LeaveCourseRequestDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"reasonForLeaving\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"otherComment\": { \"type\": \"string\" }\n        }\n      },\n      \"Discord\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\" },\n          \"username\": { \"type\": \"string\" },\n          \"discriminator\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"username\", \"discriminator\"]\n      },\n      \"UserStudentCourseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"alias\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"hasCertificate\": { \"type\": \"boolean\" },\n          \"completed\": { \"type\": \"boolean\" },\n          \"studentIsExpelled\": { \"type\": \"boolean\" },\n          \"certificateId\": { \"type\": \"string\" },\n          \"mentorGithubId\": { \"type\": \"string\" },\n          \"mentorFullName\": { \"type\": \"string\" },\n          \"totalScore\": { \"type\": \"number\" },\n          \"rank\": { \"type\": \"number\" }\n        },\n        \"required\": [\n          \"alias\",\n          \"name\",\n          \"hasCertificate\",\n          \"completed\",\n          \"studentIsExpelled\",\n          \"certificateId\",\n          \"mentorGithubId\",\n          \"mentorFullName\",\n          \"totalScore\",\n          \"rank\"\n        ]\n      },\n      \"UserStudentDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\", \"description\": \"User id\" },\n          \"githubId\": { \"type\": \"string\", \"description\": \"User github id\" },\n          \"fullName\": { \"type\": \"string\", \"description\": \"User full name\" },\n          \"country\": { \"type\": \"object\", \"description\": \"User country\" },\n          \"city\": { \"type\": \"object\", \"description\": \"User city\" },\n          \"contactsEmail\": { \"type\": \"string\", \"description\": \"User email\" },\n          \"contactsTelegram\": { \"type\": \"string\", \"description\": \"User telegram\" },\n          \"contactsLinkedIn\": { \"type\": \"string\", \"description\": \"User linkedIn\" },\n          \"contactsSkype\": { \"type\": \"string\", \"description\": \"User skype\" },\n          \"contactsPhone\": { \"type\": \"string\", \"description\": \"User phone\" },\n          \"discord\": { \"description\": \"User discord\", \"allOf\": [{ \"$ref\": \"#/components/schemas/Discord\" }] },\n          \"onGoingCourses\": {\n            \"description\": \"User on going courses\",\n            \"type\": \"array\",\n            \"items\": { \"$ref\": \"#/components/schemas/UserStudentCourseDto\" }\n          },\n          \"previousCourses\": {\n            \"description\": \"User previous courses\",\n            \"type\": \"array\",\n            \"items\": { \"$ref\": \"#/components/schemas/UserStudentCourseDto\" }\n          },\n          \"languages\": { \"description\": \"User languages\", \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n        },\n        \"required\": [\n          \"id\",\n          \"githubId\",\n          \"fullName\",\n          \"country\",\n          \"city\",\n          \"contactsEmail\",\n          \"contactsTelegram\",\n          \"contactsLinkedIn\",\n          \"contactsSkype\",\n          \"contactsPhone\",\n          \"discord\",\n          \"onGoingCourses\",\n          \"previousCourses\",\n          \"languages\"\n        ]\n      },\n      \"PaginationMetaDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"itemCount\": { \"type\": \"number\" },\n          \"total\": { \"type\": \"number\" },\n          \"current\": { \"type\": \"number\" },\n          \"pageSize\": { \"type\": \"number\" },\n          \"totalPages\": { \"type\": \"number\" }\n        },\n        \"required\": [\"itemCount\", \"total\", \"current\", \"pageSize\", \"totalPages\"]\n      },\n      \"UserStudentsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/UserStudentDto\" } },\n          \"pagination\": { \"$ref\": \"#/components/schemas/PaginationMetaDto\" }\n        },\n        \"required\": [\"content\", \"pagination\"]\n      },\n      \"StudentDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"active\": { \"type\": \"boolean\" },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"countryName\": { \"type\": \"string\", \"nullable\": true },\n          \"totalScore\": { \"type\": \"number\" },\n          \"rank\": { \"type\": \"number\" }\n        },\n        \"required\": [\"name\", \"githubId\", \"id\", \"active\", \"cityName\", \"countryName\", \"totalScore\", \"rank\"]\n      },\n      \"StudentsDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"id\": { \"type\": \"number\" }, \"githubId\": { \"type\": \"string\" }, \"name\": { \"type\": \"string\" } },\n        \"required\": [\"id\", \"githubId\", \"name\"]\n      },\n      \"MentorOptionsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"maxStudentsLimit\": { \"type\": \"number\" },\n          \"preferedStudentsLocation\": { \"type\": \"string\", \"enum\": [\"any\", \"country\", \"city\"] },\n          \"students\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/StudentsDto\" } }\n        },\n        \"required\": [\"maxStudentsLimit\", \"preferedStudentsLocation\", \"students\"]\n      },\n      \"MentorStudentDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"active\": { \"type\": \"boolean\" },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"countryName\": { \"type\": \"string\", \"nullable\": true },\n          \"totalScore\": { \"type\": \"number\" },\n          \"rank\": { \"type\": \"number\" },\n          \"feedbacks\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/StudentFeedbackDto\" } },\n          \"repoUrl\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\n          \"name\",\n          \"githubId\",\n          \"id\",\n          \"active\",\n          \"cityName\",\n          \"countryName\",\n          \"totalScore\",\n          \"rank\",\n          \"feedbacks\",\n          \"repoUrl\"\n        ]\n      },\n      \"SolutionItemStatusEnum\": { \"type\": \"string\", \"enum\": [\"in-review\", \"done\", \"random-task\"] },\n      \"MentorDashboardDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"studentGithubId\": { \"type\": \"string\" },\n          \"studentName\": { \"type\": \"string\" },\n          \"taskName\": { \"type\": \"string\" },\n          \"taskDescriptionUrl\": { \"type\": \"string\" },\n          \"courseTaskId\": { \"type\": \"number\" },\n          \"maxScore\": { \"type\": \"number\" },\n          \"resultScore\": { \"type\": \"number\", \"nullable\": true },\n          \"solutionUrl\": { \"type\": \"string\" },\n          \"status\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/SolutionItemStatusEnum\" }] },\n          \"endDate\": { \"type\": \"string\" }\n        },\n        \"required\": [\n          \"studentGithubId\",\n          \"studentName\",\n          \"taskName\",\n          \"taskDescriptionUrl\",\n          \"courseTaskId\",\n          \"maxScore\",\n          \"resultScore\",\n          \"solutionUrl\",\n          \"status\",\n          \"endDate\"\n        ]\n      },\n      \"CrossCheckStatusEnum\": { \"type\": \"string\", \"enum\": [\"initial\", \"distributed\", \"completed\"] },\n      \"Validations\": {\n        \"type\": \"object\",\n        \"properties\": { \"githubIdInUrl\": { \"type\": \"boolean\" }, \"githubPrInUrl\": { \"type\": \"boolean\" } },\n        \"required\": [\"githubIdInUrl\", \"githubPrInUrl\"]\n      },\n      \"CourseTaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"taskId\": { \"type\": \"number\" },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"jstask\",\n              \"kotlintask\",\n              \"objctask\",\n              \"htmltask\",\n              \"ipynb\",\n              \"selfeducation\",\n              \"codewars\",\n              \"test\",\n              \"codejam\",\n              \"interview\",\n              \"stage-interview\",\n              \"cv:html\",\n              \"cv:markdown\"\n            ]\n          },\n          \"name\": { \"type\": \"string\" },\n          \"checker\": { \"type\": \"string\", \"enum\": [\"auto-test\", \"assigned\", \"mentor\", \"taskOwner\", \"crossCheck\"] },\n          \"studentStartDate\": { \"type\": \"string\" },\n          \"studentEndDate\": { \"type\": \"string\" },\n          \"studentRegistrationStartDate\": { \"type\": \"string\", \"nullable\": true },\n          \"crossCheckEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"taskOwner\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/PersonDto\" }] },\n          \"taskSolutions\": { \"type\": \"object\", \"nullable\": true },\n          \"maxScore\": { \"type\": \"number\" },\n          \"scoreWeight\": { \"type\": \"number\" },\n          \"pairsCount\": { \"type\": \"number\", \"nullable\": true },\n          \"crossCheckStatus\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/CrossCheckStatusEnum\" }] },\n          \"submitText\": { \"type\": \"string\", \"nullable\": true },\n          \"validations\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/Validations\" }] }\n        },\n        \"required\": [\n          \"id\",\n          \"taskId\",\n          \"type\",\n          \"name\",\n          \"checker\",\n          \"studentStartDate\",\n          \"studentEndDate\",\n          \"studentRegistrationStartDate\",\n          \"crossCheckEndDate\",\n          \"descriptionUrl\",\n          \"taskOwner\",\n          \"taskSolutions\",\n          \"maxScore\",\n          \"scoreWeight\",\n          \"pairsCount\",\n          \"crossCheckStatus\",\n          \"submitText\",\n          \"validations\"\n        ]\n      },\n      \"CourseTaskDetailedDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"taskId\": { \"type\": \"number\" },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"jstask\",\n              \"kotlintask\",\n              \"objctask\",\n              \"htmltask\",\n              \"ipynb\",\n              \"selfeducation\",\n              \"codewars\",\n              \"test\",\n              \"codejam\",\n              \"interview\",\n              \"stage-interview\",\n              \"cv:html\",\n              \"cv:markdown\"\n            ]\n          },\n          \"name\": { \"type\": \"string\" },\n          \"checker\": { \"type\": \"string\", \"enum\": [\"auto-test\", \"assigned\", \"mentor\", \"taskOwner\", \"crossCheck\"] },\n          \"studentStartDate\": { \"type\": \"string\" },\n          \"studentEndDate\": { \"type\": \"string\" },\n          \"studentRegistrationStartDate\": { \"type\": \"string\", \"nullable\": true },\n          \"crossCheckEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"taskOwner\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/PersonDto\" }] },\n          \"taskSolutions\": { \"type\": \"object\", \"nullable\": true },\n          \"maxScore\": { \"type\": \"number\" },\n          \"scoreWeight\": { \"type\": \"number\" },\n          \"pairsCount\": { \"type\": \"number\", \"nullable\": true },\n          \"crossCheckStatus\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/CrossCheckStatusEnum\" }] },\n          \"submitText\": { \"type\": \"string\", \"nullable\": true },\n          \"validations\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/Validations\" }] },\n          \"publicAttributes\": { \"type\": \"object\" },\n          \"githubRepoName\": { \"type\": \"string\" },\n          \"sourceGithubRepoUrl\": { \"type\": \"string\" },\n          \"resultsCount\": { \"type\": \"number\" }\n        },\n        \"required\": [\n          \"id\",\n          \"taskId\",\n          \"type\",\n          \"name\",\n          \"checker\",\n          \"studentStartDate\",\n          \"studentEndDate\",\n          \"studentRegistrationStartDate\",\n          \"crossCheckEndDate\",\n          \"descriptionUrl\",\n          \"taskOwner\",\n          \"taskSolutions\",\n          \"maxScore\",\n          \"scoreWeight\",\n          \"pairsCount\",\n          \"crossCheckStatus\",\n          \"submitText\",\n          \"validations\",\n          \"publicAttributes\",\n          \"githubRepoName\",\n          \"sourceGithubRepoUrl\",\n          \"resultsCount\"\n        ]\n      },\n      \"CheckerEnum\": { \"type\": \"string\", \"enum\": [\"auto-test\", \"assigned\", \"mentor\", \"taskOwner\", \"crossCheck\"] },\n      \"CreateCourseTaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"taskId\": { \"type\": \"number\" },\n          \"maxScore\": { \"type\": \"number\" },\n          \"scoreWeight\": { \"type\": \"number\" },\n          \"checker\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/CheckerEnum\" }] },\n          \"studentStartDate\": { \"type\": \"string\" },\n          \"studentEndDate\": { \"type\": \"string\" },\n          \"studentRegistrationStartDate\": { \"type\": \"string\" },\n          \"crossCheckEndDate\": { \"type\": \"string\" },\n          \"taskOwnerId\": { \"type\": \"number\" },\n          \"pairsCount\": { \"type\": \"number\" },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"jstask\",\n              \"kotlintask\",\n              \"objctask\",\n              \"htmltask\",\n              \"ipynb\",\n              \"selfeducation\",\n              \"codewars\",\n              \"test\",\n              \"codejam\",\n              \"interview\",\n              \"stage-interview\",\n              \"cv:html\",\n              \"cv:markdown\"\n            ]\n          },\n          \"submitText\": { \"type\": \"string\" },\n          \"validations\": { \"type\": \"object\" }\n        },\n        \"required\": [\"taskId\", \"checker\", \"studentStartDate\", \"studentEndDate\", \"type\", \"submitText\", \"validations\"]\n      },\n      \"UpdateCourseTaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"jstask\",\n              \"kotlintask\",\n              \"objctask\",\n              \"htmltask\",\n              \"ipynb\",\n              \"selfeducation\",\n              \"codewars\",\n              \"test\",\n              \"codejam\",\n              \"interview\",\n              \"stage-interview\",\n              \"cv:html\",\n              \"cv:markdown\"\n            ]\n          },\n          \"name\": { \"type\": \"string\" },\n          \"checker\": { \"type\": \"string\" },\n          \"studentStartDate\": { \"type\": \"string\" },\n          \"studentEndDate\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"taskOwnerId\": { \"type\": \"number\" },\n          \"maxScore\": { \"type\": \"number\" },\n          \"scoreWeight\": { \"type\": \"number\" },\n          \"pairsCount\": { \"type\": \"number\" },\n          \"taskId\": { \"type\": \"number\" },\n          \"crossCheckEndDate\": { \"type\": \"string\" },\n          \"submitText\": { \"type\": \"string\" },\n          \"validations\": { \"type\": \"object\" },\n          \"studentRegistrationStartDate\": { \"type\": \"string\" }\n        },\n        \"required\": [\"studentStartDate\", \"studentEndDate\", \"submitText\", \"validations\"]\n      },\n      \"Organizer\": { \"type\": \"object\", \"properties\": { \"id\": { \"type\": \"number\" } }, \"required\": [\"id\"] },\n      \"CreateCourseEventDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"eventId\": { \"type\": \"number\" },\n          \"special\": { \"type\": \"string\" },\n          \"dateTime\": { \"type\": \"string\" },\n          \"endTime\": { \"type\": \"string\" },\n          \"duration\": { \"type\": \"number\" },\n          \"place\": { \"type\": \"string\" },\n          \"organizer\": { \"$ref\": \"#/components/schemas/Organizer\" },\n          \"organizerId\": { \"type\": \"number\" },\n          \"broadcastUrl\": { \"type\": \"string\" },\n          \"coordinator\": { \"type\": \"string\" },\n          \"comment\": { \"type\": \"string\" }\n        },\n        \"required\": [\"eventId\"]\n      },\n      \"CourseEventDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"lecture_online\",\n              \"lecture_offline\",\n              \"lecture_mixed\",\n              \"lecture_self_study\",\n              \"warmup\",\n              \"info\",\n              \"workshop\",\n              \"meetup\",\n              \"cross_check_deadline\",\n              \"webinar\",\n              \"special\"\n            ]\n          },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"dateTime\": { \"type\": \"string\" },\n          \"endTime\": { \"type\": \"string\" },\n          \"organizer\": { \"$ref\": \"#/components/schemas/PersonDto\" }\n        },\n        \"required\": [\"id\", \"name\", \"type\", \"description\", \"descriptionUrl\", \"dateTime\", \"endTime\", \"organizer\"]\n      },\n      \"UpdateCourseEventDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"special\": { \"type\": \"string\" },\n          \"dateTime\": { \"type\": \"string\" },\n          \"endTime\": { \"type\": \"string\" },\n          \"duration\": { \"type\": \"number\" },\n          \"place\": { \"type\": \"string\" },\n          \"organizer\": { \"$ref\": \"#/components/schemas/Organizer\" },\n          \"organizerId\": { \"type\": \"number\" },\n          \"broadcastUrl\": { \"type\": \"string\" },\n          \"coordinator\": { \"type\": \"string\" },\n          \"comment\": { \"type\": \"string\" }\n        }\n      },\n      \"Attributes\": { \"type\": \"object\", \"properties\": { \"template\": { \"type\": \"string\", \"nullable\": true } } },\n      \"InterviewDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"type\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"startDate\": { \"type\": \"string\" },\n          \"endDate\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\", \"nullable\": true },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"attributes\": { \"$ref\": \"#/components/schemas/Attributes\" },\n          \"studentRegistrationStartDate\": {\n            \"format\": \"date-time\",\n            \"type\": \"string\",\n            \"description\": \"Date when student can register for the interview\"\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"type\",\n          \"name\",\n          \"startDate\",\n          \"endDate\",\n          \"description\",\n          \"descriptionUrl\",\n          \"attributes\",\n          \"studentRegistrationStartDate\"\n        ]\n      },\n      \"InterviewCommentDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"id\": { \"type\": \"number\" }, \"commentToStudent\": { \"type\": \"string\", \"nullable\": true } },\n        \"required\": [\"id\", \"commentToStudent\"]\n      },\n      \"InterviewStatus\": { \"type\": \"number\", \"enum\": [1, 0] },\n      \"InterviewPairDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"result\": { \"type\": \"number\", \"nullable\": true },\n          \"status\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/InterviewStatus\" }] },\n          \"interviewer\": { \"$ref\": \"#/components/schemas/PersonDto\" },\n          \"student\": { \"$ref\": \"#/components/schemas/PersonDto\" }\n        },\n        \"required\": [\"id\", \"result\", \"status\", \"interviewer\", \"student\"]\n      },\n      \"InterviewDistributeDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"clean\": { \"type\": \"boolean\", \"default\": false },\n          \"registrationEnabled\": { \"type\": \"boolean\", \"default\": true }\n        },\n        \"required\": [\"clean\", \"registrationEnabled\"]\n      },\n      \"InterviewDistributeResponseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"courseTaskId\": { \"type\": \"number\" },\n          \"mentorId\": { \"type\": \"number\" },\n          \"studentId\": { \"type\": \"number\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"courseTaskId\", \"mentorId\", \"studentId\", \"createdDate\", \"updatedDate\"]\n      },\n      \"AvailableStudentDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"countryName\": { \"type\": \"string\", \"nullable\": true },\n          \"isGoodCandidate\": { \"type\": \"boolean\" },\n          \"rating\": { \"type\": \"string\", \"nullable\": true },\n          \"totalScore\": { \"type\": \"number\" },\n          \"registeredDate\": { \"type\": \"string\" },\n          \"maxScore\": { \"type\": \"number\" },\n          \"feedbackVersion\": { \"type\": \"number\" }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"githubId\",\n          \"cityName\",\n          \"countryName\",\n          \"isGoodCandidate\",\n          \"rating\",\n          \"totalScore\",\n          \"registeredDate\",\n          \"maxScore\",\n          \"feedbackVersion\"\n        ]\n      },\n      \"InterviewFeedbackDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"version\": { \"type\": \"number\" },\n          \"json\": { \"type\": \"object\" },\n          \"isCompleted\": { \"type\": \"boolean\" },\n          \"maxScore\": { \"type\": \"number\" }\n        },\n        \"required\": [\"isCompleted\", \"maxScore\"]\n      },\n      \"PutInterviewFeedbackDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"version\": { \"type\": \"number\" },\n          \"json\": { \"type\": \"object\" },\n          \"decision\": { \"type\": \"string\" },\n          \"isGoodCandidate\": { \"type\": \"boolean\" },\n          \"isCompleted\": { \"type\": \"boolean\" },\n          \"score\": { \"type\": \"number\" }\n        },\n        \"required\": [\"version\", \"json\", \"isCompleted\"]\n      },\n      \"CheckTasksDeadlineDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"deadlineInHours\": { \"type\": \"number\" } },\n        \"required\": [\"deadlineInHours\"]\n      },\n      \"UserDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"id\": { \"type\": \"number\" }, \"name\": { \"type\": \"string\" }, \"githubId\": { \"type\": \"string\" } },\n        \"required\": [\"id\", \"name\", \"githubId\"]\n      },\n      \"ExpelledStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\" },\n          \"course\": { \"$ref\": \"#/components/schemas/CourseDto\" },\n          \"user\": { \"$ref\": \"#/components/schemas/UserDto\" },\n          \"reasonForLeaving\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"otherComment\": { \"type\": \"string\" },\n          \"submittedAt\": { \"type\": \"string\", \"format\": \"date-time\" }\n        },\n        \"required\": [\"id\", \"course\", \"user\", \"otherComment\", \"submittedAt\"]\n      },\n      \"CountryStatDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"countryName\": { \"type\": \"string\" }, \"count\": { \"type\": \"number\" } },\n        \"required\": [\"countryName\", \"count\"]\n      },\n      \"CountriesStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"countries\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CountryStatDto\" } } },\n        \"required\": [\"countries\"]\n      },\n      \"CourseStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"activeStudentsCount\": { \"type\": \"number\" },\n          \"totalStudents\": { \"type\": \"number\" },\n          \"studentsWithMentorCount\": { \"type\": \"number\" },\n          \"certifiedStudentsCount\": { \"type\": \"number\" },\n          \"eligibleForCertificationCount\": { \"type\": \"number\" }\n        },\n        \"required\": [\n          \"activeStudentsCount\",\n          \"totalStudents\",\n          \"studentsWithMentorCount\",\n          \"certifiedStudentsCount\",\n          \"eligibleForCertificationCount\"\n        ]\n      },\n      \"CourseMentorsStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"mentorsActiveCount\": { \"type\": \"number\" },\n          \"mentorsTotalCount\": { \"type\": \"number\" },\n          \"epamMentorsCount\": { \"type\": \"number\" }\n        },\n        \"required\": [\"mentorsActiveCount\", \"mentorsTotalCount\", \"epamMentorsCount\"]\n      },\n      \"CourseAggregateStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"studentsCountries\": { \"$ref\": \"#/components/schemas/CountriesStatsDto\" },\n          \"studentsStats\": { \"$ref\": \"#/components/schemas/CourseStatsDto\" },\n          \"mentorsCountries\": { \"$ref\": \"#/components/schemas/CountriesStatsDto\" },\n          \"mentorsStats\": { \"$ref\": \"#/components/schemas/CourseMentorsStatsDto\" },\n          \"courseTasks\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CourseTaskDto\" } },\n          \"studentsCertificatesCountries\": { \"$ref\": \"#/components/schemas/CountriesStatsDto\" }\n        },\n        \"required\": [\n          \"studentsCountries\",\n          \"studentsStats\",\n          \"mentorsCountries\",\n          \"mentorsStats\",\n          \"courseTasks\",\n          \"studentsCertificatesCountries\"\n        ]\n      },\n      \"TaskPerformanceStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"totalAchievement\": { \"type\": \"number\", \"description\": \"Total number of students who submitted the task\" },\n          \"minimalAchievement\": {\n            \"type\": \"number\",\n            \"description\": \"Number of students scoring between 1% and 20% of the maximum points\"\n          },\n          \"lowAchievement\": {\n            \"type\": \"number\",\n            \"description\": \"Number of students scoring between 21% and 50% of the maximum points\"\n          },\n          \"moderateAchievement\": {\n            \"type\": \"number\",\n            \"description\": \"Number of students scoring between 51% and 70% of the maximum points\"\n          },\n          \"highAchievement\": {\n            \"type\": \"number\",\n            \"description\": \"Number of students scoring between 71% and 90% of the maximum points\"\n          },\n          \"exceptionalAchievement\": {\n            \"type\": \"number\",\n            \"description\": \"Number of students scoring between 91% and 99% of the maximum points\"\n          },\n          \"perfectScores\": { \"type\": \"number\", \"description\": \"Number of students achieving a perfect score of 100%\" }\n        },\n        \"required\": [\n          \"totalAchievement\",\n          \"minimalAchievement\",\n          \"lowAchievement\",\n          \"moderateAchievement\",\n          \"highAchievement\",\n          \"exceptionalAchievement\",\n          \"perfectScores\"\n        ]\n      },\n      \"CrossCheckCriteriaDataDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"key\": { \"type\": \"string\" },\n          \"max\": { \"type\": \"number\" },\n          \"text\": { \"type\": \"string\" },\n          \"type\": { \"type\": \"string\", \"enum\": [\"title\", \"subtask\", \"penalty\"] },\n          \"point\": { \"type\": \"number\" },\n          \"textComment\": { \"type\": \"string\" }\n        },\n        \"required\": [\"key\", \"text\", \"type\"]\n      },\n      \"HistoricalScoreDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"comment\": { \"type\": \"string\" },\n          \"dateTime\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"criteria\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CrossCheckCriteriaDataDto\" } }\n        },\n        \"required\": [\"comment\", \"dateTime\"]\n      },\n      \"CrossCheckMessageAuthorDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"githubId\": { \"type\": \"string\" }, \"id\": { \"type\": \"number\" } },\n        \"required\": [\"githubId\", \"id\"]\n      },\n      \"CrossCheckMessageDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"author\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/CrossCheckMessageAuthorDto\" }] },\n          \"content\": { \"type\": \"string\" },\n          \"timestamp\": { \"type\": \"string\" },\n          \"isReviewerRead\": { \"type\": \"boolean\" },\n          \"isStudentRead\": { \"type\": \"boolean\" },\n          \"role\": { \"type\": \"string\", \"enum\": [\"reviewer\", \"student\"] }\n        },\n        \"required\": [\"author\", \"content\", \"timestamp\", \"isReviewerRead\", \"isStudentRead\", \"role\"]\n      },\n      \"CrossCheckPairDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"student\": { \"$ref\": \"#/components/schemas/PersonDto\" },\n          \"checker\": { \"$ref\": \"#/components/schemas/PersonDto\" },\n          \"task\": { \"$ref\": \"#/components/schemas/IdNameDto\" },\n          \"score\": { \"type\": \"number\" },\n          \"id\": { \"type\": \"number\" },\n          \"comment\": { \"type\": \"string\" },\n          \"url\": { \"type\": \"string\" },\n          \"reviewedDate\": { \"type\": \"string\" },\n          \"privateRepository\": { \"type\": \"string\" },\n          \"submittedDate\": { \"type\": \"string\" },\n          \"historicalScores\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/HistoricalScoreDto\" } },\n          \"messages\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CrossCheckMessageDto\" } }\n        },\n        \"required\": [\n          \"student\",\n          \"checker\",\n          \"task\",\n          \"score\",\n          \"id\",\n          \"comment\",\n          \"url\",\n          \"reviewedDate\",\n          \"privateRepository\",\n          \"submittedDate\",\n          \"historicalScores\",\n          \"messages\"\n        ]\n      },\n      \"PaginationDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"pageSize\": { \"type\": \"number\" },\n          \"current\": { \"type\": \"number\" },\n          \"total\": { \"type\": \"number\" },\n          \"totalPages\": { \"type\": \"number\" }\n        },\n        \"required\": [\"pageSize\", \"current\", \"total\", \"totalPages\"]\n      },\n      \"CrossCheckPairResponseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"items\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CrossCheckPairDto\" } },\n          \"pagination\": { \"$ref\": \"#/components/schemas/PaginationDto\" }\n        },\n        \"required\": [\"items\", \"pagination\"]\n      },\n      \"AvailableReviewStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"checksCount\": { \"type\": \"number\" },\n          \"completedChecksCount\": { \"type\": \"number\" }\n        },\n        \"required\": [\"name\", \"id\", \"checksCount\", \"completedChecksCount\"]\n      },\n      \"CrossCheckAuthorDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"discord\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/Discord\" }] }\n        },\n        \"required\": [\"id\", \"name\", \"githubId\", \"discord\"]\n      },\n      \"CrossCheckSolutionReviewDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"dateTime\": { \"type\": \"number\" },\n          \"comment\": { \"type\": \"string\" },\n          \"criteria\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CrossCheckCriteriaDataDto\" } },\n          \"author\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/CrossCheckAuthorDto\" }] },\n          \"score\": { \"type\": \"number\" },\n          \"messages\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CrossCheckMessageDto\" } }\n        },\n        \"required\": [\"id\", \"dateTime\", \"comment\", \"author\", \"score\", \"messages\"]\n      },\n      \"CrossCheckFeedbackDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"url\": { \"type\": \"string\" },\n          \"reviews\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CrossCheckSolutionReviewDto\" } }\n        }\n      },\n      \"MentorDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"id\": { \"type\": \"number\" }, \"githubId\": { \"type\": \"string\" }, \"name\": { \"type\": \"string\" } },\n        \"required\": [\"id\", \"githubId\", \"name\"]\n      },\n      \"TaskResultsDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"courseTaskId\": { \"type\": \"number\" }, \"score\": { \"type\": \"number\" } },\n        \"required\": [\"courseTaskId\", \"score\"]\n      },\n      \"ContactsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"phone\": { \"type\": \"string\", \"nullable\": true },\n          \"email\": { \"type\": \"string\", \"nullable\": true },\n          \"epamEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"skype\": { \"type\": \"string\", \"nullable\": true },\n          \"whatsApp\": { \"type\": \"string\", \"nullable\": true },\n          \"telegram\": { \"type\": \"string\", \"nullable\": true },\n          \"notes\": { \"type\": \"string\", \"nullable\": true },\n          \"linkedIn\": { \"type\": \"string\", \"nullable\": true },\n          \"discord\": { \"type\": \"string\", \"nullable\": true }\n        }\n      },\n      \"ScoreStudentDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"active\": { \"type\": \"boolean\" },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"countryName\": { \"type\": \"string\", \"nullable\": true },\n          \"totalScore\": { \"type\": \"number\" },\n          \"rank\": { \"type\": \"number\" },\n          \"mentor\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/MentorDto\" }] },\n          \"totalScoreChangeDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"crossCheckScore\": { \"type\": \"number\" },\n          \"repositoryLastActivityDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"taskResults\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TaskResultsDto\" } },\n          \"isActive\": { \"type\": \"boolean\" },\n          \"contacts\": { \"$ref\": \"#/components/schemas/ContactsDto\" }\n        },\n        \"required\": [\n          \"name\",\n          \"githubId\",\n          \"id\",\n          \"active\",\n          \"cityName\",\n          \"countryName\",\n          \"totalScore\",\n          \"rank\",\n          \"mentor\",\n          \"totalScoreChangeDate\",\n          \"crossCheckScore\",\n          \"repositoryLastActivityDate\",\n          \"taskResults\",\n          \"isActive\",\n          \"contacts\"\n        ]\n      },\n      \"ScoreDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ScoreStudentDto\" } },\n          \"pagination\": { \"$ref\": \"#/components/schemas/PaginationMetaDto\" }\n        },\n        \"required\": [\"content\", \"pagination\"]\n      },\n      \"SaveTaskSolutionDto\": { \"type\": \"object\", \"properties\": { \"url\": { \"type\": \"string\" } }, \"required\": [\"url\"] },\n      \"TaskSolutionDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"id\": { \"type\": \"number\" }, \"courseTaskId\": { \"type\": \"number\" }, \"url\": { \"type\": \"string\" } },\n        \"required\": [\"id\", \"courseTaskId\", \"url\"]\n      },\n      \"CourseScheduleItemDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"score\": { \"type\": \"number\", \"nullable\": true },\n          \"name\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"status\": {\n            \"type\": \"string\",\n            \"enum\": [\"done\", \"available\", \"archived\", \"future\", \"missed\", \"review\", \"registered\", \"unavailable\"]\n          },\n          \"startDate\": { \"type\": \"string\" },\n          \"endDate\": { \"type\": \"string\" },\n          \"crossCheckEndDate\": { \"type\": \"string\" },\n          \"organizer\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/PersonDto\" }] },\n          \"maxScore\": { \"type\": \"number\", \"nullable\": true },\n          \"scoreWeight\": { \"type\": \"number\", \"nullable\": true },\n          \"descriptionUrl\": { \"type\": \"string\", \"nullable\": true },\n          \"tag\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"lecture\",\n              \"coding\",\n              \"self-study\",\n              \"interview\",\n              \"cross-check-submit\",\n              \"cross-check-review\",\n              \"test\",\n              \"team-distribution\"\n            ]\n          },\n          \"type\": { \"type\": \"string\", \"enum\": [\"courseTask\", \"courseEvent\", \"courseTeamDistribution\"] }\n        },\n        \"required\": [\n          \"score\",\n          \"name\",\n          \"id\",\n          \"status\",\n          \"startDate\",\n          \"endDate\",\n          \"crossCheckEndDate\",\n          \"organizer\",\n          \"maxScore\",\n          \"scoreWeight\",\n          \"descriptionUrl\",\n          \"tag\",\n          \"type\"\n        ]\n      },\n      \"CourseCopyFromDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"copyFromCourseId\": { \"type\": \"number\" } },\n        \"required\": [\"copyFromCourseId\"]\n      },\n      \"CourseScheduleTokenDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"token\": { \"type\": \"string\" } },\n        \"required\": [\"token\"]\n      },\n      \"CreateTeamDistributionDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"startDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"endDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"minTeamSize\": { \"type\": \"number\" },\n          \"maxTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSizeMode\": { \"type\": \"boolean\" },\n          \"minTotalScore\": { \"type\": \"number\" }\n        },\n        \"required\": [\n          \"name\",\n          \"startDate\",\n          \"endDate\",\n          \"description\",\n          \"descriptionUrl\",\n          \"minTeamSize\",\n          \"maxTeamSize\",\n          \"strictTeamSize\",\n          \"strictTeamSizeMode\",\n          \"minTotalScore\"\n        ]\n      },\n      \"TeamDistributionDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"registrationStatus\": {\n            \"type\": \"string\",\n            \"enum\": [\"available\", \"unavailable\", \"future\", \"completed\", \"distributed\", \"closed\"]\n          },\n          \"startDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"endDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"minTeamSize\": { \"type\": \"number\" },\n          \"maxTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSizeMode\": { \"type\": \"boolean\" },\n          \"minTotalScore\": { \"type\": \"number\" }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"registrationStatus\",\n          \"startDate\",\n          \"endDate\",\n          \"description\",\n          \"descriptionUrl\",\n          \"minTeamSize\",\n          \"maxTeamSize\",\n          \"strictTeamSize\",\n          \"strictTeamSizeMode\",\n          \"minTotalScore\"\n        ]\n      },\n      \"UpdateTeamDistributionDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"startDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"endDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"minTeamSize\": { \"type\": \"number\" },\n          \"maxTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSizeMode\": { \"type\": \"boolean\" },\n          \"minTotalScore\": { \"type\": \"number\" }\n        },\n        \"required\": [\n          \"name\",\n          \"startDate\",\n          \"endDate\",\n          \"description\",\n          \"descriptionUrl\",\n          \"minTeamSize\",\n          \"maxTeamSize\",\n          \"strictTeamSize\",\n          \"strictTeamSizeMode\",\n          \"minTotalScore\"\n        ]\n      },\n      \"TeamDistributionStudentDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"fullName\": { \"type\": \"string\" },\n          \"cvLink\": { \"type\": \"string\" },\n          \"discord\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/Discord\" }] },\n          \"telegram\": { \"type\": \"string\" },\n          \"email\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"rank\": { \"type\": \"number\" },\n          \"totalScore\": { \"type\": \"number\" },\n          \"location\": { \"type\": \"string\" },\n          \"cvUuid\": { \"type\": \"string\" }\n        },\n        \"required\": [\n          \"id\",\n          \"fullName\",\n          \"cvLink\",\n          \"discord\",\n          \"telegram\",\n          \"email\",\n          \"githubId\",\n          \"rank\",\n          \"totalScore\",\n          \"location\",\n          \"cvUuid\"\n        ]\n      },\n      \"TeamDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"chatLink\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"teamLeadId\": { \"type\": \"number\" },\n          \"teamDistributionId\": { \"type\": \"number\" },\n          \"students\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TeamDistributionStudentDto\" } }\n        },\n        \"required\": [\"id\", \"name\", \"chatLink\", \"description\", \"teamLeadId\", \"teamDistributionId\", \"students\"]\n      },\n      \"TeamDistributionDetailedDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"courseId\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"studentsWithoutTeamCount\": { \"type\": \"number\" },\n          \"teamsCount\": { \"type\": \"number\" },\n          \"myTeam\": { \"$ref\": \"#/components/schemas/TeamDto\" },\n          \"minTeamSize\": { \"type\": \"number\" },\n          \"maxTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSize\": { \"type\": \"number\" },\n          \"strictTeamSizeMode\": { \"type\": \"boolean\" }\n        },\n        \"required\": [\n          \"id\",\n          \"courseId\",\n          \"name\",\n          \"studentsWithoutTeamCount\",\n          \"teamsCount\",\n          \"myTeam\",\n          \"minTeamSize\",\n          \"maxTeamSize\",\n          \"strictTeamSize\",\n          \"strictTeamSizeMode\"\n        ]\n      },\n      \"TeamsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/TeamDto\" } },\n          \"pagination\": { \"$ref\": \"#/components/schemas/PaginationMetaDto\" }\n        },\n        \"required\": [\"content\", \"pagination\"]\n      },\n      \"CreateTeamDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"chatLink\": { \"type\": \"string\" },\n          \"studentIds\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n        },\n        \"required\": [\"name\", \"description\", \"chatLink\"]\n      },\n      \"UpdateTeamDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"chatLink\": { \"type\": \"string\" },\n          \"studentIds\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n        },\n        \"required\": [\"name\", \"description\", \"chatLink\"]\n      },\n      \"TeamPasswordDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"password\": { \"type\": \"string\" } },\n        \"required\": [\"password\"]\n      },\n      \"JoinTeamDto\": { \"type\": \"object\", \"properties\": { \"password\": { \"type\": \"string\" } }, \"required\": [\"password\"] },\n      \"TeamInfoDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"chatLink\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"teamLeadId\": { \"type\": \"number\" },\n          \"teamDistributionId\": { \"type\": \"number\" }\n        },\n        \"required\": [\"id\", \"name\", \"chatLink\", \"description\", \"teamLeadId\", \"teamDistributionId\"]\n      },\n      \"SelfEducationQuestionSelectedAnswersDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"answers\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"question\": { \"type\": \"string\" },\n          \"multiple\": { \"type\": \"boolean\" },\n          \"questionImage\": { \"type\": \"string\" },\n          \"answersType\": { \"type\": \"string\" },\n          \"selectedAnswers\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n        },\n        \"required\": [\"answers\", \"question\", \"multiple\", \"selectedAnswers\"]\n      },\n      \"TaskVerificationAttemptDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"createdDate\": { \"type\": \"number\" },\n          \"courseTaskId\": { \"type\": \"number\" },\n          \"score\": { \"type\": \"number\" },\n          \"maxScore\": { \"type\": \"number\" },\n          \"questions\": {\n            \"type\": \"array\",\n            \"items\": { \"$ref\": \"#/components/schemas/SelfEducationQuestionSelectedAnswersDto\" }\n          }\n        },\n        \"required\": [\"createdDate\", \"courseTaskId\", \"score\", \"maxScore\", \"questions\"]\n      },\n      \"Object\": { \"type\": \"object\", \"properties\": {} },\n      \"CreateTaskVerificationDto\": { \"type\": \"object\", \"properties\": { \"id\": { \"type\": \"number\" } } },\n      \"CourseUserDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"courseId\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"isManager\": { \"type\": \"boolean\" },\n          \"isSupervisor\": { \"type\": \"boolean\" },\n          \"isJuryActivist\": { \"type\": \"boolean\" },\n          \"isDementor\": { \"type\": \"boolean\" },\n          \"isActivist\": { \"type\": \"boolean\" }\n        },\n        \"required\": [\n          \"id\",\n          \"courseId\",\n          \"name\",\n          \"githubId\",\n          \"isManager\",\n          \"isSupervisor\",\n          \"isJuryActivist\",\n          \"isDementor\",\n          \"isActivist\"\n        ]\n      },\n      \"UpdateCourseUserDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"isManager\": { \"type\": \"boolean\" },\n          \"isSupervisor\": { \"type\": \"boolean\" },\n          \"isDementor\": { \"type\": \"boolean\" },\n          \"isActivist\": { \"type\": \"boolean\" },\n          \"userId\": { \"type\": \"number\" }\n        },\n        \"required\": [\"isManager\", \"isSupervisor\", \"isDementor\", \"isActivist\", \"userId\"]\n      },\n      \"CourseRolesDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"isManager\": { \"type\": \"boolean\" },\n          \"isSupervisor\": { \"type\": \"boolean\" },\n          \"isDementor\": { \"type\": \"boolean\" },\n          \"isActivist\": { \"type\": \"boolean\" }\n        },\n        \"required\": [\"isManager\", \"isSupervisor\", \"isDementor\", \"isActivist\"]\n      },\n      \"StudentId\": { \"type\": \"object\", \"properties\": { \"id\": { \"type\": \"number\" } }, \"required\": [\"id\"] },\n      \"MentorDetailsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"isActive\": { \"type\": \"boolean\" },\n          \"cityName\": { \"type\": \"string\" },\n          \"countryName\": { \"type\": \"string\" },\n          \"maxStudentsLimit\": { \"type\": \"number\" },\n          \"students\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/StudentId\" } },\n          \"interviews\": { \"type\": \"object\" },\n          \"screenings\": { \"type\": \"object\" },\n          \"taskResultsStats\": { \"type\": \"object\" },\n          \"studentsPreference\": { \"type\": \"string\", \"enum\": [\"any\", \"country\", \"city\"] },\n          \"studentsCount\": { \"type\": \"number\" },\n          \"contactsEpamEmail\": { \"type\": \"string\" }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"githubId\",\n          \"isActive\",\n          \"cityName\",\n          \"countryName\",\n          \"maxStudentsLimit\",\n          \"students\",\n          \"studentsPreference\",\n          \"contactsEpamEmail\"\n        ]\n      },\n      \"SearchMentorDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"id\": { \"type\": \"number\" }, \"githubId\": { \"type\": \"string\" }, \"name\": { \"type\": \"string\" } },\n        \"required\": [\"id\", \"githubId\", \"name\"]\n      },\n      \"ResultDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"score\": { \"type\": \"number\" }, \"courseTaskId\": { \"type\": \"number\" } }\n      },\n      \"MentorStudentSummaryDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"isActive\": { \"type\": \"boolean\" },\n          \"cityName\": { \"type\": \"string\" },\n          \"countryName\": { \"type\": \"string\" },\n          \"students\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"contactsEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsPhone\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsSkype\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsTelegram\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsNotes\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsWhatsApp\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\n          \"id\",\n          \"githubId\",\n          \"name\",\n          \"isActive\",\n          \"cityName\",\n          \"countryName\",\n          \"students\",\n          \"contactsEmail\",\n          \"contactsPhone\",\n          \"contactsSkype\",\n          \"contactsTelegram\",\n          \"contactsNotes\",\n          \"contactsWhatsApp\"\n        ]\n      },\n      \"StudentSummaryDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"totalScore\": { \"type\": \"number\" },\n          \"results\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ResultDto\" } },\n          \"isActive\": { \"type\": \"boolean\" },\n          \"mentor\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/MentorStudentSummaryDto\" }] },\n          \"rank\": { \"type\": \"number\" },\n          \"repository\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\"totalScore\", \"results\", \"isActive\", \"mentor\", \"rank\", \"repository\"]\n      },\n      \"ExpelCriteriaDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"courseTaskIds\": {\n            \"example\": [123, 456, 789],\n            \"description\": \"Array of course task IDs\",\n            \"type\": \"array\",\n            \"items\": { \"type\": \"number\" }\n          },\n          \"minScore\": { \"type\": \"number\", \"example\": 100, \"description\": \"Minimum score threshold\" }\n        }\n      },\n      \"ExpelOptionsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"keepWithMentor\": {\n            \"type\": \"boolean\",\n            \"example\": true,\n            \"description\": \"Whether to keep the student with their mentor\"\n          },\n          \"saveAssigningToMentor\": {\n            \"type\": \"boolean\",\n            \"example\": true,\n            \"description\": \"Save assigning to the mentor (default: false)\"\n          }\n        }\n      },\n      \"ExpelStatusDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"criteria\": {\n            \"description\": \"Criteria for expelling students\",\n            \"allOf\": [{ \"$ref\": \"#/components/schemas/ExpelCriteriaDto\" }]\n          },\n          \"options\": {\n            \"description\": \"Additional options for expelling\",\n            \"allOf\": [{ \"$ref\": \"#/components/schemas/ExpelOptionsDto\" }]\n          },\n          \"expellingReason\": {\n            \"type\": \"string\",\n            \"example\": \"Cheating\",\n            \"description\": \"Reason for expelling the student\"\n          }\n        },\n        \"required\": [\"criteria\", \"options\", \"expellingReason\"]\n      },\n      \"MentorReviewDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\", \"description\": \"Task solution id\" },\n          \"taskName\": { \"type\": \"string\", \"description\": \"Course task name\" },\n          \"taskId\": { \"type\": \"number\", \"description\": \"Course task id\" },\n          \"solutionUrl\": { \"type\": \"string\", \"description\": \"Task solution url\" },\n          \"submittedAt\": { \"format\": \"date-time\", \"type\": \"string\", \"description\": \"Task solution submission date\" },\n          \"checker\": { \"type\": \"string\", \"description\": \"Checker github id\" },\n          \"score\": { \"type\": \"number\", \"description\": \"Task solution score\" },\n          \"maxScore\": { \"type\": \"number\", \"description\": \"Task max score\" },\n          \"student\": { \"type\": \"string\", \"description\": \"Student github id\" },\n          \"studentId\": { \"type\": \"number\", \"description\": \"Student id\" },\n          \"reviewedAt\": { \"format\": \"date-time\", \"type\": \"string\", \"description\": \"Task solution review date\" },\n          \"taskDescriptionUrl\": { \"type\": \"string\", \"description\": \"Task description url\" }\n        },\n        \"required\": [\n          \"id\",\n          \"taskName\",\n          \"taskId\",\n          \"solutionUrl\",\n          \"submittedAt\",\n          \"checker\",\n          \"score\",\n          \"maxScore\",\n          \"student\",\n          \"studentId\",\n          \"reviewedAt\",\n          \"taskDescriptionUrl\"\n        ]\n      },\n      \"MentorReviewsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/MentorReviewDto\" } },\n          \"pagination\": { \"$ref\": \"#/components/schemas/PaginationMetaDto\" }\n        },\n        \"required\": [\"content\", \"pagination\"]\n      },\n      \"MentorReviewAssignDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"courseTaskId\": { \"type\": \"number\" },\n          \"mentorId\": { \"type\": \"number\" },\n          \"studentId\": { \"type\": \"number\" }\n        },\n        \"required\": [\"courseTaskId\", \"studentId\"]\n      },\n      \"Map\": { \"type\": \"object\", \"properties\": {} },\n      \"NotificationUserSettingsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"enabled\": { \"type\": \"boolean\" },\n          \"settings\": { \"$ref\": \"#/components/schemas/Map\" }\n        },\n        \"required\": [\"id\", \"name\", \"enabled\", \"settings\"]\n      },\n      \"UserNotificationsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"notifications\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/NotificationUserSettingsDto\" } },\n          \"connections\": { \"$ref\": \"#/components/schemas/Map\" }\n        },\n        \"required\": [\"notifications\", \"connections\"]\n      },\n      \"NotificationUserConnectionsDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"connections\": { \"$ref\": \"#/components/schemas/Map\" } },\n        \"required\": [\"connections\"]\n      },\n      \"UpdateNotificationUserSettingsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"notificationId\": { \"type\": \"string\" },\n          \"enabled\": { \"type\": \"boolean\" },\n          \"channelId\": { \"type\": \"string\" }\n        },\n        \"required\": [\"notificationId\", \"enabled\", \"channelId\"]\n      },\n      \"NotificationConnectionExistsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"channelId\": { \"type\": \"string\" },\n          \"externalId\": { \"type\": \"string\" },\n          \"userId\": { \"type\": \"number\" }\n        },\n        \"required\": [\"channelId\"]\n      },\n      \"NotificationConnectionDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"channelId\": { \"type\": \"string\" },\n          \"externalId\": { \"type\": \"string\" },\n          \"userId\": { \"type\": \"number\" },\n          \"enabled\": { \"type\": \"boolean\" }\n        },\n        \"required\": [\"channelId\", \"externalId\", \"userId\", \"enabled\"]\n      },\n      \"UpsertNotificationConnectionDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"channelId\": { \"type\": \"string\" },\n          \"externalId\": { \"type\": \"string\" },\n          \"userId\": { \"type\": \"number\" },\n          \"enabled\": { \"type\": \"boolean\" }\n        },\n        \"required\": [\"channelId\", \"externalId\", \"userId\", \"enabled\"]\n      },\n      \"SendUserNotificationDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"notificationId\": { \"type\": \"string\" },\n          \"userId\": { \"type\": \"number\" },\n          \"data\": { \"type\": \"object\" },\n          \"expireDate\": { \"type\": \"number\" }\n        },\n        \"required\": [\"notificationId\", \"userId\", \"data\", \"expireDate\"]\n      },\n      \"NotificationType\": { \"type\": \"string\", \"enum\": [\"event\", \"message\"] },\n      \"ChannelSettings\": {\n        \"type\": \"object\",\n        \"properties\": { \"channelId\": { \"type\": \"string\" }, \"template\": { \"type\": \"object\" } },\n        \"required\": [\"channelId\", \"template\"]\n      },\n      \"NotificationDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"enabled\": { \"type\": \"boolean\" },\n          \"type\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/NotificationType\" }] },\n          \"channels\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ChannelSettings\" } },\n          \"parentId\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"name\", \"enabled\", \"type\", \"channels\", \"parentId\"]\n      },\n      \"UpdateNotificationDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"enabled\": { \"type\": \"boolean\" },\n          \"channels\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ChannelSettings\" } },\n          \"type\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/NotificationType\" }] },\n          \"parentId\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"name\", \"enabled\", \"channels\", \"type\", \"parentId\"]\n      },\n      \"AuthConnectionDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"channelId\": { \"type\": \"string\" }, \"externalId\": { \"type\": \"string\" } },\n        \"required\": [\"channelId\", \"externalId\"]\n      },\n      \"ProfileCourseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"fullName\": { \"type\": \"string\" },\n          \"alias\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"year\": { \"type\": \"number\" },\n          \"startDate\": { \"type\": \"string\" },\n          \"endDate\": { \"type\": \"string\" },\n          \"registrationEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"primarySkillId\": { \"type\": \"string\" },\n          \"primarySkillName\": { \"type\": \"string\" },\n          \"locationName\": { \"type\": \"string\" },\n          \"discordServerId\": { \"type\": \"number\" },\n          \"completed\": { \"type\": \"boolean\" },\n          \"planned\": { \"type\": \"boolean\" },\n          \"inviteOnly\": { \"type\": \"boolean\" },\n          \"certificateIssuer\": { \"type\": \"string\" },\n          \"usePrivateRepositories\": { \"type\": \"boolean\" },\n          \"personalMentoring\": { \"type\": \"boolean\" },\n          \"personalMentoringStartDate\": { \"type\": \"string\", \"nullable\": true },\n          \"personalMentoringEndDate\": { \"type\": \"string\", \"nullable\": true },\n          \"logo\": { \"type\": \"string\" },\n          \"discipline\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/IdNameDto\" }] },\n          \"minStudentsPerMentor\": { \"type\": \"number\" },\n          \"certificateThreshold\": { \"type\": \"number\" },\n          \"wearecommunityUrl\": { \"type\": \"string\", \"nullable\": true },\n          \"certificateDisciplines\": { \"nullable\": true, \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n        },\n        \"required\": [\n          \"id\",\n          \"createdDate\",\n          \"updatedDate\",\n          \"name\",\n          \"fullName\",\n          \"alias\",\n          \"description\",\n          \"descriptionUrl\",\n          \"year\",\n          \"startDate\",\n          \"endDate\",\n          \"registrationEndDate\",\n          \"primarySkillId\",\n          \"primarySkillName\",\n          \"locationName\",\n          \"discordServerId\",\n          \"completed\",\n          \"planned\",\n          \"inviteOnly\",\n          \"certificateIssuer\",\n          \"usePrivateRepositories\",\n          \"personalMentoring\",\n          \"personalMentoringStartDate\",\n          \"personalMentoringEndDate\",\n          \"logo\",\n          \"discipline\",\n          \"minStudentsPerMentor\",\n          \"certificateThreshold\",\n          \"wearecommunityUrl\",\n          \"certificateDisciplines\"\n        ]\n      },\n      \"UpdateUserDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"firstName\": { \"type\": \"string\", \"nullable\": true },\n          \"lastName\": { \"type\": \"string\", \"nullable\": true },\n          \"primaryEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"countryName\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsNotes\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsPhone\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsEpamEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsSkype\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsWhatsApp\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsTelegram\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsLinkedIn\": { \"type\": \"string\", \"nullable\": true },\n          \"notes\": { \"type\": \"string\", \"nullable\": true },\n          \"aboutMyself\": { \"type\": \"string\", \"nullable\": true },\n          \"languages\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"EN\",\n                \"ZH\",\n                \"HI\",\n                \"ES\",\n                \"FR\",\n                \"AR\",\n                \"BN\",\n                \"RU\",\n                \"PT\",\n                \"ID\",\n                \"UR\",\n                \"JA\",\n                \"DE\",\n                \"PA\",\n                \"TE\",\n                \"TR\",\n                \"KO\",\n                \"MR\",\n                \"KY\",\n                \"KK\",\n                \"UZ\",\n                \"KA\",\n                \"PL\",\n                \"LT\",\n                \"LV\",\n                \"BE\",\n                \"UK\"\n              ]\n            }\n          }\n        }\n      },\n      \"Education\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"university\": { \"type\": \"string\" },\n          \"faculty\": { \"type\": \"string\" },\n          \"graduationYear\": { \"type\": \"number\" }\n        },\n        \"required\": [\"university\", \"faculty\", \"graduationYear\"]\n      },\n      \"UpdateProfileInfoDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"aboutMyself\": { \"type\": \"string\", \"nullable\": true },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"countryName\": { \"type\": \"string\", \"nullable\": true },\n          \"educationHistory\": {\n            \"nullable\": true,\n            \"type\": \"array\",\n            \"items\": { \"$ref\": \"#/components/schemas/Education\" }\n          },\n          \"englishLevel\": { \"type\": \"string\", \"nullable\": true },\n          \"languages\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"contactsPhone\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsEpamEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsSkype\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsWhatsApp\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsTelegram\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsNotes\": { \"type\": \"string\", \"nullable\": true },\n          \"contactsLinkedIn\": { \"type\": \"string\", \"nullable\": true },\n          \"discord\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/Discord\" }] }\n        }\n      },\n      \"ProfileDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"publicCvUrl\": { \"type\": \"string\", \"nullable\": true } },\n        \"required\": [\"publicCvUrl\"]\n      },\n      \"PersonalProfileDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"userId\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"primaryEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"isActiveStudent\": { \"type\": \"boolean\" }\n        },\n        \"required\": [\"userId\", \"githubId\", \"primaryEmail\", \"isActiveStudent\"]\n      },\n      \"EndorsementDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"summary\": { \"type\": \"string\" }, \"data\": { \"type\": \"object\", \"nullable\": true } },\n        \"required\": [\"summary\", \"data\"]\n      },\n      \"EndorsementUserDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"firstName\": { \"type\": \"string\" },\n          \"lastName\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"githubId\", \"firstName\", \"lastName\"]\n      },\n      \"EndorsementDataDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"user\": { \"$ref\": \"#/components/schemas/EndorsementUserDto\" },\n          \"courses\": {\n            \"description\": \"User's courses\",\n            \"type\": \"array\",\n            \"items\": { \"$ref\": \"#/components/schemas/CourseDto\" }\n          },\n          \"studentsCount\": { \"type\": \"number\", \"description\": \"Number of students\" },\n          \"interviewsCount\": { \"type\": \"number\", \"description\": \"Number of interviews\" }\n        },\n        \"required\": [\"user\", \"courses\", \"studentsCount\", \"interviewsCount\"]\n      },\n      \"CreateDisciplineDto\": { \"type\": \"object\", \"properties\": { \"name\": { \"type\": \"string\" } }, \"required\": [\"name\"] },\n      \"DisciplineDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" }\n        },\n        \"required\": [\"name\", \"id\", \"createdDate\", \"updatedDate\"]\n      },\n      \"DisciplineIdsDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"ids\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } } },\n        \"required\": [\"ids\"]\n      },\n      \"UpdateDisciplineDto\": { \"type\": \"object\", \"properties\": { \"name\": { \"type\": \"string\" } }, \"required\": [\"name\"] },\n      \"ApproveMentorDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"preselectedCourses\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } } },\n        \"required\": [\"preselectedCourses\"]\n      },\n      \"CommentMentorRegistryDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"comment\": { \"type\": \"string\", \"nullable\": true } },\n        \"required\": [\"comment\"]\n      },\n      \"MentorRegistryDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"cityName\": { \"type\": \"string\", \"nullable\": true },\n          \"preferedCourses\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"preselectedCourses\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"maxStudentsLimit\": { \"type\": \"number\" },\n          \"preferedStudentsLocation\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"technicalMentoring\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"courses\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"sendDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"receivedDate\": { \"format\": \"date-time\", \"type\": \"string\" },\n          \"hasCertificate\": { \"type\": \"boolean\" },\n          \"englishMentoring\": { \"type\": \"boolean\" },\n          \"primaryEmail\": { \"type\": \"object\" },\n          \"languagesMentoring\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"contactsEpamEmail\": { \"type\": \"string\", \"nullable\": true },\n          \"comment\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\n          \"id\",\n          \"githubId\",\n          \"cityName\",\n          \"preferedCourses\",\n          \"preselectedCourses\",\n          \"maxStudentsLimit\",\n          \"preferedStudentsLocation\",\n          \"name\",\n          \"technicalMentoring\",\n          \"courses\",\n          \"sendDate\",\n          \"receivedDate\",\n          \"hasCertificate\",\n          \"englishMentoring\",\n          \"primaryEmail\",\n          \"languagesMentoring\",\n          \"contactsEpamEmail\",\n          \"comment\"\n        ]\n      },\n      \"FilterMentorRegistryResponse\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"mentors\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/MentorRegistryDto\" } },\n          \"total\": { \"type\": \"number\" }\n        },\n        \"required\": [\"mentors\", \"total\"]\n      },\n      \"InviteMentorsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"disciplines\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"isMentor\": { \"type\": \"boolean\" },\n          \"text\": { \"type\": \"string\" }\n        },\n        \"required\": [\"disciplines\", \"isMentor\", \"text\"]\n      },\n      \"SaveCertificateDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"publicId\": { \"type\": \"string\" },\n          \"studentId\": { \"type\": \"number\" },\n          \"s3Bucket\": { \"type\": \"string\" },\n          \"s3Key\": { \"type\": \"string\" },\n          \"issueDate\": { \"type\": \"string\" }\n        },\n        \"required\": [\"publicId\", \"studentId\", \"s3Bucket\", \"s3Key\", \"issueDate\"]\n      },\n      \"CreateDiscordServerDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"gratitudeUrl\": { \"type\": \"string\" },\n          \"mentorsChatUrl\": { \"type\": \"string\" }\n        },\n        \"required\": [\"name\", \"gratitudeUrl\", \"mentorsChatUrl\"]\n      },\n      \"DiscordServerDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"createdDate\": { \"type\": \"number\" },\n          \"updatedDate\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"gratitudeUrl\": { \"type\": \"string\" },\n          \"mentorsChatUrl\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\"id\", \"createdDate\", \"updatedDate\", \"name\", \"gratitudeUrl\", \"mentorsChatUrl\"]\n      },\n      \"UpdateDiscordServerDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"gratitudeUrl\": { \"type\": \"string\" },\n          \"mentorsChatUrl\": { \"type\": \"string\" }\n        },\n        \"required\": [\"name\", \"gratitudeUrl\", \"mentorsChatUrl\"]\n      },\n      \"ResumeCourseMentor\": {\n        \"type\": \"object\",\n        \"properties\": { \"name\": { \"type\": \"string\" }, \"githubId\": { \"type\": \"string\" }, \"id\": { \"type\": \"number\" } },\n        \"required\": [\"name\", \"githubId\", \"id\"]\n      },\n      \"ResumeCourseDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"fullName\": { \"type\": \"string\" },\n          \"rank\": { \"type\": \"number\" },\n          \"totalScore\": { \"type\": \"number\" },\n          \"certificateId\": { \"type\": \"string\", \"nullable\": true },\n          \"completed\": { \"type\": \"boolean\" },\n          \"mentor\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/ResumeCourseMentor\" }] },\n          \"locationName\": { \"type\": \"string\" }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"fullName\",\n          \"rank\",\n          \"totalScore\",\n          \"certificateId\",\n          \"completed\",\n          \"mentor\",\n          \"locationName\"\n        ]\n      },\n      \"GratitudeDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"user\": { \"$ref\": \"#/components/schemas/PersonDto\" },\n          \"id\": { \"type\": \"number\" },\n          \"badgeId\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/BadgeEnum\" }] },\n          \"comment\": { \"type\": \"string\" },\n          \"courseId\": { \"type\": \"number\" },\n          \"date\": { \"type\": \"string\" }\n        },\n        \"required\": [\"user\", \"id\", \"badgeId\", \"comment\", \"courseId\", \"date\"]\n      },\n      \"FeedbackSoftSkill\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"value\": { \"type\": \"string\", \"enum\": [\"None\", \"Poor\", \"Fair\", \"Good\", \"Great\", \"Excellent\"] },\n          \"id\": {\n            \"type\": \"string\",\n            \"enum\": [\"skill.soft.responsible\", \"skill.soft.team-player\", \"skill.soft.communicable\"]\n          }\n        },\n        \"required\": [\"value\", \"id\"]\n      },\n      \"FeedbackCourseDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"name\": { \"type\": \"string\" }, \"id\": { \"type\": \"number\" } },\n        \"required\": [\"name\", \"id\"]\n      },\n      \"FeedbackDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"date\": { \"type\": \"string\" },\n          \"recommendation\": { \"type\": \"string\" },\n          \"englishLevel\": { \"type\": \"string\" },\n          \"recommendationComment\": { \"type\": \"string\" },\n          \"suggestions\": { \"type\": \"string\" },\n          \"softSkills\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/FeedbackSoftSkill\" } },\n          \"mentor\": { \"$ref\": \"#/components/schemas/ResumeCourseMentor\" },\n          \"course\": { \"$ref\": \"#/components/schemas/FeedbackCourseDto\" }\n        },\n        \"required\": [\n          \"date\",\n          \"recommendation\",\n          \"englishLevel\",\n          \"recommendationComment\",\n          \"suggestions\",\n          \"softSkills\",\n          \"mentor\",\n          \"course\"\n        ]\n      },\n      \"ResumeDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"uuid\": { \"type\": \"string\" },\n          \"avatarLink\": { \"type\": \"string\", \"nullable\": true },\n          \"visibleCourses\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"courses\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/ResumeCourseDto\" } },\n          \"desiredPosition\": { \"type\": \"string\", \"nullable\": true },\n          \"email\": { \"type\": \"string\", \"nullable\": true },\n          \"englishLevel\": {\n            \"type\": \"string\",\n            \"enum\": [\"unknown\", \"a0\", \"a1\", \"a2\", \"b1\", \"b2\", \"c1\", \"c2\"],\n            \"nullable\": true\n          },\n          \"expires\": { \"type\": \"number\", \"nullable\": true },\n          \"gratitudes\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/GratitudeDto\" } },\n          \"feedbacks\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/FeedbackDto\" } },\n          \"fullTime\": { \"type\": \"boolean\" },\n          \"githubUsername\": { \"type\": \"string\", \"nullable\": true },\n          \"id\": { \"type\": \"number\" },\n          \"linkedin\": { \"type\": \"string\", \"nullable\": true },\n          \"locations\": { \"type\": \"string\", \"nullable\": true },\n          \"militaryService\": { \"type\": \"string\", \"enum\": [\"served\", \"liable\", \"notLiable\"], \"nullable\": true },\n          \"name\": { \"type\": \"string\", \"nullable\": true },\n          \"notes\": { \"type\": \"string\", \"nullable\": true },\n          \"phone\": { \"type\": \"string\", \"nullable\": true },\n          \"selfIntroLink\": { \"type\": \"string\", \"nullable\": true },\n          \"skype\": { \"type\": \"string\", \"nullable\": true },\n          \"startFrom\": { \"type\": \"string\", \"nullable\": true },\n          \"telegram\": { \"type\": \"string\", \"nullable\": true },\n          \"website\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\n          \"uuid\",\n          \"avatarLink\",\n          \"visibleCourses\",\n          \"courses\",\n          \"desiredPosition\",\n          \"email\",\n          \"englishLevel\",\n          \"expires\",\n          \"gratitudes\",\n          \"feedbacks\",\n          \"fullTime\",\n          \"githubUsername\",\n          \"id\",\n          \"linkedin\",\n          \"locations\",\n          \"militaryService\",\n          \"name\",\n          \"notes\",\n          \"phone\",\n          \"selfIntroLink\",\n          \"skype\",\n          \"startFrom\",\n          \"telegram\",\n          \"website\"\n        ]\n      },\n      \"FormDataDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"avatarLink\": { \"type\": \"string\", \"nullable\": true },\n          \"visibleCourses\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"desiredPosition\": { \"type\": \"string\", \"nullable\": true },\n          \"email\": { \"type\": \"string\", \"nullable\": true },\n          \"englishLevel\": {\n            \"type\": \"string\",\n            \"enum\": [\"unknown\", \"a0\", \"a1\", \"a2\", \"b1\", \"b2\", \"c1\", \"c2\"],\n            \"nullable\": true\n          },\n          \"fullTime\": { \"type\": \"boolean\" },\n          \"githubUsername\": { \"type\": \"string\", \"nullable\": true },\n          \"linkedin\": { \"type\": \"string\", \"nullable\": true },\n          \"locations\": { \"type\": \"string\", \"nullable\": true },\n          \"militaryService\": { \"type\": \"string\", \"enum\": [\"served\", \"liable\", \"notLiable\"], \"nullable\": true },\n          \"name\": { \"type\": \"string\", \"nullable\": true },\n          \"notes\": { \"type\": \"string\", \"nullable\": true },\n          \"phone\": { \"type\": \"string\", \"nullable\": true },\n          \"selfIntroLink\": { \"type\": \"string\", \"nullable\": true },\n          \"skype\": { \"type\": \"string\", \"nullable\": true },\n          \"startFrom\": { \"type\": \"string\", \"nullable\": true },\n          \"telegram\": { \"type\": \"string\", \"nullable\": true },\n          \"website\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\n          \"avatarLink\",\n          \"visibleCourses\",\n          \"desiredPosition\",\n          \"email\",\n          \"englishLevel\",\n          \"fullTime\",\n          \"githubUsername\",\n          \"linkedin\",\n          \"locations\",\n          \"militaryService\",\n          \"name\",\n          \"notes\",\n          \"phone\",\n          \"selfIntroLink\",\n          \"skype\",\n          \"startFrom\",\n          \"telegram\",\n          \"website\"\n        ]\n      },\n      \"Resume\": { \"type\": \"object\", \"properties\": {} },\n      \"ConsentDto\": { \"type\": \"object\", \"properties\": { \"consent\": { \"type\": \"boolean\" } }, \"required\": [\"consent\"] },\n      \"GiveConsentDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"consent\": { \"type\": \"boolean\" }, \"expires\": { \"type\": \"number\" } },\n        \"required\": [\"consent\", \"expires\"]\n      },\n      \"StatusDto\": { \"type\": \"object\", \"properties\": { \"expires\": { \"type\": \"number\" } }, \"required\": [\"expires\"] },\n      \"VisibilityDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"isHidden\": { \"type\": \"boolean\" } },\n        \"required\": [\"isHidden\"]\n      },\n      \"ApplicantResumeDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"uuid\": { \"type\": \"string\" },\n          \"avatarLink\": { \"type\": \"string\", \"nullable\": true },\n          \"visibleCourses\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"desiredPosition\": { \"type\": \"string\", \"nullable\": true },\n          \"email\": { \"type\": \"string\", \"nullable\": true },\n          \"englishLevel\": {\n            \"type\": \"string\",\n            \"enum\": [\"unknown\", \"a0\", \"a1\", \"a2\", \"b1\", \"b2\", \"c1\", \"c2\"],\n            \"nullable\": true\n          },\n          \"expires\": { \"type\": \"number\", \"nullable\": true },\n          \"fullTime\": { \"type\": \"boolean\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"linkedin\": { \"type\": \"string\", \"nullable\": true },\n          \"locations\": { \"type\": \"string\", \"nullable\": true },\n          \"militaryService\": { \"type\": \"string\", \"enum\": [\"served\", \"liable\", \"notLiable\"], \"nullable\": true },\n          \"name\": { \"type\": \"string\", \"nullable\": true },\n          \"notes\": { \"type\": \"string\", \"nullable\": true },\n          \"phone\": { \"type\": \"string\", \"nullable\": true },\n          \"selfIntroLink\": { \"type\": \"string\", \"nullable\": true },\n          \"skype\": { \"type\": \"string\", \"nullable\": true },\n          \"startFrom\": { \"type\": \"string\", \"nullable\": true },\n          \"telegram\": { \"type\": \"string\", \"nullable\": true },\n          \"website\": { \"type\": \"string\", \"nullable\": true }\n        },\n        \"required\": [\n          \"uuid\",\n          \"avatarLink\",\n          \"visibleCourses\",\n          \"desiredPosition\",\n          \"email\",\n          \"englishLevel\",\n          \"expires\",\n          \"fullTime\",\n          \"githubId\",\n          \"id\",\n          \"linkedin\",\n          \"locations\",\n          \"militaryService\",\n          \"name\",\n          \"notes\",\n          \"phone\",\n          \"selfIntroLink\",\n          \"skype\",\n          \"startFrom\",\n          \"telegram\",\n          \"website\"\n        ]\n      },\n      \"CreateUserGroupDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"users\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\"taskOwner\", \"manager\", \"supervisor\", \"student\", \"mentor\", \"dementor\", \"activist\"]\n            }\n          }\n        },\n        \"required\": [\"name\", \"users\", \"roles\"]\n      },\n      \"UserGroupDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"users\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/UserDto\" } },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\"taskOwner\", \"manager\", \"supervisor\", \"student\", \"mentor\", \"dementor\", \"activist\"]\n            }\n          }\n        },\n        \"required\": [\"id\", \"name\", \"users\", \"roles\"]\n      },\n      \"UpdateUserGroupDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"users\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"roles\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\",\n              \"enum\": [\"taskOwner\", \"manager\", \"supervisor\", \"student\", \"mentor\", \"dementor\", \"activist\"]\n            }\n          }\n        },\n        \"required\": [\"name\", \"users\", \"roles\"]\n      },\n      \"CheckScheduleChangesDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"lastHours\": { \"type\": \"number\" } },\n        \"required\": [\"lastHours\"]\n      },\n      \"CreateGratitudeDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"userIds\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"courseId\": { \"type\": \"number\" },\n          \"comment\": { \"type\": \"string\" },\n          \"badgeId\": { \"type\": \"string\" }\n        },\n        \"required\": [\"userIds\", \"courseId\", \"comment\", \"badgeId\"]\n      },\n      \"BadgeEnum\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"Congratulations\",\n          \"Expert_help\",\n          \"Great_speaker\",\n          \"Good_job\",\n          \"Helping_hand\",\n          \"Hero\",\n          \"Thank_you\",\n          \"Outstanding_work\",\n          \"Top_performer\",\n          \"Job_Offer\",\n          \"RS_activist\",\n          \"Jury_Team\",\n          \"Mentor\",\n          \"Contributor\",\n          \"Coordinator\",\n          \"Thanks\"\n        ]\n      },\n      \"BadgeDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"id\": { \"allOf\": [{ \"$ref\": \"#/components/schemas/BadgeEnum\" }] }\n        },\n        \"required\": [\"name\", \"id\"]\n      },\n      \"HeroesRadarBadgeDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"string\" },\n          \"badgeId\": { \"type\": \"string\" },\n          \"comment\": { \"type\": \"string\" },\n          \"date\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"badgeId\", \"comment\", \"date\"]\n      },\n      \"HeroRadarDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"githubId\": { \"type\": \"string\" },\n          \"name\": { \"type\": \"string\" },\n          \"rank\": { \"type\": \"number\" },\n          \"total\": { \"type\": \"number\" },\n          \"badges\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/HeroesRadarBadgeDto\" } }\n        },\n        \"required\": [\"githubId\", \"name\", \"rank\", \"total\", \"badges\"]\n      },\n      \"HeroesRadarDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"content\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/HeroRadarDto\" } },\n          \"pagination\": { \"$ref\": \"#/components/schemas/PaginationMetaDto\" }\n        },\n        \"required\": [\"content\", \"pagination\"]\n      },\n      \"CountryDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"countryName\": { \"type\": \"string\" } },\n        \"required\": [\"countryName\"]\n      },\n      \"EventDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"descriptionUrl\": { \"type\": \"string\", \"nullable\": true },\n          \"description\": { \"type\": \"string\", \"nullable\": true },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"lecture_online\",\n              \"lecture_offline\",\n              \"lecture_mixed\",\n              \"lecture_self_study\",\n              \"warmup\",\n              \"info\",\n              \"workshop\",\n              \"meetup\",\n              \"cross_check_deadline\",\n              \"webinar\",\n              \"special\"\n            ]\n          },\n          \"discipline\": { \"nullable\": true, \"allOf\": [{ \"$ref\": \"#/components/schemas/IdNameDto\" }] }\n        },\n        \"required\": [\"id\", \"name\", \"descriptionUrl\", \"description\", \"type\", \"discipline\"]\n      },\n      \"CreateEventDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"type\": { \"type\": \"string\" },\n          \"disciplineId\": { \"type\": \"number\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" }\n        },\n        \"required\": [\"name\", \"type\", \"disciplineId\", \"descriptionUrl\", \"description\"]\n      },\n      \"UpdateEventDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"type\": { \"type\": \"string\" },\n          \"disciplineId\": { \"type\": \"number\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" }\n        },\n        \"required\": [\"name\", \"type\", \"disciplineId\", \"descriptionUrl\", \"description\"]\n      },\n      \"CreateTaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"attributes\": { \"type\": \"object\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"githubRepoName\": { \"type\": \"string\" },\n          \"sourceGithubRepoUrl\": { \"type\": \"string\" },\n          \"disciplineId\": { \"type\": \"number\" },\n          \"githubPrRequired\": { \"type\": \"boolean\" },\n          \"type\": { \"type\": \"string\" },\n          \"skills\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"tags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n        },\n        \"required\": [\n          \"name\",\n          \"attributes\",\n          \"descriptionUrl\",\n          \"description\",\n          \"githubRepoName\",\n          \"sourceGithubRepoUrl\",\n          \"disciplineId\",\n          \"githubPrRequired\",\n          \"type\",\n          \"skills\",\n          \"tags\"\n        ]\n      },\n      \"UsedCourseDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"name\": { \"type\": \"string\" }, \"isActive\": { \"type\": \"boolean\" } },\n        \"required\": [\"name\", \"isActive\"]\n      },\n      \"TaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"jstask\",\n              \"kotlintask\",\n              \"objctask\",\n              \"htmltask\",\n              \"ipynb\",\n              \"selfeducation\",\n              \"codewars\",\n              \"test\",\n              \"codejam\",\n              \"interview\",\n              \"stage-interview\",\n              \"cv:html\",\n              \"cv:markdown\"\n            ]\n          },\n          \"name\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"githubRepoName\": { \"type\": \"string\" },\n          \"sourceGithubRepoUrl\": { \"type\": \"string\" },\n          \"discipline\": { \"$ref\": \"#/components/schemas/IdNameDto\" },\n          \"githubPrRequired\": { \"type\": \"boolean\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" },\n          \"tags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"skills\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"attributes\": { \"type\": \"object\" },\n          \"courses\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/UsedCourseDto\" } }\n        },\n        \"required\": [\n          \"type\",\n          \"name\",\n          \"id\",\n          \"descriptionUrl\",\n          \"description\",\n          \"githubRepoName\",\n          \"sourceGithubRepoUrl\",\n          \"discipline\",\n          \"githubPrRequired\",\n          \"createdDate\",\n          \"updatedDate\",\n          \"tags\",\n          \"skills\",\n          \"attributes\",\n          \"courses\"\n        ]\n      },\n      \"UpdateTaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"name\": { \"type\": \"string\" },\n          \"attributes\": { \"type\": \"object\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"githubRepoName\": { \"type\": \"string\" },\n          \"sourceGithubRepoUrl\": { \"type\": \"string\" },\n          \"disciplineId\": { \"type\": \"number\" },\n          \"githubPrRequired\": { \"type\": \"boolean\" },\n          \"type\": { \"type\": \"string\" },\n          \"skills\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"tags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n        },\n        \"required\": [\n          \"name\",\n          \"attributes\",\n          \"descriptionUrl\",\n          \"description\",\n          \"githubRepoName\",\n          \"sourceGithubRepoUrl\",\n          \"disciplineId\",\n          \"githubPrRequired\",\n          \"type\",\n          \"skills\",\n          \"tags\"\n        ]\n      },\n      \"CriteriaDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"max\": { \"type\": \"number\" },\n          \"type\": { \"type\": \"string\", \"enum\": [\"title\", \"subtask\", \"penalty\"] },\n          \"text\": { \"type\": \"string\" },\n          \"key\": { \"type\": \"string\" },\n          \"index\": { \"type\": \"number\" }\n        },\n        \"required\": [\"type\", \"text\", \"key\", \"index\"]\n      },\n      \"TaskCriteriaDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"criteria\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/CriteriaDto\" } } },\n        \"required\": [\"criteria\"]\n      },\n      \"CreatePromptDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": { \"type\": \"string\" },\n          \"text\": { \"type\": \"string\" },\n          \"temperature\": { \"type\": \"number\" }\n        },\n        \"required\": [\"type\", \"text\", \"temperature\"]\n      },\n      \"PromptDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"type\": { \"type\": \"string\" },\n          \"text\": { \"type\": \"string\" },\n          \"temperature\": { \"type\": \"number\" }\n        },\n        \"required\": [\"id\", \"type\", \"text\", \"temperature\"]\n      },\n      \"UpdatePromptDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"temperature\": { \"type\": \"number\" },\n          \"type\": { \"type\": \"string\" },\n          \"text\": { \"type\": \"string\" }\n        },\n        \"required\": [\"temperature\", \"type\", \"text\"]\n      },\n      \"BasicAutoTestTaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"name\": { \"type\": \"string\" },\n          \"maxAttemptsNumber\": { \"type\": \"number\", \"nullable\": true },\n          \"numberOfQuestions\": { \"type\": \"number\", \"nullable\": true },\n          \"strictAttemptsMode\": { \"type\": \"number\", \"nullable\": true },\n          \"thresholdPercentage\": { \"type\": \"number\", \"nullable\": true }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"maxAttemptsNumber\",\n          \"numberOfQuestions\",\n          \"strictAttemptsMode\",\n          \"thresholdPercentage\"\n        ]\n      },\n      \"QuestionDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"question\": { \"type\": \"string\" },\n          \"multiple\": { \"type\": \"boolean\" },\n          \"answers\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"questionImage\": { \"type\": \"string\" },\n          \"answersType\": { \"type\": \"string\" }\n        },\n        \"required\": [\"question\", \"multiple\", \"answers\"]\n      },\n      \"PublicAttributesDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"maxAttemptsNumber\": { \"type\": \"number\" },\n          \"numberOfQuestions\": { \"type\": \"number\" },\n          \"strictAttemptsMode\": { \"type\": \"boolean\" },\n          \"tresholdPercentage\": { \"type\": \"number\" },\n          \"questions\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/QuestionDto\" } }\n        },\n        \"required\": [\"maxAttemptsNumber\", \"numberOfQuestions\", \"strictAttemptsMode\", \"tresholdPercentage\", \"questions\"]\n      },\n      \"AutoTestAttributesDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"public\": { \"$ref\": \"#/components/schemas/PublicAttributesDto\" },\n          \"answers\": { \"type\": \"array\", \"items\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } } }\n        },\n        \"required\": [\"public\", \"answers\"]\n      },\n      \"AutoTestTaskDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"jstask\",\n              \"kotlintask\",\n              \"objctask\",\n              \"htmltask\",\n              \"ipynb\",\n              \"selfeducation\",\n              \"codewars\",\n              \"test\",\n              \"codejam\",\n              \"interview\",\n              \"stage-interview\",\n              \"cv:html\",\n              \"cv:markdown\"\n            ]\n          },\n          \"name\": { \"type\": \"string\" },\n          \"id\": { \"type\": \"number\" },\n          \"descriptionUrl\": { \"type\": \"string\" },\n          \"description\": { \"type\": \"string\" },\n          \"githubRepoName\": { \"type\": \"string\" },\n          \"sourceGithubRepoUrl\": { \"type\": \"string\" },\n          \"discipline\": { \"$ref\": \"#/components/schemas/IdNameDto\" },\n          \"githubPrRequired\": { \"type\": \"boolean\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" },\n          \"tags\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"skills\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"attributes\": { \"$ref\": \"#/components/schemas/AutoTestAttributesDto\" },\n          \"courses\": { \"type\": \"array\", \"items\": { \"$ref\": \"#/components/schemas/UsedCourseDto\" } }\n        },\n        \"required\": [\n          \"type\",\n          \"name\",\n          \"id\",\n          \"descriptionUrl\",\n          \"description\",\n          \"githubRepoName\",\n          \"sourceGithubRepoUrl\",\n          \"discipline\",\n          \"githubPrRequired\",\n          \"createdDate\",\n          \"updatedDate\",\n          \"tags\",\n          \"skills\",\n          \"attributes\",\n          \"courses\"\n        ]\n      },\n      \"CreateContributorDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"description\": { \"type\": \"string\" }, \"userId\": { \"type\": \"number\" } },\n        \"required\": [\"description\", \"userId\"]\n      },\n      \"ContributorUserDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"firstName\": { \"type\": \"string\" },\n          \"lastName\": { \"type\": \"string\" }\n        },\n        \"required\": [\"id\", \"githubId\", \"firstName\", \"lastName\"]\n      },\n      \"ContributorDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"description\": { \"type\": \"string\" },\n          \"user\": { \"$ref\": \"#/components/schemas/ContributorUserDto\" },\n          \"id\": { \"type\": \"number\" },\n          \"createdDate\": { \"type\": \"string\" },\n          \"updatedDate\": { \"type\": \"string\" }\n        },\n        \"required\": [\"description\", \"user\", \"id\", \"createdDate\", \"updatedDate\"]\n      },\n      \"UpdateContributorDto\": {\n        \"type\": \"object\",\n        \"properties\": { \"description\": { \"type\": \"string\" }, \"userId\": { \"type\": \"number\" } }\n      },\n      \"MentorCourseStatsDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"courseName\": { \"type\": \"string\", \"description\": \"Name of the course\" },\n          \"studentsCount\": { \"type\": \"number\", \"description\": \"Number of certified students mentored in this course\" }\n        },\n        \"required\": [\"courseName\", \"studentsCount\"]\n      },\n      \"TopMentorDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"rank\": { \"type\": \"number\", \"description\": \"Position in the mentors ranking\" },\n          \"githubId\": { \"type\": \"string\", \"description\": \"GitHub username\" },\n          \"name\": { \"type\": \"string\", \"description\": \"Full name of the mentor\" },\n          \"totalStudents\": { \"type\": \"number\", \"description\": \"Total number of certified students mentored\" },\n          \"totalGratitudes\": { \"type\": \"number\", \"description\": \"Total number of gratitudes received\" },\n          \"courseStats\": {\n            \"description\": \"Student counts per course\",\n            \"type\": \"array\",\n            \"items\": { \"$ref\": \"#/components/schemas/MentorCourseStatsDto\" }\n          }\n        },\n        \"required\": [\"rank\", \"githubId\", \"name\", \"totalStudents\", \"totalGratitudes\", \"courseStats\"]\n      },\n      \"AuthUserDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"roles\": { \"type\": \"object\", \"additionalProperties\": { \"type\": \"string\", \"enum\": [\"mentor\", \"student\"] } },\n          \"isAdmin\": { \"type\": \"boolean\" },\n          \"isHirer\": { \"type\": \"boolean\" },\n          \"appRoles\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n          \"courses\": {\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"roles\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"string\",\n                    \"enum\": [\"manager\", \"supervisor\", \"student\", \"mentor\", \"dementor\", \"activist\"]\n                  }\n                }\n              },\n              \"required\": [\"roles\"]\n            }\n          }\n        },\n        \"required\": [\"id\", \"githubId\", \"roles\", \"isAdmin\", \"isHirer\", \"appRoles\", \"courses\"]\n      },\n      \"DevtoolsUserDto\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": { \"type\": \"number\" },\n          \"githubId\": { \"type\": \"string\" },\n          \"mentor\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } },\n          \"student\": { \"type\": \"array\", \"items\": { \"type\": \"number\" } }\n        },\n        \"required\": [\"id\", \"githubId\", \"mentor\", \"student\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "nestjs/src/tasks/dto/create-task.dto.ts",
    "content": "import { TaskType } from '@entities/task';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class CreateTaskDto {\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty()\n  @IsOptional()\n  attributes: Record<string, any>;\n\n  @IsString()\n  @ApiProperty()\n  descriptionUrl: string;\n\n  @IsString()\n  @ApiProperty()\n  @IsOptional()\n  description: string;\n\n  @IsString()\n  @ApiProperty()\n  @IsOptional()\n  githubRepoName: string;\n\n  @IsString()\n  @ApiProperty()\n  @IsOptional()\n  sourceGithubRepoUrl: string;\n\n  @IsNumber()\n  @ApiProperty()\n  disciplineId: number;\n\n  @IsBoolean()\n  @ApiProperty()\n  githubPrRequired: boolean;\n\n  @IsNotEmpty()\n  @IsEnum(TaskType)\n  @ApiProperty()\n  type: TaskType;\n\n  @IsArray()\n  @ApiProperty()\n  @IsOptional()\n  skills: string[];\n\n  @IsArray()\n  @ApiProperty()\n  @IsOptional()\n  tags: string[];\n}\n"
  },
  {
    "path": "nestjs/src/tasks/dto/index.ts",
    "content": "export * from './create-task.dto';\nexport * from './update-task.dto';\nexport * from './task.dto';\n"
  },
  {
    "path": "nestjs/src/tasks/dto/task.dto.ts",
    "content": "import { Task, TaskType } from '@entities/task';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { uniqBy } from 'lodash';\nimport { IdNameDto } from 'src/core/dto';\nimport { UsedCourseDto } from 'src/courses/dto/used-course.dto';\n\nexport class TaskDto {\n  constructor(task: Task) {\n    this.id = task.id;\n    this.name = task.name;\n    this.type = task.type;\n    this.descriptionUrl = task.descriptionUrl;\n    this.description = task.description;\n    this.githubRepoName = task.githubRepoName;\n    this.sourceGithubRepoUrl = task.sourceGithubRepoUrl;\n    this.discipline = task.discipline ? new IdNameDto(task.discipline) : null;\n    this.courses = task.courseTasks\n      ? uniqBy(\n          task.courseTasks\n            .filter(task => !task.disabled)\n            .map(({ course }) => new UsedCourseDto({ name: course.name, isActive: !course.completed })),\n          course => course.name,\n        ).sort((a, b) => {\n          if (a.isActive === b.isActive) {\n            return a.name.localeCompare(b.name);\n          }\n          return Number(b.isActive) - Number(a.isActive);\n        })\n      : [];\n    this.githubPrRequired = task.githubPrRequired;\n    this.tags = task.tags;\n    this.skills = task.skills;\n    this.attributes = task.attributes;\n    this.createdDate = task.createdDate;\n    this.updatedDate = task.updatedDate;\n  }\n\n  @ApiProperty({ enum: TaskType })\n  public type: TaskType;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public descriptionUrl: string;\n\n  @ApiProperty()\n  public description: string;\n\n  @ApiProperty()\n  public githubRepoName: string;\n\n  @ApiProperty()\n  public sourceGithubRepoUrl: string;\n\n  @ApiProperty({ type: IdNameDto })\n  public discipline: IdNameDto | null;\n\n  @ApiProperty()\n  public githubPrRequired: boolean;\n\n  @ApiProperty()\n  public createdDate: string;\n\n  @ApiProperty()\n  public updatedDate: string;\n\n  @ApiProperty()\n  public tags: string[];\n\n  @ApiProperty()\n  public skills: string[];\n\n  @ApiProperty()\n  public attributes: Record<string, unknown>;\n\n  @ApiProperty({ type: [UsedCourseDto] })\n  public courses: UsedCourseDto[];\n}\n"
  },
  {
    "path": "nestjs/src/tasks/dto/update-task.dto.ts",
    "content": "import { CreateTaskDto } from './create-task.dto';\n\nexport class UpdateTaskDto extends CreateTaskDto {}\n"
  },
  {
    "path": "nestjs/src/tasks/tasks-criteria/dto/criteria.dto.ts",
    "content": "import { IsNotEmpty, IsNumber, IsOptional, IsString, IsEnum } from 'class-validator';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { CrossCheckCriteriaType } from '@entities/taskCriteria';\n\nexport class CriteriaDto {\n  @IsNumber()\n  @IsOptional()\n  @ApiProperty({ required: false })\n  max?: number;\n\n  @IsNotEmpty()\n  @IsEnum(CrossCheckCriteriaType)\n  @ApiProperty({ enum: CrossCheckCriteriaType })\n  type: CrossCheckCriteriaType;\n\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  text: string;\n\n  @IsString()\n  @ApiProperty()\n  key: string;\n\n  @IsNumber()\n  @ApiProperty()\n  index: number;\n}\n"
  },
  {
    "path": "nestjs/src/tasks/tasks-criteria/dto/task-criteria.dto.ts",
    "content": "import { IsArray, IsNotEmpty } from 'class-validator';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { CriteriaDto } from './criteria.dto';\n\nexport class TaskCriteriaDto {\n  @IsNotEmpty()\n  @IsArray()\n  @ApiProperty({ type: [CriteriaDto] })\n  criteria: CriteriaDto[];\n}\n"
  },
  {
    "path": "nestjs/src/tasks/tasks-criteria/index.ts",
    "content": "export { TasksCriteriaController } from './tasks-criteria.controller';\nexport { TasksCriteriaService } from './tasks-criteria.service';\n"
  },
  {
    "path": "nestjs/src/tasks/tasks-criteria/tasks-criteria.controller.ts",
    "content": "import { Controller, Get, Body, Patch, Param, UseGuards, Post } from '@nestjs/common';\nimport { ApiBody, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\n\nimport { TasksCriteriaService } from './tasks-criteria.service';\nimport { TaskCriteriaDto } from './dto/task-criteria.dto';\nimport { DefaultGuard, RoleGuard, RequiredRoles, Role, CourseRole } from '../../auth';\n\n@Controller('tasks/:taskId/criteria')\n@ApiTags('tasks-criteria')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class TasksCriteriaController {\n  constructor(private readonly taskCriteriaService: TasksCriteriaService) {}\n\n  @Get()\n  @ApiOperation({ operationId: 'getTaskCriteria' })\n  @ApiOkResponse({ type: TaskCriteriaDto })\n  async get(@Param('taskId') taskId: number) {\n    const data = await this.taskCriteriaService.getCriteria(taskId);\n    return data;\n  }\n\n  @Post()\n  @ApiOperation({ operationId: 'createTaskCriteria' })\n  @ApiOkResponse({ type: TaskCriteriaDto })\n  @ApiBody({ type: TaskCriteriaDto })\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  async create(@Param('taskId') taskId: number, @Body() taskCriteriaDto: TaskCriteriaDto) {\n    const data = await this.taskCriteriaService.createCriteria(taskId, taskCriteriaDto.criteria);\n    return data;\n  }\n\n  @Patch()\n  @ApiOperation({ operationId: 'updateTaskCriteria' })\n  @ApiOkResponse({ type: TaskCriteriaDto })\n  @ApiBody({ type: TaskCriteriaDto })\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  async update(@Param('taskId') taskId: number, @Body() taskCriteriaDto: TaskCriteriaDto) {\n    const data = await this.taskCriteriaService.updateCriteria(taskId, taskCriteriaDto);\n    return data;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/tasks/tasks-criteria/tasks-criteria.service.ts",
    "content": "import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { TaskCriteria } from '@entities/taskCriteria';\nimport { Repository } from 'typeorm';\nimport { Task } from '@entities/task';\n\nimport { TaskCriteriaDto } from './dto/task-criteria.dto';\nimport { CriteriaDto } from './dto/criteria.dto';\n\n@Injectable()\nexport class TasksCriteriaService {\n  constructor(\n    @InjectRepository(TaskCriteria)\n    private readonly taskCriteriaRepository: Repository<TaskCriteria>,\n    @InjectRepository(Task)\n    private readonly taskRepository: Repository<Task>,\n  ) {}\n\n  async createCriteria(taskId: number, criteria: CriteriaDto[]) {\n    const { criteria: existingCriteria } = await this.getCriteria(taskId);\n\n    if (existingCriteria) {\n      throw new BadRequestException('Criteria already exists');\n    }\n\n    const task = await this.taskRepository.findOneBy({ id: taskId });\n    if (!task) {\n      throw new NotFoundException('Task does not exist');\n    }\n\n    const taskCriteria = new TaskCriteria(taskId, criteria);\n    await this.taskCriteriaRepository.save(taskCriteria);\n    await this.taskRepository.update({ id: taskId }, { criteria: taskCriteria });\n\n    return this.getCriteria(taskId);\n  }\n\n  async getCriteria(taskId: number): Promise<TaskCriteriaDto> {\n    const data = await this.taskCriteriaRepository.findOne({ where: { taskId } });\n    return { criteria: data?.criteria } as TaskCriteriaDto;\n  }\n\n  async updateCriteria(taskId: number, taskCriteriaDto: TaskCriteriaDto) {\n    const { criteria: existingCriteria } = await this.getCriteria(taskId);\n\n    if (!existingCriteria) {\n      throw new NotFoundException('Criteria does not exist');\n    }\n\n    await this.taskCriteriaRepository.update({ taskId }, { criteria: taskCriteriaDto.criteria });\n\n    return this.getCriteria(taskId);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/tasks/tasks.controller.ts",
    "content": "import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { TasksService } from './tasks.service';\nimport { CreateTaskDto, UpdateTaskDto, TaskDto } from './dto';\n\n@Controller('tasks')\n@ApiTags('tasks')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class TasksController {\n  constructor(private readonly service: TasksService) {}\n\n  @Post('/')\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'createTask' })\n  @ApiOkResponse({ type: TaskDto })\n  public async create(@Body() dto: CreateTaskDto) {\n    const data = await this.service.create(dto);\n    return new TaskDto(data);\n  }\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getTasks' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOkResponse({ type: [TaskDto] })\n  public async getAll() {\n    const items = await this.service.getAll();\n    return items.map(item => new TaskDto(item));\n  }\n\n  @Delete('/:id')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'deleteTask' })\n  public async delete(@Param('id', ParseIntPipe) id: number) {\n    return this.service.delete(id);\n  }\n\n  @Patch('/:id')\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOperation({ operationId: 'updateTask' })\n  @ApiOkResponse({ type: TaskDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateTaskDto) {\n    const data = await this.service.update(id, dto);\n    return new TaskDto(data);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/tasks/tasks.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { Task } from '@entities/task';\nimport { TasksController } from './tasks.controller';\nimport { TasksService } from './tasks.service';\nimport { TaskCriteria } from '@entities/taskCriteria';\nimport { TasksCriteriaController, TasksCriteriaService } from './tasks-criteria';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([Task, TaskCriteria])],\n  controllers: [TasksController, TasksCriteriaController],\n  providers: [TasksService, TasksCriteriaService],\n})\nexport class TasksModule {}\n"
  },
  {
    "path": "nestjs/src/tasks/tasks.service.ts",
    "content": "import { Task } from '@entities/task';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Repository } from 'typeorm';\nimport { CreateTaskDto, UpdateTaskDto } from './dto';\n\n@Injectable()\nexport class TasksService {\n  constructor(\n    @InjectRepository(Task)\n    private repository: Repository<Task>,\n  ) {}\n\n  public async getAll() {\n    return this.repository.find({\n      relations: {\n        discipline: true,\n        courseTasks: { course: true },\n      },\n      order: {\n        updatedDate: 'DESC',\n      },\n    });\n  }\n\n  public async getById(id: number) {\n    return this.repository.findOneBy({ id });\n  }\n\n  public async create(data: CreateTaskDto) {\n    const { id } = await this.repository.save(data);\n    return this.repository.findOneByOrFail({ id });\n  }\n\n  public async update(id: number, data: UpdateTaskDto) {\n    await this.repository.update(id, data);\n    return this.repository.findOneByOrFail({ id });\n  }\n\n  public async delete(id: number): Promise<void> {\n    await this.repository.softDelete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/user-groups/dto/create-user-group.dto.ts",
    "content": "import { CourseRole } from '@entities/session';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsNotEmpty, IsString } from 'class-validator';\n\nexport class CreateUserGroupDto {\n  @IsNotEmpty()\n  @IsString()\n  @ApiProperty()\n  name: string;\n\n  @ApiProperty({ type: [Number] })\n  @IsArray()\n  users: number[];\n\n  @ApiProperty({ enum: CourseRole, isArray: true })\n  @IsArray()\n  roles: CourseRole[];\n}\n"
  },
  {
    "path": "nestjs/src/user-groups/dto/index.ts",
    "content": "export * from './user-group.dto';\nexport * from './create-user-group.dto';\nexport * from './update-user-group.dto';\n"
  },
  {
    "path": "nestjs/src/user-groups/dto/update-user-group.dto.ts",
    "content": "import { CreateUserGroupDto } from './create-user-group.dto';\n\nexport class UpdateUserGroupDto extends CreateUserGroupDto {}\n"
  },
  {
    "path": "nestjs/src/user-groups/dto/user-group.dto.ts",
    "content": "import { CourseRole } from '@entities/session';\nimport { UserGroup } from '@entities/userGroup';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsNumber, IsString } from 'class-validator';\nimport { UserDto } from 'src/users/dto';\n\nexport class UserGroupDto {\n  constructor(userGroup: Omit<UserGroup, 'createdDate' | 'updatedDate' | 'users'> & { users: UserDto[] }) {\n    this.id = userGroup.id;\n    this.name = userGroup.name;\n    this.users = userGroup.users;\n    this.roles = userGroup.roles;\n  }\n\n  @ApiProperty()\n  @IsNumber()\n  id: number;\n\n  @ApiProperty()\n  @IsString()\n  name: string;\n\n  @ApiProperty({ type: [UserDto] })\n  @IsArray()\n  users: UserDto[];\n\n  @ApiProperty({ enum: CourseRole, isArray: true })\n  @IsArray()\n  roles: CourseRole[];\n}\n"
  },
  {
    "path": "nestjs/src/user-groups/index.ts",
    "content": ""
  },
  {
    "path": "nestjs/src/user-groups/user-groups.controller.ts",
    "content": "import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Put, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { DefaultGuard, RequiredRoles, Role, RoleGuard } from 'src/auth';\nimport { CreateUserGroupDto, UpdateUserGroupDto } from './dto';\nimport { UserGroupDto } from './dto/user-group.dto';\nimport { UserGroupsService } from './user-groups.service';\n\n@Controller('user-group')\n@ApiTags('user-group')\n@RequiredRoles([Role.Admin])\n@UseGuards(DefaultGuard, RoleGuard)\nexport class UserGroupsController {\n  constructor(private readonly service: UserGroupsService) {}\n\n  @Post()\n  @ApiOperation({ operationId: 'createUserGroup' })\n  @ApiOkResponse({ type: UserGroupDto })\n  public async create(@Body() dto: CreateUserGroupDto) {\n    const data = await this.service.create(dto);\n    return new UserGroupDto(data);\n  }\n\n  @Get()\n  @ApiOperation({ operationId: 'getUserGroups' })\n  @ApiOkResponse({ type: [UserGroupDto] })\n  public async getAll() {\n    const items = await this.service.getAll();\n    return items.map(item => new UserGroupDto(item));\n  }\n\n  @Put(':id')\n  @ApiOperation({ operationId: 'updateUserGroup' })\n  @ApiOkResponse({ type: UserGroupDto })\n  public async update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateUserGroupDto) {\n    const data = await this.service.update(id, dto);\n    return new UserGroupDto(data);\n  }\n\n  @Delete(':id')\n  @ApiOperation({ operationId: 'deleteUserGroup' })\n  @ApiOkResponse({ type: UserGroupDto })\n  public async delete(@Param('id', ParseIntPipe) id: number) {\n    return this.service.delete(id);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/user-groups/user-groups.module.ts",
    "content": "import { UserGroup } from '@entities/userGroup';\nimport { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { UsersModule } from 'src/users';\nimport { UserGroupsController } from './user-groups.controller';\nimport { UserGroupsService } from './user-groups.service';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([UserGroup]), UsersModule],\n  controllers: [UserGroupsController],\n  providers: [UserGroupsService],\n})\nexport class UserGroupsModule {}\n"
  },
  {
    "path": "nestjs/src/user-groups/user-groups.service.ts",
    "content": "import { User } from '@entities/user';\nimport { UserGroup } from '@entities/userGroup';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { UsersService } from 'src/users/users.service';\nimport { Repository } from 'typeorm';\nimport { CreateUserGroupDto, UpdateUserGroupDto, UserGroupDto } from './dto';\n\n@Injectable()\nexport class UserGroupsService {\n  constructor(\n    @InjectRepository(UserGroup)\n    private repository: Repository<UserGroup>,\n    private usersService: UsersService,\n  ) {}\n\n  public async getAll() {\n    const userGroups = await this.repository.find();\n    const usersIds: number[] = [...new Set(userGroups.reduce((acc, { users }) => [...acc, ...users], [] as number[]))];\n    const users = await this.usersService.getUsersByUserIds(usersIds);\n\n    return this.formatGroups(userGroups, users);\n  }\n\n  public async create(data: CreateUserGroupDto) {\n    const userGroup = await this.repository.save(this.repository.create(data));\n    const users = await this.usersService.getUsersByUserIds(userGroup.users);\n    const group = this.formatGroup(userGroup, users);\n    return group;\n  }\n\n  public async update(id: number, data: UpdateUserGroupDto) {\n    await this.repository.update(id, data);\n\n    const userGroup = await this.repository.findOneByOrFail({ id });\n    const users = await this.usersService.getUsersByUserIds(userGroup.users);\n    const group = this.formatGroup(userGroup, users);\n    return group;\n  }\n\n  public async delete(id: number): Promise<void> {\n    await this.repository.delete(id);\n  }\n\n  private formatUser(users: User[]) {\n    return (userId: number) => {\n      const user = users.find(ur => ur.id === userId) ?? {\n        id: -1,\n        githubId: 'UNKNOWN',\n        firstName: '',\n        lastName: '',\n      };\n\n      return {\n        id: user.id,\n        githubId: user.githubId,\n        name: [user.firstName, user.lastName].filter(Boolean).join(' '),\n      };\n    };\n  }\n\n  private formatGroup(userGroup: UserGroup, users: User[]): UserGroupDto {\n    return {\n      id: userGroup.id,\n      name: userGroup.name,\n      roles: userGroup.roles,\n      users: userGroup.users.map(this.formatUser(users)),\n    };\n  }\n\n  private formatGroups(userGroups: UserGroup[], users: User[]): UserGroupDto[] {\n    const usersGroups = userGroups\n      .map(userGroup => this.formatGroup(userGroup, users))\n      .sort((a, b) => a.name.localeCompare(b.name));\n\n    return usersGroups;\n  }\n}\n"
  },
  {
    "path": "nestjs/src/users/dto/index.ts",
    "content": "export { UserSearchDto } from './user-search.dto';\nexport { UserDto } from './user.dto';\n"
  },
  {
    "path": "nestjs/src/users/dto/user-search.dto.ts",
    "content": "import { User } from '@entities/user';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { UsersService } from '../users.service';\n\nexport class CourseRecord {\n  constructor(obj: { courseName: string; id: number }) {\n    this.id = obj.id;\n    this.courseName = obj.courseName;\n  }\n\n  @ApiProperty({ type: String })\n  courseName: string;\n\n  @ApiProperty({ type: Number })\n  id: number;\n}\n\nexport class UserSearchDto {\n  constructor(user: User, isAdmin?: boolean) {\n    this.id = user.id;\n    this.name = UsersService.getFullName(user);\n    this.githubId = user.githubId;\n\n    this.primaryEmail = isAdmin ? (user.primaryEmail ?? null) : null;\n    this.contactsEmail = isAdmin ? user.contactsEmail : null;\n    this.contactsEpamEmail = isAdmin ? user.contactsEpamEmail : null;\n    this.contactsDiscord = isAdmin ? (user.discord?.username ?? null) : null;\n    this.contactsTelegram = isAdmin ? (user.contactsTelegram ?? null) : null;\n\n    this.cityName = isAdmin ? user.cityName : null;\n    this.countryName = isAdmin ? user.countryName : null;\n\n    this.mentors =\n      user.mentors?.map(mentor => ({\n        id: mentor.id,\n        courseName: mentor.course?.name,\n      })) ?? [];\n\n    this.students =\n      user.students\n        ?.filter(student => student.certificate != null)\n        .map(student => ({\n          id: student.id,\n          courseName: student.course?.name,\n        })) ?? [];\n  }\n\n  @ApiProperty()\n  public id: number;\n\n  @ApiProperty()\n  public githubId: string;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty({ nullable: true, type: String })\n  public cityName: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public countryName: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public contactsEmail: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public contactsEpamEmail: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public primaryEmail: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public contactsDiscord: string | null;\n\n  @ApiProperty({ nullable: true, type: String })\n  public contactsTelegram: string | null;\n\n  @ApiProperty({ nullable: true, type: [CourseRecord] })\n  public mentors: CourseRecord[];\n\n  @ApiProperty({ nullable: true, type: [CourseRecord] })\n  public students: CourseRecord[];\n}\n"
  },
  {
    "path": "nestjs/src/users/dto/user.dto.ts",
    "content": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsString } from 'class-validator';\n\nexport class UserDto {\n  constructor(user: { id: number; githubId: string; name: string }) {\n    this.id = user.id;\n    this.name = user.name;\n    this.githubId = user.githubId;\n  }\n\n  @ApiProperty()\n  @IsNumber()\n  public id: number;\n\n  @ApiProperty()\n  @IsString()\n  public name: string;\n\n  @ApiProperty()\n  @IsString()\n  public githubId: string;\n}\n"
  },
  {
    "path": "nestjs/src/users/index.ts",
    "content": "export * from './users.module';\n"
  },
  {
    "path": "nestjs/src/users/users.controller.ts",
    "content": "import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common';\nimport { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CourseRole, CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard } from '../auth';\nimport { UserSearchDto } from './dto';\nimport { UsersService } from './users.service';\n\n@Controller('users')\n@ApiTags('users')\n@UseGuards(DefaultGuard, RoleGuard)\nexport class UsersController {\n  constructor(private readonly usersService: UsersService) {}\n\n  @Get('/search')\n  @ApiOperation({ operationId: 'searchUsers' })\n  @RequiredRoles([Role.Admin, CourseRole.Manager])\n  @ApiOkResponse({ type: [UserSearchDto] })\n  public async searchUsers(@Req() req: CurrentRequest, @Query('query') query?: string) {\n    const users = await this.usersService.searchUsers(query);\n    return users.map(user => new UserSearchDto(user, req.user.isAdmin || req.user.isHirer));\n  }\n}\n"
  },
  {
    "path": "nestjs/src/users/users.module.ts",
    "content": "import { Module } from '@nestjs/common';\nimport { UsersService } from './users.service';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { User } from '@entities/user';\nimport { UsersController } from './users.controller';\n\n@Module({\n  imports: [TypeOrmModule.forFeature([User])],\n  providers: [UsersService],\n  controllers: [UsersController],\n  exports: [UsersService],\n})\nexport class UsersModule {}\n"
  },
  {
    "path": "nestjs/src/users/users.service.ts",
    "content": "import { User } from '@entities/user';\nimport { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { Brackets, In, Repository } from 'typeorm';\n\n@Injectable()\nexport class UsersService {\n  constructor(\n    @InjectRepository(User)\n    private userRepository: Repository<User>,\n  ) {}\n\n  public async getByGithubId(id: string) {\n    const githubId = id.toLowerCase();\n\n    const [user] = await this.userRepository.find({ where: { githubId } });\n    if (user == null) {\n      return null;\n    }\n    return user;\n  }\n\n  public getUserByProvider(provider: string, providerUserId: string) {\n    return this.userRepository.findOne({\n      where: { provider, providerUserId },\n      relations: ['mentors', 'students', 'mentors.course', 'students.course', 'courseUsers', 'courseUsers.course'],\n    });\n  }\n\n  public saveUser(user: Partial<User>) {\n    return this.userRepository.save(user);\n  }\n\n  public async updateUser(id: number, user: Partial<Omit<User, 'id' | 'githubId'>>) {\n    await this.userRepository.update(id, user);\n  }\n\n  public getUserByUserId(userId: number) {\n    return this.userRepository.findOneOrFail({\n      where: { id: userId },\n    });\n  }\n\n  public getUsersByUserIds(userIds: number[]) {\n    return this.userRepository.find({\n      where: { id: In(userIds) },\n    });\n  }\n\n  public static getFullName({ firstName, lastName }: { firstName: string; lastName: string }) {\n    const result = [];\n    if (firstName) {\n      result.push(firstName.trim());\n    }\n    if (lastName) {\n      result.push(lastName.trim());\n    }\n    return result.join(' ');\n  }\n\n  public async searchUsers(reqQuery?: string) {\n    if (!reqQuery) {\n      return [];\n    }\n\n    const search = `${reqQuery.trim()}%`;\n\n    // Search by full name, githubId, discord username\n    const searchTerms = search.split(' ');\n\n    const query = this.userRepository.createQueryBuilder().select(['id']).limit(20);\n\n    searchTerms.forEach((term, index) => {\n      query.andWhere(\n        new Brackets(qb => {\n          qb.where(`\"firstName\" ILIKE :searchText${index}`, { [`searchText${index}`]: `%${term}%` })\n            .orWhere(`\"lastName\" ILIKE :searchText${index}`, { [`searchText${index}`]: `%${term}%` })\n            .orWhere(`\"githubId\" ILIKE :searchText${index}`, { [`searchText${index}`]: `%${term}%` })\n            .orWhere(`CAST(\"discord\" AS jsonb)->>'username' ILIKE :searchText${index}`, {\n              [`searchText${index}`]: `%${term}%`,\n            });\n        }),\n      );\n    });\n\n    const userIds = await query.getRawMany();\n\n    if (userIds.length === 0) {\n      return [];\n    }\n\n    // Get full user data by ids\n    return this.userRepository.find({\n      where: { id: In(userIds.map(({ id }) => id)) },\n      relations: ['mentors', 'students', 'mentors.course', 'students.course', 'students.certificate'],\n    });\n  }\n\n  public static getPrimaryUserFields(modelName = 'user') {\n    return [\n      `${modelName}.id`,\n      `${modelName}.firstName`,\n      `${modelName}.lastName`,\n      `${modelName}.githubId`,\n      `${modelName}.cityName`,\n      `${modelName}.countryName`,\n      `${modelName}.discord`,\n    ];\n  }\n\n  public static getUserContactsFields(modelName = 'user') {\n    return [\n      `${modelName}.contactsEmail`,\n      `${modelName}.contactsTelegram`,\n      `${modelName}.contactsLinkedIn`,\n      `${modelName}.contactsSkype`,\n      `${modelName}.contactsPhone`,\n    ];\n  }\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/dto/notification-connection-exists.dto.ts",
    "content": "import { NotificationChannelId } from '@entities/notificationChannel';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class NotificationConnectionExistsDto {\n  @ApiProperty()\n  @IsString()\n  public channelId: NotificationChannelId;\n\n  @ApiProperty({ required: false })\n  @IsString()\n  @IsOptional()\n  public externalId?: string;\n\n  @ApiProperty({ required: false })\n  @IsNumber()\n  @IsOptional()\n  public userId?: number;\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/dto/notification-connection.dto.ts",
    "content": "import { NotificationChannelId } from '@entities/notificationChannel';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class NotificationConnectionDto {\n  constructor(connection: NotificationUserConnection) {\n    this.channelId = connection.channelId;\n    this.enabled = connection.enabled;\n    this.userId = connection.userId;\n    this.externalId = connection.externalId;\n  }\n\n  @ApiProperty()\n  @IsString()\n  public channelId: NotificationChannelId;\n\n  @ApiProperty()\n  @IsString()\n  public externalId: string;\n\n  @ApiProperty()\n  @IsNumber()\n  public userId: number;\n\n  @ApiProperty()\n  @IsBoolean()\n  @IsOptional()\n  public enabled: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/dto/notification-user-connections.dto.ts",
    "content": "import { NotificationChannelId } from '@entities/notificationChannel';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean } from 'class-validator';\n\nexport class ConnectionDetails {\n  @ApiProperty()\n  value: string;\n\n  @ApiProperty()\n  @IsBoolean()\n  enabled: boolean;\n\n  @ApiProperty({ required: false })\n  lastLinkSentAt?: string;\n}\n\nexport class NotificationUserConnectionsDto {\n  @ApiProperty({ type: Map })\n  public connections: Record<NotificationChannelId, ConnectionDetails>;\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/dto/notification-user-settings.dto.ts",
    "content": "import { Notification } from '@entities/notification';\nimport { NotificationChannelId } from '@entities/notificationChannel';\nimport { NotificationUserSettings } from '@entities/notificationUserSettings';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsArray, IsBoolean } from 'class-validator';\n\nexport class NotificationUserSettingsDto {\n  constructor(notification: Notification & { settings: NotificationUserSettings[] }) {\n    this.id = notification.id;\n    this.name = notification.name;\n    this.enabled = notification.enabled;\n    this.settings = {};\n    notification.settings.forEach(setting => {\n      this.settings[setting.channelId] = setting.enabled;\n    });\n  }\n\n  @ApiProperty()\n  public id: string;\n\n  @ApiProperty()\n  public name: string;\n\n  @ApiProperty()\n  @IsBoolean()\n  public enabled: boolean;\n\n  @ApiProperty({ type: Map })\n  public settings: Record<string, boolean>;\n}\n\nexport class ConnectionDetails {\n  @ApiProperty()\n  value: string;\n\n  @ApiProperty()\n  @IsBoolean()\n  enabled: boolean;\n}\n\nexport class UserNotificationsDto {\n  @ApiProperty({ type: [NotificationUserSettingsDto] })\n  @IsArray()\n  public notifications: NotificationUserSettingsDto[];\n\n  @ApiProperty({ type: Map })\n  public connections: Record<NotificationChannelId, ConnectionDetails>;\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/dto/send-user-notification.dto.ts",
    "content": "import { NotificationId } from '@entities/notification';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class SendUserNotificationDto {\n  @ApiProperty()\n  @IsString()\n  public notificationId: NotificationId;\n\n  @ApiProperty()\n  @IsNumber()\n  public userId: number;\n\n  @ApiProperty()\n  @IsOptional()\n  public data: object;\n\n  @ApiProperty()\n  @IsNumber()\n  @IsOptional()\n  // date in ms\n  public expireDate?: number;\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/dto/update-notification-user-settings.dto.ts",
    "content": "import { NotificationChannelId } from '@entities/notificationChannel';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean } from 'class-validator';\n\nexport class UpdateNotificationUserSettingsDto {\n  @ApiProperty()\n  public notificationId: string;\n\n  @ApiProperty()\n  @IsBoolean()\n  public enabled: boolean;\n\n  @ApiProperty()\n  public channelId: NotificationChannelId;\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/dto/upsert-notification-connection.dto.ts",
    "content": "import { NotificationChannelId } from '@entities/notificationChannel';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';\n\nexport class UpsertNotificationConnectionDto {\n  @ApiProperty()\n  @IsString()\n  public channelId: NotificationChannelId;\n\n  @ApiProperty()\n  @IsString()\n  public externalId: string;\n\n  @ApiProperty()\n  @IsNumber()\n  public userId: number;\n\n  @ApiProperty()\n  @IsBoolean()\n  @IsOptional()\n  public enabled: boolean;\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/index.ts",
    "content": "export { UsersNotificationsModule } from './users-notifications.module';\nexport { UserNotificationsService } from './users.notifications.service';\n"
  },
  {
    "path": "nestjs/src/users-notifications/users-notifications.module.ts",
    "content": "import { forwardRef, Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport { User } from '@entities/user';\nimport { Notification } from '@entities/notification';\nimport { UsersNotificationsController } from './users.notifications.controller';\nimport { UserNotificationsService } from './users.notifications.service';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { NotificationUserSettings } from '@entities/notificationUserSettings';\nimport { NotificationsModule } from 'src/notifications/notifications.module';\nimport { AuthModule } from 'src/auth/auth.module';\nimport { UsersModule } from 'src/users/users.module';\nimport { NotificationChannelSettings } from '@entities/notificationChannelSettings';\n\n@Module({\n  imports: [\n    TypeOrmModule.forFeature([\n      User,\n      Notification,\n      NotificationUserSettings,\n      NotificationUserConnection,\n      NotificationChannelSettings,\n    ]),\n    NotificationsModule,\n    UsersModule,\n    forwardRef(() => AuthModule),\n  ],\n  controllers: [UsersNotificationsController],\n  providers: [UserNotificationsService],\n  exports: [UserNotificationsService],\n})\nexport class UsersNotificationsModule {}\n"
  },
  {
    "path": "nestjs/src/users-notifications/users.notifications.controller.ts",
    "content": "import { Body, Controller, Get, HttpCode, NotFoundException, Post, Put, Req, UseGuards } from '@nestjs/common';\nimport { ApiBody, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';\nimport { CurrentRequest, DefaultGuard, RequiredRoles, Role, RoleGuard, AuthService } from 'src/auth';\nimport { UpdateNotificationUserSettingsDto } from './dto/update-notification-user-settings.dto';\nimport { NotificationUserSettingsDto, UserNotificationsDto } from './dto/notification-user-settings.dto';\nimport { NotificationConnectionExistsDto } from './dto/notification-connection-exists.dto';\nimport { UpsertNotificationConnectionDto } from './dto/upsert-notification-connection.dto';\nimport { NotificationConnectionDto } from './dto/notification-connection.dto';\nimport { UserNotificationsService } from './users.notifications.service';\nimport { SendUserNotificationDto } from './dto/send-user-notification.dto';\nimport { ConnectionDetails, NotificationUserConnectionsDto } from './dto/notification-user-connections.dto';\nimport { UsersService } from 'src/users/users.service';\n\n@Controller('users/notifications')\n@ApiTags('users notifications')\n@UseGuards(DefaultGuard)\nexport class UsersNotificationsController {\n  constructor(\n    private userNotificationsService: UserNotificationsService,\n    private authService: AuthService,\n    private usersService: UsersService,\n  ) {}\n\n  @Get('/')\n  @ApiOperation({ operationId: 'getUserNotifications' })\n  @ApiOkResponse({ type: UserNotificationsDto })\n  public async getUserNotifications(@Req() req: CurrentRequest) {\n    const {\n      user: { id },\n    } = req;\n    const [notifications, connectionsResponse] = await Promise.all([\n      this.userNotificationsService.getUserNotificationsSettings(id),\n      this.getUserConnections(req),\n    ]);\n\n    return {\n      notifications: notifications.map(notification => new NotificationUserSettingsDto(notification)),\n      connections: connectionsResponse.connections,\n    };\n  }\n\n  @Get('/connections')\n  @ApiOperation({ operationId: 'getUserNotificationConnections' })\n  @ApiOkResponse({ type: NotificationUserConnectionsDto })\n  public async getUserConnections(@Req() req: CurrentRequest) {\n    const {\n      user: { id },\n    } = req;\n    const [connections, lastLink, profile] = await Promise.all([\n      this.userNotificationsService.getUserConnections(id),\n      this.authService.getLoginStateByUserId(id),\n      this.usersService.getUserByUserId(id),\n    ]);\n\n    const dtoConnections = Object.fromEntries(\n      connections.map(connection => [\n        connection.channelId,\n        {\n          value: connection.externalId,\n          enabled: connection.enabled,\n          lastLinkSentAt: lastLink?.data.channelId === connection.channelId ? lastLink.createdDate : undefined,\n        } as ConnectionDetails,\n      ]),\n    );\n\n    if (profile.discord) {\n      dtoConnections.discord = {\n        value: `${profile.discord.id}`,\n        // there is no way to get connections status for discord. User may block us, but we never know.\n        enabled: true,\n      };\n    }\n\n    return {\n      connections: dtoConnections,\n    };\n  }\n\n  @Put('/')\n  @ApiOperation({ operationId: 'updateUserNotifications' })\n  @ApiOkResponse({ type: [UpdateNotificationUserSettingsDto] })\n  @ApiBody({ type: [UpdateNotificationUserSettingsDto] })\n  public async updateUserNotifications(@Req() req: CurrentRequest, @Body() dto: UpdateNotificationUserSettingsDto[]) {\n    return await this.userNotificationsService.saveUserNotificationSettings(req.user.id, dto);\n  }\n\n  @Post('/confirmation/email')\n  @ApiOperation({ operationId: 'sendEmailConfirmationLink' })\n  public async sendEmailConfirmation(@Req() req: CurrentRequest) {\n    const { id } = req.user;\n\n    await this.userNotificationsService.sendEmailConfirmation(id);\n  }\n\n  @Post('/connection/find')\n  @UseGuards(RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOkResponse({ type: NotificationConnectionDto })\n  @HttpCode(200)\n  public async findConnection(@Body() dto: NotificationConnectionExistsDto): Promise<NotificationConnectionDto> {\n    const connection = await this.userNotificationsService.getUserConnection(dto);\n    if (!connection) {\n      throw new NotFoundException('no such connection');\n    }\n    return new NotificationConnectionDto(connection);\n  }\n\n  @Post('/connection')\n  @UseGuards(RoleGuard)\n  @RequiredRoles([Role.Admin])\n  @ApiOkResponse({ type: NotificationConnectionDto })\n  public async createUserConnection(@Body() dto: UpsertNotificationConnectionDto) {\n    const connection = await this.userNotificationsService.saveUserConnection(dto);\n    return new NotificationConnectionDto(connection);\n  }\n\n  @Post('/send')\n  @RequiredRoles([Role.Admin])\n  @ApiOperation({ operationId: 'sendNotification' })\n  @ApiOkResponse()\n  @ApiForbiddenResponse()\n  public async sendNotification(@Body() dto: SendUserNotificationDto) {\n    await this.userNotificationsService.sendEventNotification(dto);\n  }\n}\n"
  },
  {
    "path": "nestjs/src/users-notifications/users.notifications.service.ts",
    "content": "import { Notification, NotificationType } from '@entities/notification';\nimport { NotificationUserSettings } from '@entities/notificationUserSettings';\nimport { BadRequestException, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { UpdateNotificationUserSettingsDto } from 'src/users-notifications/dto/update-notification-user-settings.dto';\nimport { IsNull, Repository } from 'typeorm';\nimport { NotificationChannelSettings } from '@entities/notificationChannelSettings';\nimport { NotificationChannelId } from '@entities/notificationChannel';\nimport { NotificationConnectionExistsDto } from 'src/users-notifications/dto/notification-connection-exists.dto';\nimport { NotificationUserConnection } from '@entities/notificationUserConnection';\nimport { UpsertNotificationConnectionDto } from 'src/users-notifications/dto/upsert-notification-connection.dto';\nimport { SendUserNotificationDto } from './dto/send-user-notification.dto';\nimport { addHours, differenceInMilliseconds } from 'date-fns';\nimport { NotificationData, NotificationsService } from '../notifications/notifications.service';\nimport { AuthService } from '../auth';\nimport { UsersService } from '../users/users.service';\nimport { GithubStrategy } from '../auth/strategies/github.strategy';\n\n@Injectable()\nexport class UserNotificationsService {\n  constructor(\n    @InjectRepository(Notification)\n    private notificationsRepository: Repository<Notification>,\n    @InjectRepository(NotificationUserSettings)\n    private userNotificationsRepository: Repository<NotificationUserSettings>,\n    @InjectRepository(NotificationUserConnection)\n    private notificationUserConnectionRepository: Repository<NotificationUserConnection>,\n    @InjectRepository(NotificationChannelSettings)\n    private channelsSettingsRepository: Repository<NotificationChannelSettings>,\n    private notificationsService: NotificationsService,\n    private authService: AuthService,\n    private userService: UsersService,\n    private githubService: GithubStrategy,\n  ) {}\n\n  public getUserNotificationsSettings(userId: number) {\n    return this.notificationsRepository\n      .createQueryBuilder('notification')\n      .leftJoinAndMapMany(\n        'notification.settings',\n        NotificationUserSettings,\n        'userSettings',\n        'userSettings.notificationId = notification.id and userSettings.userId = :userId',\n        { userId },\n      )\n      .where({ enabled: true, type: NotificationType.event, parent: IsNull() })\n      .orderBy('name')\n      .getMany() as Promise<(Notification & { settings: NotificationUserSettings[] })[]>;\n  }\n\n  public async saveUserNotificationSettings(userId: number, notifications: UpdateNotificationUserSettingsDto[]) {\n    await this.userNotificationsRepository\n      .createQueryBuilder()\n      .insert()\n      .into(NotificationUserSettings)\n      .values(\n        notifications.map(notification => ({\n          notificationId: notification.notificationId,\n          channelId: notification.channelId,\n          enabled: notification.enabled,\n          userId: userId,\n        })),\n      )\n      .orUpdate(['enabled'], ['channelId', 'userId', 'notificationId'])\n      .execute();\n  }\n\n  public getUserConnection(info: NotificationConnectionExistsDto) {\n    const { channelId, externalId, userId } = info;\n    if (!userId && !externalId) return;\n    const config = externalId ? { externalId } : { userId };\n    return this.notificationUserConnectionRepository.findOne({\n      where: {\n        channelId,\n        ...config,\n      },\n    });\n  }\n\n  public getUserConnections(userId: number) {\n    return this.notificationUserConnectionRepository.find({\n      where: {\n        userId,\n      },\n    });\n  }\n\n  public saveUserConnection(connection: UpsertNotificationConnectionDto) {\n    return this.notificationUserConnectionRepository.save(connection);\n  }\n\n  public deleteUserConnection(connection: { channelId: NotificationChannelId; userId: number }) {\n    return this.notificationUserConnectionRepository.delete(connection);\n  }\n\n  private async getUserNotificationSettings(userId: number, notificationId: string) {\n    const notification = await (this.notificationsRepository\n      .createQueryBuilder('notification')\n      .leftJoinAndMapMany(\n        'notification.userSettings',\n        NotificationUserSettings,\n        'userSettings',\n        '(userSettings.notificationId = notification.id or userSettings.notificationId = notification.parentId) and userSettings.userId = :userId',\n        { userId },\n      )\n      .innerJoinAndMapMany(\n        'notification.channels',\n        NotificationChannelSettings,\n        'channelSettings',\n        'channelSettings.notificationId = :notificationId',\n        { notificationId },\n      )\n      .innerJoinAndMapMany(\n        'notification.connections',\n        NotificationUserConnection,\n        'connection',\n        'connection.userId = :userId',\n        { userId },\n      )\n      .where({ id: notificationId, enabled: true, type: NotificationType.event })\n      .getOne() as Promise<\n      Notification & {\n        userSettings: NotificationUserSettings[];\n        channels: NotificationChannelSettings[];\n        connections: NotificationUserConnection[];\n      }\n    >);\n    const connectionMap = new Map(notification?.connections.map(connection => [connection.channelId, connection]));\n    const userSettings = new Map(notification?.userSettings.map(setting => [setting.channelId, setting.enabled]));\n\n    return (\n      notification?.channels\n        .map(channel => {\n          const { channelId } = channel;\n          const externalId = connectionMap.get(channelId)?.externalId;\n\n          const settingEnabled =\n            channelId !== 'discord' ? userSettings.get(channelId) !== false : userSettings.get(channelId) === true;\n          const enabled = connectionMap.get(channelId)?.enabled && settingEnabled;\n\n          return {\n            ...channel,\n            enabled,\n            externalId,\n          };\n        })\n        .filter(channel => !!channel.externalId && channel.enabled) ?? []\n    );\n  }\n\n  private async getUserConnectionsSettings(userId: number, notificationId: string) {\n    const channels = (await this.channelsSettingsRepository\n      .createQueryBuilder('channel')\n      .innerJoinAndMapOne(\n        'channel.connection',\n        NotificationUserConnection,\n        'connection',\n        'connection.channelId = channel.channelId and connection.userId = :userId',\n        { userId },\n      )\n      .where({\n        notificationId,\n      })\n      .getMany()) as (NotificationChannelSettings & { connection: NotificationUserConnection })[];\n\n    return (\n      channels\n        .map(channel => {\n          const { channelId, connection } = channel;\n          const { externalId, enabled } = connection;\n\n          return {\n            ...channel,\n            // we have to account on the flag for other channels, email can be send with no constrains\n            enabled: enabled || channelId === 'email',\n            externalId,\n          };\n        })\n        .filter(channel => !!channel.externalId && channel.enabled) ?? []\n    );\n  }\n\n  /**\n   * Automatic user notification based on triggers. sent to subscribed channels based on subscription\n   */\n  public async sendEventNotification(notificationDto: SendUserNotificationDto) {\n    const { userId, data, notificationId, expireDate } = notificationDto;\n    const notification = await this.notificationsService.getNotification(notificationId);\n    if (!notification || !notification.enabled) return;\n\n    const channels = await (notification.type == NotificationType.event\n      ? this.getUserNotificationSettings(userId, notificationId)\n      : this.getUserConnectionsSettings(userId, notificationId));\n\n    const channelMap = new Map<NotificationChannelId, NotificationData>();\n    channels.forEach(channel => {\n      if (channel.channelId === 'discord') return;\n      const message = this.notificationsService.buildChannelMessage(channel, data);\n      if (message) {\n        const { channelId, template, to } = message;\n        channelMap.set(channelId, { template, to });\n      }\n    });\n\n    if (channelMap.size === 0) return;\n\n    await this.notificationsService.publishNotification({\n      notificationId,\n      channelId: Array.from(channelMap.keys()),\n      userId,\n      expireDate,\n      data: Object.fromEntries(channelMap) as Record<NotificationChannelId, NotificationData>,\n    });\n  }\n\n  public async sendEmailConfirmation(userId: number, checkTimeLimit = true) {\n    const [user, lastLink, connections] = await Promise.all([\n      this.userService.getUserByUserId(userId),\n      checkTimeLimit ? this.authService.getLoginStateByUserId(userId) : Promise.resolve(),\n      this.getUserConnections(userId),\n    ]);\n    const email = user.contactsEmail;\n    if (!email) return;\n\n    if (connections.find(connection => connection.channelId === 'email' && connection.enabled)) return;\n\n    if (checkTimeLimit && lastLink && differenceInMilliseconds(new Date(), lastLink.createdDate) < 1000 * 60) {\n      throw new BadRequestException('Link was just sent. Please try later');\n    }\n\n    const link = await this.githubService.getAuthorizeUrl({\n      data: {\n        channelId: 'email',\n        externalId: email,\n      },\n      userId,\n      expires: addHours(new Date(), 24).toISOString(),\n    });\n\n    await this.notificationsService.sendMessage({\n      notificationId: 'emailConfirmation',\n      userId,\n      data: {\n        confirmationLink: link,\n      },\n      channelId: 'email',\n      channelValue: email,\n    });\n  }\n}\n"
  },
  {
    "path": "nestjs/src/utils/index.ts",
    "content": "export * from './shuffle';\n"
  },
  {
    "path": "nestjs/src/utils/shuffle.test.ts",
    "content": "import { isShuffledArrays, shuffleRec } from './shuffle';\n\ndescribe('shuffle utils', () => {\n  describe('isShuffledArrays', () => {\n    test.each<{ a: (number | string)[]; b: (number | string)[]; expected: boolean }>([\n      { a: [1, 2, 3], b: [1, 2, 3], expected: false },\n      { a: [1, 2, 3], b: [3, 2, 1], expected: true },\n      { a: [1, 2, 3], b: [1, 3, 2], expected: true },\n      { a: [1, 2], b: [1, 2, 3], expected: false },\n      { a: [], b: [], expected: false },\n      { a: [1], b: [1], expected: false },\n      { a: ['a', 'b'], b: ['b', 'a'], expected: true },\n      { a: [1, 2, 3], b: [1, 2], expected: false },\n    ])('isShuffledArrays($a, $b) should be $expected', ({ a, b, expected }) => {\n      expect(isShuffledArrays(a, b)).toBe(expected);\n    });\n  });\n\n  describe('shuffleRec', () => {\n    test.each<number[][]>([[[]], [[1]], [[1, 1, 1]]])(\n      'should return a copy of the array for %p when shuffle is not possible or result remains same',\n      input => {\n        const result = shuffleRec(input);\n        expect(result).toEqual(input);\n        expect(result).not.toBe(input);\n      },\n    );\n\n    test.each<unknown[][]>([[[1, 2, 3]], [[1, 2, 3, 4, 5]], [['a', 'b', 'c', 'd']]])(\n      'should return a shuffled version of %p',\n      input => {\n        const result = shuffleRec(input);\n\n        expect(result).toHaveLength(input.length);\n        expect([...result].sort()).toEqual([...input].sort());\n        expect(isShuffledArrays(input, result)).toBe(true);\n      },\n    );\n\n    test('should respect maxAttempts', () => {\n      const input = [1, 2];\n      const result = shuffleRec(input, 0);\n      expect(result).toHaveLength(input.length);\n      expect([...result].sort()).toEqual([...input].sort());\n    });\n  });\n});\n"
  },
  {
    "path": "nestjs/src/utils/shuffle.ts",
    "content": "import { randomBytes } from 'crypto';\n\nclass Generator {\n  buff: Uint8Array;\n  constructor(size: number) {\n    this.buff = new Uint8Array(size);\n    const bytes = randomBytes(this.buff.length);\n    this.buff.set(bytes);\n  }\n\n  next(n: number) {\n    const val = this.buff[n - 1];\n    if (val === undefined) {\n      throw new Error('Index out of bounds');\n    }\n    return val % n;\n  }\n}\n\nexport function isShuffledArrays<T>(a: T[], b: T[]): boolean {\n  if (a.length !== b.length) return false;\n\n  for (let i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) {\n      return true;\n    }\n  }\n\n  return false;\n}\n\nfunction shuffle<T>(arr: T[]): T[] {\n  const copy = [...arr];\n\n  let n = copy.length;\n  const generator = new Generator(n);\n\n  while (n > 1) {\n    const randomIndex = generator.next(n--);\n    const el = copy[n] as T;\n    const targetEl = copy[randomIndex] as T;\n\n    copy[n] = targetEl;\n    copy[randomIndex] = el;\n  }\n\n  return copy;\n}\n\nexport function shuffleRec<T>(arr: T[], maxAttempts: number | undefined = 1000): T[] {\n  if (arr.length <= 1) {\n    return [...arr];\n  }\n\n  const first = arr[0];\n  if (arr.every(el => el === first)) {\n    return [...arr];\n  }\n\n  let attempts = 0;\n  let res = shuffle(arr);\n\n  while (!isShuffledArrays(arr, res) && attempts < maxAttempts) {\n    res = shuffle(arr);\n    attempts++;\n  }\n\n  return res;\n}\n"
  },
  {
    "path": "nestjs/test/app.e2e-spec.ts",
    "content": "import { Test, TestingModule } from '@nestjs/testing';\nimport { INestApplication } from '@nestjs/common';\nimport request from 'supertest';\nimport { AppModule } from './../src/app.module';\n\ndescribe('AppController (e2e)', () => {\n  let app: INestApplication;\n\n  beforeEach(async () => {\n    const moduleFixture: TestingModule = await Test.createTestingModule({\n      imports: [AppModule],\n    }).compile();\n\n    app = moduleFixture.createNestApplication();\n    await app.init();\n  });\n\n  it('/ (GET)', () => {\n    return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');\n  });\n});\n"
  },
  {
    "path": "nestjs/tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\", \"**/*test.ts\", \"*.config.mts\"]\n}\n"
  },
  {
    "path": "nestjs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"declaration\": true,\n    \"removeComments\": true,\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"target\": \"es2022\",\n    \"sourceMap\": true,\n    \"outDir\": \"./dist\",\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@entities/*\": [\"../server/src/models/*\"],\n      \"@common/*\": [\"../common/*\"]\n    },\n    \"incremental\": true,\n    \"skipLibCheck\": true,\n    \"strictNullChecks\": true,\n    \"strictPropertyInitialization\": false,\n    \"strict\": true,\n    \"strictBindCallApply\": true,\n    \"noImplicitAny\": true,\n    \"noUnusedLocals\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"noFallthroughCasesInSwitch\": false,\n    \"forceConsistentCasingInFileNames\": false,\n    \"types\": [\"vitest/globals\"]\n  },\n  \"ts-node\": {\n    \"require\": [\"tsconfig-paths/register\"]\n  }\n}\n"
  },
  {
    "path": "nestjs/vitest.config.mts",
    "content": "import path from 'node:path';\nimport swc from 'unplugin-swc';\nimport { defineConfig, mergeConfig } from 'vitest/config';\nimport shared from '../vitest.shared.mjs';\n\nexport default mergeConfig(\n  shared,\n  defineConfig({\n    plugins: [\n      swc.vite({\n        module: { type: 'es6' },\n      }),\n    ],\n    resolve: {\n      alias: {\n        '@entities': path.resolve(import.meta.dirname, '../server/src/models'),\n        src: path.resolve(import.meta.dirname, 'src'),\n      },\n    },\n    test: {\n      include: ['src/**/*.spec.ts', 'src/**/*.test.ts'],\n      coverage: {\n        include: ['src/**/*.(t|j)s'],\n        reportsDirectory: './coverage',\n      },\n      deps: {\n        interopDefault: true,\n      },\n    },\n  }),\n);\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"rsschool-app\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"workspaces\": [\n    \"server\",\n    \"nestjs\",\n    \"client\"\n  ],\n  \"engines\": {\n    \"node\": \">=22\",\n    \"npm\": \">=10\"\n  },\n  \"scripts\": {\n    \"start\": \"turbo run start\",\n    \"build\": \"turbo run build\",\n    \"lint\": \"turbo run lint\",\n    \"test\": \"turbo run test\",\n    \"test:ci\": \"turbo run test:ci\",\n    \"compile\": \"turbo run compile\",\n    \"format\": \"oxfmt\",\n    \"ci:format\": \"oxfmt --check\",\n    \"db:restore\": \"podman exec -i db psql -U rs_master -d rs_school < ./setup/backup-local.sql\",\n    \"db:dump\": \"PGPASSWORD=12345678 pg_dump -h localhost --username rs_master rs_school --file ./setup/backup-local.sql\",\n    \"db:dump:win\": \"pg_dump -h localhost --username rs_master rs_school > ./setup/backup-local.sql\",\n    \"db:up\": \"podman compose -f ./setup/docker-compose.yml up  -d\",\n    \"db:down\": \"podman compose -f ./setup/docker-compose.yml down\",\n    \"openapi\": \"turbo run openapi\"\n  },\n  \"dependencies\": {\n    \"axios\": \"1.8.4\",\n    \"dayjs\": \"^1.11\",\n    \"lodash\": \"4.17.21\",\n    \"typeorm\": \"0.3.20\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"10.0.1\",\n    \"@swc/core\": \"^1.15.18\",\n    \"@total-typescript/ts-reset\": \"0.6.1\",\n    \"@types/jest\": \"30.0.0\",\n    \"@types/lodash\": \"4.17.24\",\n    \"@types/node\": \"24.12.0\",\n    \"@vitest/coverage-v8\": \"~4.1.0\",\n    \"@vitest/eslint-plugin\": \"^1.6.11\",\n    \"dotenv\": \"^16.5.0\",\n    \"eslint\": \"10.0.3\",\n    \"eslint-config-turbo\": \"2.8.16\",\n    \"eslint-import-resolver-typescript\": \"4.4.4\",\n    \"eslint-plugin-boundaries\": \"5.4.0\",\n    \"globals\": \"^17.4.0\",\n    \"jest\": \"30.2.0\",\n    \"oxfmt\": \"0.38.0\",\n    \"std-env\": \"^4.0.0\",\n    \"ts-node\": \"10.9.2\",\n    \"turbo\": \"2.8.12\",\n    \"typescript\": \"5.9.3\",\n    \"typescript-eslint\": \"8.57.0\",\n    \"unplugin-swc\": \"^1.5.9\",\n    \"vitest\": \"~4.1.0\"\n  },\n  \"packageManager\": \"npm@10.7.0\"\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"extends\": [\"config:base\"],\n  \"ignorePaths\": [\"/tools/bumblebee/**\", \"/server/**\"],\n  \"dependencyDashboard\": true,\n  \"dependencyDashboardApproval\": true,\n  \"lockFileMaintenance\": { \"enabled\": true },\n  \"labels\": [\"renovate\"],\n  \"transitiveRemediation\": true,\n  \"platform\": \"github\",\n  \"repositories\": [\"rolling-scopes/rsschool-app\"],\n  \"packageRules\": [\n    {\n      \"matchPackageNames\": [\"react-dom\", \"@types/react\", \"react\"],\n      \"groupName\": \"react\"\n    },\n    {\n      \"matchPackageNames\": [\"jest\", \"@types/jest\"],\n      \"groupName\": \"jest\"\n    },\n    {\n      \"matchPackagePatterns\": [\"actions/\"],\n      \"groupName\": \"actions\"\n    },\n    {\n      \"matchPackagePatterns\": [\"^eslint\"],\n      \"groupName\": \"eslint\"\n    }\n  ]\n}\n"
  },
  {
    "path": "server/.dockerignore",
    "content": "node_modules\n.turbo\n"
  },
  {
    "path": "server/.swcrc",
    "content": "{\n  \"jsc\": {\n    \"target\": \"es2021\",\n    \"parser\": {\n      \"syntax\": \"typescript\",\n      \"decorators\": true\n    },\n    \"transform\": {\n      \"legacyDecorator\": true,\n      \"decoratorMetadata\": true\n    },\n    \"keepClassNames\": true\n  },\n  \"module\": {\n    \"type\": \"es6\"\n  },\n  \"sourceMaps\": true\n}\n"
  },
  {
    "path": "server/Dockerfile",
    "content": "FROM node:24-alpine\n\nEXPOSE 8080\n\nENV NODE_ENV=production\nENV NODE_PORT=8080\nENV TZ=utc\nENV RS_ENV=production\n\nWORKDIR /app\n\nCOPY server/tsconfig.json /app/server/\nCOPY server/package.json /app/server/\n\nCOPY package.json /app\nCOPY package-lock.json /app\n\nRUN npm install --production --no-optional\n\nCOPY server/public /app/server/public\nCOPY server/dist /app/server/dist\n\nCMD [ \"node\", \"/app/server/dist/server/src/index.js\" ]\n"
  },
  {
    "path": "server/Dockerfile.lambda",
    "content": "FROM node:24-bullseye-slim AS builder\n\nWORKDIR /container_out\n\nCOPY package.json package.json\nCOPY package-lock.json package-lock.json\nCOPY server/tsconfig.json server/tsconfig.json\nCOPY server/.env server/.env\nCOPY server/package.json server/package.json\n\nRUN npm ci --production --no-optional\n\nCOPY server/dist server/dist\n\n# Lambda Container with AWS Lambda Web Adapter\nFROM node:24-bullseye-slim\n\nENV NODE_ENV=production\nENV TZ=utc\nENV RS_ENV=staging\nENV NODE_PORT=8080\nENV PORT=8080\nENV AWS_LWA_PORT=8080\nENV AWS_LWA_REMOVE_BASE_PATH=/api\nENV DOTENV_CONFIG_PATH=/var/task/server/.env\n\nWORKDIR /var/task\n\nCOPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.0-rc1 /lambda-adapter /opt/extensions/lambda-adapter\n\nCOPY --from=builder /container_out /var/task/\n\nCMD [ \"node\", \"-r\", \"dotenv/config\", \"/var/task/server/dist/server/src/index.js\" ]\n"
  },
  {
    "path": "server/README.md",
    "content": "# Server\n\nThis workspace contains the deprecated server API. It is no longer maintained and will be removed in the future. All new development should be done in the `nestjs` workspace."
  },
  {
    "path": "server/eslint.config.mjs",
    "content": "import defaultConfig from '../eslint.config.mjs';\n\nexport default [\n  ...defaultConfig,\n  {\n    files: ['**/*.ts'],\n    rules: {\n      '@typescript-eslint/no-explicit-any': 0,\n    },\n  },\n];\n"
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"server\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"license\": \"Mozilla Public License 2.0\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"compile\": \"tsc --noEmit\",\n    \"start\": \"tsnd --ignore-watch node_modules --inspect --notify false -r dotenv/config -- ./src/index.ts | pino-pretty -i time,hostname,pid,host,method,remoteAddress\",\n    \"start:prod\": \"node dist/server/src/index.js\",\n    \"lint\": \"eslint src\",\n    \"test\": \"vitest run\",\n    \"test:ci\": \"vitest run\",\n    \"test-watch\": \"vitest watch\",\n    \"coverage\": \"vitest run --coverage\",\n    \"typeorm\": \"typeorm-ts-node-commonjs\",\n    \"typeorm:migration:generate\": \"npm run migration\",\n    \"migration\": \"typeorm-ts-node-commonjs migration:generate -d src/dataSource.ts\"\n  },\n  \"dependencies\": {\n    \"@apalchys/pino-cloudwatch\": \"0.9.0\",\n    \"@koa/cors\": \"3.0.0\",\n    \"@koa/router\": \"8.0.8\",\n    \"http-status-codes\": \"2.1.4\",\n    \"json2csv\": \"5.0.7\",\n    \"jsonwebtoken\": \"8.5.1\",\n    \"koa\": \"2.16.2\",\n    \"koa-basic-auth\": \"4.0.0\",\n    \"koa-bodyparser\": \"4.2.1\",\n    \"koa-jwt\": \"3.6.0\",\n    \"koa-static\": \"5.0.0\",\n    \"luxon\": \"2.5.2\",\n    \"moment-timezone\": \"0.5.35\",\n    \"node-schedule\": \"2.0.0\",\n    \"octokit\": \"1.7.1\",\n    \"pg\": \"8.11.3\",\n    \"pino-multi-stream\": \"4.2.0\",\n    \"reflect-metadata\": \"0.1.13\"\n  },\n  \"devDependencies\": {\n    \"@types/json2csv\": \"5.0.7\",\n    \"@types/jsonwebtoken\": \"8.5.8\",\n    \"@types/koa\": \"2.11.3\",\n    \"@types/koa__cors\": \"2.2.3\",\n    \"@types/koa__router\": \"8.0.2\",\n    \"@types/koa-basic-auth\": \"2.0.4\",\n    \"@types/koa-bodyparser\": \"4.2.0\",\n    \"@types/koa-static\": \"4.0.2\",\n    \"@types/luxon\": \"2.0.9\",\n    \"@types/node-schedule\": \"1.3.2\",\n    \"@types/passport-github2\": \"1.2.5\",\n    \"@types/pg\": \"8.6.4\",\n    \"@types/pino-multi-stream\": \"3.1.2\",\n    \"cross-env\": \"7.0.3\",\n    \"pino-pretty\": \"7.5.1\",\n    \"ts-node-dev\": \"2.0.0\",\n    \"tsconfig-paths\": \"4.1.0\"\n  }\n}\n"
  },
  {
    "path": "server/public/swagger.yml",
    "content": "info:\n  title: rs.school API\n  version: 1.0.0\n  description: ''\nbasePath: /api\nswagger: '2.0'\npaths:\n  '/course/{courseId}/mentor/students':\n    get:\n      description: Returns mentors students results\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: User object\n  '/course/{courseId}/students':\n    get:\n      description: Returns course students\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of students\n    post:\n      description: Add/Update course students\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n        - name: students\n          in: body\n          schema:\n            type: array\n            items:\n              type: object\n              properties:\n                githubId:\n                  type: string\n                isExpelled:\n                  type: boolean\n                expellingReason:\n                  type: boolean\n                readyFullTime:\n                  type: boolean\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of students\n  '/course/{courseId}/externalAccounts':\n    get:\n      description: Returns external accounts of students\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of students with external accounts\n  '/course/{courseId}/mentors':\n    get:\n      description: Saves course mentors\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of mentors\n    post:\n      description: Returns course mentors\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n        - name: mentors\n          in: body\n          schema:\n            type: array\n            items:\n              type: object\n              properties:\n                githubId:\n                  type: string\n                maxStudentsLimit:\n                  type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of mentors\n  '/course/{courseId}/pairs':\n    post:\n      description: Assign student to mentor\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n        - name: pairs\n          in: body\n          schema:\n            type: array\n            items:\n              type: object\n              properties:\n                studentGithubId:\n                  type: string\n                mentorGithubId:\n                  type: string\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of pairs\n  '/course/{courseId}/tasks':\n    get:\n      description: Returns course tasks\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of tasks object\n  '/course/{courseId}/task':\n    post:\n      description: Assign task to course/stage\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: Result\n  '/course/{courseId}/task/{courseTaskId}':\n    put:\n      description: Update course task\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: Result\n    delete:\n      description: Delete course task\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: Result\n  '/course/{courseId}/task/{courseTaskId}/shuffle':\n    post:\n      description: Assign course task to checker\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n        - name: courseTaskId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: Result\n  '/course/{courseId}/scores':\n    post:\n      description: Save course task score\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n        - name: scores\n          in: body\n          schema:\n            type: array\n            items:\n              type: object\n              properties:\n                studentGithubId:\n                  type: string\n                mentorGithubId:\n                  type: string\n                courseTaskId:\n                  type: integer\n                comment:\n                  type: string\n                githubPrUrl:\n                  type: string\n                score:\n                  type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: ''\n  '/course/{courseId}/score':\n    post:\n      description: Save course task score\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: ''\n    get:\n      description: Get course score data\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: ''\n  '/course/{courseId}/expulsion':\n    post:\n      description: Expel student from course by active mentor\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n        - name: data\n          in: body\n          schema:\n            type: object\n            properties:\n              studentId:\n                type: string\n              comment:\n                type: string\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: result\n  '/course/{courseId}/feedback':\n    post:\n      description: Post feedback about a mentor or student\n      security:\n        - cookieAuth: []\n      parameters:\n        - name: courseId\n          in: path\n          description: Course Id\n          required: true\n          type: integer\n        - name: data\n          in: body\n          schema:\n            type: object\n            properties:\n              toUserId:\n                type: integer\n              text:\n                type: string\n              badgeId:\n                type: string\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: result\n  '/course/{courseId}':\n    get:\n      description: Return info about course\n      parameters:\n        - name: courseId\n          in: path\n          required: true\n          type: integer\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: ''\n  /course:\n    post:\n      description: Create course\n      security:\n        - cookieAuth: []\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: ''\n  /courses:\n    get:\n      description: Gets courses info\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: Courses info\n  /profile:\n    get:\n      description: get student profile\n      security:\n        - cookieAuth: []\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: profile\n  /session:\n    get:\n      description: Gets current user session\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: operation status\n  /tasks:\n    post:\n      description: Add/Update tasks\n      security:\n        - cookieAuth: []\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: operation status\n    get:\n      description: Gets tasks\n      security:\n        - cookieAuth: []\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: List of tasks\n  /users:\n    post:\n      description: Update user activist status\n      security:\n        - cookieAuth: []\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: operation status\n  '/users/search/:searchText':\n    post:\n      description: Search users\n      security:\n        - cookieAuth: []\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: operation status\n  /v2/me:\n    get:\n      description: Returns users profile\n      produces:\n        - application/json\n      responses:\n        '200':\n          description: User object\ndefinitions: {}\nresponses: {}\nparameters: {}\nsecurityDefinitions: {}\ntags: []\n"
  },
  {
    "path": "server/src/app.ts",
    "content": "import cors from '@koa/cors';\nimport Koa from 'koa';\nimport bodyParser from 'koa-bodyparser';\nimport serve from 'koa-static';\nimport koaJwt from 'koa-jwt';\n\nimport { config } from './config';\nimport { ILogger, loggerMiddleware, createDefaultLogger } from './logger';\n\nimport { routesMiddleware, routeLoggerMiddleware } from './routes';\nimport { startBackgroundJobs } from './schedule';\nimport { dataSourceOptions } from './dataSourceOptions';\nimport { createConnection } from 'typeorm';\n\nexport class App {\n  public koa = new Koa();\n  private appLogger: ILogger;\n\n  constructor(logger: ILogger = createDefaultLogger()) {\n    this.appLogger = logger;\n\n    this.koa.use(loggerMiddleware(this.appLogger));\n\n    this.koa.use(bodyParser({ jsonLimit: '20mb', enableTypes: ['json', 'form', 'text'] }));\n    if (process.env.NODE_ENV === 'production') {\n      this.koa.use(cors({ credentials: true, allowMethods: '*', origin: config.host }));\n    }\n\n    this.koa.use(\n      koaJwt({ key: 'user', cookie: 'auth-token', secret: config.sessionKey, debug: true, passthrough: true }),\n    );\n\n    process.on('unhandledRejection', reason => this.appLogger.error(reason as any));\n  }\n\n  public start(listen = false) {\n    const routes = routesMiddleware(this.appLogger);\n\n    this.koa.use(routes.publicRouter.routes());\n    this.koa.use(routes.publicRouter.allowedMethods());\n\n    this.koa.use(routeLoggerMiddleware);\n    this.koa.use(serve('public'));\n\n    if (listen) {\n      this.koa.listen(config.port);\n      this.appLogger.info(`Service is running on ${config.port} port`);\n    }\n    return this.koa;\n  }\n\n  public async pgConnect(): Promise<boolean> {\n    const logger = this.appLogger.child({ module: 'db' });\n    const connection = await createConnection(dataSourceOptions);\n    logger.info('Connected to Postgres');\n\n    logger.info('Executing migrations...');\n    await connection.runMigrations();\n    logger.info('Migrations executed successfully');\n\n    return true;\n  }\n\n  public async startBackgroundJobs() {\n    if (process.env.NODE_ENV !== 'production') {\n      return Promise.resolve();\n    }\n    return startBackgroundJobs(this.appLogger.child({ module: 'schedule' }));\n  }\n}\n"
  },
  {
    "path": "server/src/config.ts",
    "content": "export interface IConfig {\n  users: {\n    cloud: {\n      username: string;\n      password: string;\n    };\n  };\n  auth: {\n    callback: string;\n    github_client_id: string;\n    github_client_secret: string;\n    successRedirect: string;\n    activityWebhookSecret: string;\n    consentSecret: string;\n  };\n  aws: {\n    secretAccessKey: string;\n    accessKeyId: string;\n    region: string;\n    restApiUrl: string;\n    restApiKey: string;\n  };\n  admin: {\n    username: string;\n    password: string;\n  };\n  isDevMode: boolean;\n  github: {\n    org: string;\n    privateKey: string;\n    appId: string;\n    installationId: string;\n    hooksSecret: string;\n  };\n  port: number;\n  sessionKey: string;\n  host: string;\n}\n\nexport const config: IConfig = {\n  users: {\n    cloud: {\n      username: process.env.RSSHCOOL_API_USERS_CLOUD_USERNAME || 'test',\n      password: process.env.RSSHCOOL_API_USERS_CLOUD_PASSWORD || 'test',\n    },\n  },\n  auth: {\n    callback: process.env.RSSHCOOL_API_AUTH_CALLBACK || 'http://localhost:3001/auth/github/callback',\n    github_client_id: process.env.RSSHCOOL_API_AUTH_CLIENT_ID || 'client-id',\n    github_client_secret: process.env.RSSHCOOL_API_AUTH_CLIENT_SECRET || 'client-secret',\n    successRedirect: process.env.RSSHCOOL_API_AUTH_SUCCESS_REDIRECT || 'http://localhost:3000',\n    activityWebhookSecret: process.env.ACTIVITY_WEBHOOK_SECRET || 'activity-webhook',\n    consentSecret: process.env.CONSENT_SECRET || 'consent-secret',\n  },\n  admin: {\n    username: process.env.RSSHCOOL_API_ADMIN_USERNAME || '',\n    password: process.env.RSSHCOOL_API_ADMIN_PASSWORD || '',\n  },\n  github: {\n    org: 'rolling-scopes-school',\n    privateKey: process.env.RSSHCOOL_API_GITHUB_PRIVATE_KEY || '',\n    appId: process.env.RSSHCOOL_API_GITHUB_APP_ID || '',\n    installationId: process.env.RSSHCOOL_API_GITHUB_APP_INSTALL_ID || '',\n    hooksSecret: process.env.RSSHCOOL_API_GITHUB_HOOKS_SECRET || 'hooks_secret',\n  },\n  isDevMode: process.env.NODE_ENV !== 'production',\n  aws: {\n    secretAccessKey: process.env.RSSHCOOL_API_AWS_SECRET_ACCESS_KEY || '',\n    accessKeyId: process.env.RSSHCOOL_API_AWS_ACCESS_KEY_ID || '',\n    region: process.env.RSSHCOOL_API_AWS_REGION || '',\n    restApiUrl: process.env.RSSHCOOL_API_AWS_REST_API_URL || '',\n    restApiKey: process.env.RSSHCOOL_API_AWS_REST_API_KEY || '',\n  },\n  port: parseInt(process.env.NODE_PORT || '3001', 10),\n  sessionKey: process.env.RSSHCOOL_API_SESSION_KEY || 'secret',\n  host: process.env.RSSHCOOL_HOST || 'http://localhost:3000',\n};\n"
  },
  {
    "path": "server/src/dataSource.ts",
    "content": "import 'dotenv/config';\n\nimport { DataSource } from 'typeorm';\nimport { dataSourceOptions } from './dataSourceOptions';\n\nexport default new DataSource(dataSourceOptions);\n"
  },
  {
    "path": "server/src/dataSourceOptions.ts",
    "content": "import { models } from './models';\nimport { migrations } from './migrations';\nimport { DataSourceOptions } from 'typeorm';\n\nexport const dataSourceOptions: DataSourceOptions = {\n  type: 'postgres',\n  ssl: process.env.RS_ENV\n    ? {\n        rejectUnauthorized: false,\n      }\n    : undefined, // localhost should not use ssl\n  host: process.env.RSSHCOOL_PG_HOST,\n  port: process.env.RS_ENV !== 'staging' ? 5432 : undefined,\n  username: process.env.RSSHCOOL_PG_USERNAME,\n  password: process.env.RSSHCOOL_PG_PASSWORD,\n  database: process.env.RSSHCOOL_PG_DATABASE,\n  entities: models,\n  migrations,\n  logging: ['migration', 'error', 'warn'],\n};\n"
  },
  {
    "path": "server/src/index.ts",
    "content": "import 'reflect-metadata'; // for typeorm\nimport { App } from './app';\n\nconst app = new App();\napp\n  .pgConnect()\n  .then(() => app.start(true))\n  .then(() => app.startBackgroundJobs())\n  .catch(e => console.error(e));\n"
  },
  {
    "path": "server/src/logger.ts",
    "content": "import Router from '@koa/router';\nimport { AxiosError, isAxiosError } from 'axios';\nimport pinoLogger from 'pino-multi-stream';\nimport { ParsedUrlQuery } from 'querystring';\nimport { config } from './config';\nimport axios from 'axios';\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst cloudwatch = require('@apalchys/pino-cloudwatch'); //tslint:disable-line\n\nexport interface ILog {\n  data?: any;\n  host: string;\n  method: string;\n  query: ParsedUrlQuery;\n  remoteAddress: string;\n  status: number;\n  url: string;\n  userAgent: string;\n  userId?: string;\n}\n\ntype ErrorObj = {\n  err: Partial<Error>;\n};\n\nexport interface ILogger {\n  info(obj: object | string, ...params: any[]): void;\n  warn(obj: object | string, ...params: any[]): void;\n  error(obj: Error | ErrorObj | string, ...params: any[]): void;\n  child(options: { module: string; userId?: string }): ILogger;\n}\n\nexport async function sendError(err: any): Promise<void> {\n  if (process.env.NODE_ENV !== 'production') {\n    return;\n  }\n  const error = {\n    message: err.message ?? '',\n    cause: err ?? '',\n  };\n  await axios\n    .post(`${config.aws.restApiUrl}/errors`, error, { headers: { 'x-api-key': config.aws.restApiKey } })\n    .catch(() => null);\n}\n\nexport const loggerMiddleware =\n  (externalLogger: ILogger) =>\n  async (ctx: Router.RouterContext<any, { logger: ILogger }>, next: () => Promise<any>) => {\n    const logger = externalLogger;\n\n    const data: Partial<ILog> = {\n      status: ctx.status,\n      method: ctx.method,\n      url: ctx.url,\n      query: ctx.query,\n    };\n    const start = Date.now();\n    try {\n      ctx.logger = logger;\n      await next();\n      data.status = ctx.status;\n    } catch (e) {\n      if (isAxiosError(e)) {\n        const error = e as AxiosError<Error>;\n        logger.error(error.message, {\n          data: error.response?.data,\n          status: error.response?.status,\n        });\n        await sendError({ message: error.message, data: error.response?.data, status: error.response?.status });\n      } else {\n        logger.error(e as Error);\n        await sendError(e);\n      }\n      data.status = (e as any).status;\n    }\n    logger.info({\n      msg: 'Processed request',\n      duration: Date.now() - start,\n      ...data,\n      userId: ctx.state && ctx.state.user ? ctx.state.user.id : undefined,\n    });\n  };\n\nexport function createDefaultLogger() {\n  const streams = [{ stream: process.stdout }];\n  const { accessKeyId, secretAccessKey, region } = config.aws;\n  if (process.env.NODE_ENV === 'production' && accessKeyId && secretAccessKey) {\n    const writeStream = cloudwatch({\n      interval: 2000,\n      aws_access_key_id: accessKeyId,\n      aws_secret_access_key: secretAccessKey,\n      aws_region: region,\n      group: '/app/rsschool-api',\n      stream: `${new Date().toISOString().split('T')[0]}-server`,\n    });\n    streams.push(writeStream);\n  }\n  return pinoLogger({ streams, base: null, timestamp: false }) as ILogger;\n}\n"
  },
  {
    "path": "server/src/migrations/1630340371992-UserMigration.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class UserMigration1630340371992 implements MigrationInterface {\n  name = 'UserMigration1630340371992';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task_result\" ADD \"lastCheckerId\" integer`);\n    await queryRunner.query(`COMMENT ON COLUMN \"user\".\"discord\" IS NULL`);\n    await queryRunner.query(`ALTER TABLE \"user\" ALTER COLUMN \"discord\" SET DEFAULT null`);\n    await queryRunner.query(`COMMENT ON COLUMN \"student\".\"startDate\" IS NULL`);\n    await queryRunner.query(`ALTER TABLE \"student\" ALTER COLUMN \"startDate\" SET DEFAULT '\"1970-01-01T00:00:00.000Z\"'`);\n    await queryRunner.query(\n      `ALTER TABLE \"task_result\" ADD CONSTRAINT \"FK_0d531a05b39c159334a1724e1b0\" FOREIGN KEY (\"lastCheckerId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task_result\" DROP CONSTRAINT \"FK_0d531a05b39c159334a1724e1b0\"`);\n    await queryRunner.query(`ALTER TABLE \"task_result\" DROP COLUMN \"lastCheckerId\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1630341383942-TaskResult.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TaskResult1630341383942 implements MigrationInterface {\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`\n      UPDATE \"task_result\"\n      SET \"lastCheckerId\" = CAST(\"historicalScores\"->-1->>'authorId' AS INT)\n      WHERE CAST(\"historicalScores\"->-1->>'authorId' AS INT) > 0\n    `);\n  }\n\n  public async down(_: QueryRunner): Promise<void> {\n    // do nothing\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1630342025950-StudentMigration.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class StudentMigration1630342025950 implements MigrationInterface {\n  name = 'StudentMigration1630342025950';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`COMMENT ON COLUMN \"student\".\"startDate\" IS NULL`);\n    await queryRunner.query(`ALTER TABLE \"student\" ALTER COLUMN \"startDate\" SET DEFAULT '1970-01-01T00:00:00.000Z'`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"student\" ALTER COLUMN \"startDate\" SET DEFAULT '1970-01-01 00:00:00+00'`);\n    await queryRunner.query(`COMMENT ON COLUMN \"student\".\"startDate\" IS NULL`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1630342266002-UserMigration.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class UserMigration1630342266002 implements MigrationInterface {\n  name = 'UserMigration1630342266002';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" DROP CONSTRAINT \"FK_0b3c9d5127523db43a8c4997f59\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" DROP CONSTRAINT \"FK_277a1b8395fd2896391b01b7612\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"student\" ALTER COLUMN \"startDate\" SET DEFAULT '1970-01-01T00:00:00.000Z'`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" ADD CONSTRAINT \"FK_0b3c9d5127523db43a8c4997f59\" FOREIGN KEY (\"interviewQuestionId\") REFERENCES \"interview_question\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" ADD CONSTRAINT \"FK_277a1b8395fd2896391b01b7612\" FOREIGN KEY (\"interviewQuestionCategoryId\") REFERENCES \"interview_question_category\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" DROP CONSTRAINT \"FK_277a1b8395fd2896391b01b7612\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" DROP CONSTRAINT \"FK_0b3c9d5127523db43a8c4997f59\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"student\" ALTER COLUMN \"startDate\" SET DEFAULT '1970-01-01 00:00:00+00'`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" ADD CONSTRAINT \"FK_277a1b8395fd2896391b01b7612\" FOREIGN KEY (\"interviewQuestionCategoryId\") REFERENCES \"interview_question_category\"(\"id\") ON DELETE CASCADE ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"interview_question_categories_interview_question_category\" ADD CONSTRAINT \"FK_0b3c9d5127523db43a8c4997f59\" FOREIGN KEY (\"interviewQuestionId\") REFERENCES \"interview_question\"(\"id\") ON DELETE CASCADE ON UPDATE NO ACTION`,\n    );\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1630347897950-StudentMigration.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class StudentMigration1630347897950 implements MigrationInterface {\n  name = 'StudentMigration1630347897950';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"student\" ALTER COLUMN \"startDate\" SET DEFAULT '1970-01-01T00:00:00.000Z'`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"public\".\"student\" ALTER COLUMN \"startDate\" SET DEFAULT '1970-01-01 00:00:00+00'`,\n    );\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1632333725126-ResumeMigration.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class ResumeMigration1632333725126 implements MigrationInterface {\n  name = 'ResumeMigration1632333725126';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"resume\" (\"id\" SERIAL NOT NULL, \"githubId\" text NOT NULL, \"name\" text, \"selfIntroLink\" text, \"startFrom\" text, \"fullTime\" boolean NOT NULL DEFAULT false, \"expires\" numeric, \"militaryService\" text, \"englishLevel\" text, \"avatarLink\" text, \"desiredPosition\" text, \"notes\" text, \"phone\" text, \"email\" text, \"skype\" text, \"telegram\" text, \"linkedin\" text, \"locations\" text, \"githubUsername\" text, \"website\" text, \"isHidden\" boolean NOT NULL DEFAULT false, CONSTRAINT \"UQ_ee6434baa5d6a66edf5c8fa1229\" UNIQUE (\"githubId\"), CONSTRAINT \"PK_7ff05ea7599e13fac01ac812e48\" PRIMARY KEY (\"id\"))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"resume\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1635365797478-User.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class User1635365797478 implements MigrationInterface {\n  name = 'User1635365797478';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" ADD \"providerUserId\" character varying(64)`);\n    await queryRunner.query(`ALTER TABLE \"user\" ADD \"provider\" character varying(32)`);\n    await queryRunner.query(`CREATE INDEX \"IDX_d223b6ab8859d668ab080c3628\" ON \"user\" (\"providerUserId\") `);\n    await queryRunner.query(\n      `ALTER TABLE \"user\" ADD CONSTRAINT \"UQ_bbaf6a936b2124dc6448ba3448f\" UNIQUE (\"providerUserId\", \"provider\")`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" DROP CONSTRAINT \"UQ_bbaf6a936b2124dc6448ba3448f\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_d223b6ab8859d668ab080c3628\"`);\n    await queryRunner.query(`ALTER TABLE \"user\" DROP COLUMN \"provider\"`);\n    await queryRunner.query(`ALTER TABLE \"user\" DROP COLUMN \"providerUserId\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1637591194886-StageInterview.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class StageInterview1637591194886 implements MigrationInterface {\n  name = 'StageInterview1637591194886';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`CREATE INDEX \"IDX_2e4ed1c8264a48ffe7f8547401\" ON \"stage_interview\" (\"studentId\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_db66372bf51271337293b341bf\" ON \"stage_interview\" (\"mentorId\") `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_db66372bf51271337293b341bf\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_2e4ed1c8264a48ffe7f8547401\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1638302439645-CourseMigration.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseMigration1638302439645 implements MigrationInterface {\n  name = 'CourseMigration1638302439645';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"personalMentoring\" boolean NOT NULL DEFAULT true`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"personalMentoring\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1639418471577-Indicies.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Indicies1639418471577 implements MigrationInterface {\n  name = 'Indicies1639418471577';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`CREATE INDEX \"IDX_87c5a426accd8659ac76e8d3fb\" ON \"course_task\" (\"disabled\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_e848fe0c47f23605364a5f163f\" ON \"student\" (\"isFailed\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_f277c5f942b6421c4e02e4b959\" ON \"student\" (\"isExpelled\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_d0a655e0bd36811dc5e74a1b64\" ON \"task_verification\" (\"updatedDate\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_d8959fe22a43ff7773b3640992\" ON \"task_verification\" (\"studentId\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_dae85baef040e0c3eaf1794ff6\" ON \"task_verification\" (\"courseTaskId\") `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_dae85baef040e0c3eaf1794ff6\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_d8959fe22a43ff7773b3640992\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_d0a655e0bd36811dc5e74a1b64\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_f277c5f942b6421c4e02e4b959\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_e848fe0c47f23605364a5f163f\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_87c5a426accd8659ac76e8d3fb\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1639427578702-Update.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Update1639427578702 implements MigrationInterface {\n  name = 'Update1639427578702';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP CONSTRAINT \"FK_33927c9b6369c34ee32f7084215\"`);\n    await queryRunner.query(`ALTER TABLE \"stage_interview\" DROP CONSTRAINT \"FK_47cb62b5215db20cd02ce51305c\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP CONSTRAINT \"FK_8d1bc199ec06383ae933039bf2d\"`);\n    await queryRunner.query(`ALTER TABLE \"course_event\" DROP CONSTRAINT \"FK_50d7cfb1d0d26c574bb64ffb869\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_33927c9b6369c34ee32f708421\"`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"stageId\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"createdDate\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"updatedDate\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"comment\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"studentId\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"created_date\" TIMESTAMP NOT NULL DEFAULT now()`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"updated_date\" TIMESTAMP NOT NULL DEFAULT now()`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"deleted_date\" TIMESTAMP`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"student_id\" integer NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"mentor_id\" integer`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"content\" json NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"recommendation\" character varying(64) NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"english_level\" character varying(8)`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"author_id\" integer NOT NULL`);\n    await queryRunner.query(`CREATE INDEX \"IDX_600ad506d38c98395590e76ea1\" ON \"student_feedback\" (\"student_id\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_adba43a9054da3ee83e6531d7d\" ON \"student_feedback\" (\"mentor_id\") `);\n    await queryRunner.query(\n      `ALTER TABLE \"student_feedback\" ADD CONSTRAINT \"FK_600ad506d38c98395590e76ea1f\" FOREIGN KEY (\"student_id\") REFERENCES \"student\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_feedback\" ADD CONSTRAINT \"FK_adba43a9054da3ee83e6531d7da\" FOREIGN KEY (\"mentor_id\") REFERENCES \"mentor\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_feedback\" ADD CONSTRAINT \"FK_f133ab9aba2bb7c28da9a93351d\" FOREIGN KEY (\"author_id\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP CONSTRAINT \"FK_f133ab9aba2bb7c28da9a93351d\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP CONSTRAINT \"FK_adba43a9054da3ee83e6531d7da\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP CONSTRAINT \"FK_600ad506d38c98395590e76ea1f\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_adba43a9054da3ee83e6531d7d\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_600ad506d38c98395590e76ea1\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"author_id\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"english_level\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"recommendation\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"content\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"mentor_id\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"student_id\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"deleted_date\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"updated_date\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" DROP COLUMN \"created_date\"`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"studentId\" integer`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"comment\" character varying NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now()`);\n    await queryRunner.query(`ALTER TABLE \"student_feedback\" ADD \"createdDate\" TIMESTAMP NOT NULL DEFAULT now()`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"stageId\" integer`);\n    await queryRunner.query(`CREATE INDEX \"IDX_33927c9b6369c34ee32f708421\" ON \"course_task\" (\"stageId\") `);\n    await queryRunner.query(\n      `ALTER TABLE \"course_event\" ADD CONSTRAINT \"FK_50d7cfb1d0d26c574bb64ffb869\" FOREIGN KEY (\"stageId\") REFERENCES \"stage\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_feedback\" ADD CONSTRAINT \"FK_8d1bc199ec06383ae933039bf2d\" FOREIGN KEY (\"studentId\") REFERENCES \"student\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"stage_interview\" ADD CONSTRAINT \"FK_47cb62b5215db20cd02ce51305c\" FOREIGN KEY (\"stageId\") REFERENCES \"stage\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"course_task\" ADD CONSTRAINT \"FK_33927c9b6369c34ee32f7084215\" FOREIGN KEY (\"stageId\") REFERENCES \"stage\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1639502600339-Student.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Student1639502600339 implements MigrationInterface {\n  name = 'Student1639502600339';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"student\" ADD \"mentoring\" boolean DEFAULT true`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"student\" DROP COLUMN \"mentoring\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1642884123347-ResumeSelectCourses.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class ResumeSelectCourses1642884123347 implements MigrationInterface {\n  name = 'ResumeSelectCourses1642884123347';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" ADD \"visibleCourses\" integer array NOT NULL DEFAULT '{}'`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP COLUMN \"visibleCourses\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1643481312933-Task.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Task1643481312933 implements MigrationInterface {\n  name = 'Task1643481312933';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task\" ADD \"skills\" text NOT NULL DEFAULT ''`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task\" DROP COLUMN \"skills\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1643550350939-LoginState.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class LoginState1643550350939 implements MigrationInterface {\n  name = 'LoginState1643550350939';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"login_state\" (\"id\" character varying NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"data\" text NOT NULL, CONSTRAINT \"PK_73bea2737e9230e18dc8dc1e7f2\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(`CREATE INDEX \"IDX_06facda60b88268da22c37ddec\" ON \"login_state\" (\"createdDate\") `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_06facda60b88268da22c37ddec\"`);\n    await queryRunner.query(`DROP TABLE \"login_state\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1643926895264-Notifications.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Notifications1643926895264 implements MigrationInterface {\n  name = 'Notifications1643926895264';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"notification_channel\" (\"id\" character varying NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT \"PK_50b36f3daa5dd86f7e707740b23\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"notification_user_settings\" (\"notificationId\" character varying NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"enabled\" boolean NOT NULL, \"userId\" integer NOT NULL, \"channelId\" character varying NOT NULL, CONSTRAINT \"PK_679cad5ff478ef93af7221fd98f\" PRIMARY KEY (\"notificationId\", \"userId\", \"channelId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_d0b6bedfc9eb1243b01facefe1\" ON \"notification_user_settings\" (\"notificationId\", \"userId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_a745cd57c268bf3728acbcfccb\" ON \"notification_user_settings\" (\"channelId\") `,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"notification_channel_settings\" (\"notificationId\" character varying NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"channelId\" character varying NOT NULL, \"template\" text, CONSTRAINT \"PK_6464daee0ff1cf581129618bc8c\" PRIMARY KEY (\"notificationId\", \"channelId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_773a8c01eb6d281590cdbcaabd\" ON \"notification_channel_settings\" (\"notificationId\") `,\n    );\n\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_2e2c071fde8ee3f26724de7e67\" ON \"notification_channel_settings\" (\"channelId\") `,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"notification\" (\"id\" character varying NOT NULL, \"name\" character varying NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"scope\" character varying NOT NULL DEFAULT 'general', \"enabled\" boolean NOT NULL DEFAULT false, CONSTRAINT \"PK_705b6c7cdf9b2c2ff7ac7872cb7\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(`CREATE INDEX \"IDX_50802da9f1d09f275d964dd491\" ON \"notification\" (\"name\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_7d5f118233212829d30962ce3a\" ON \"notification\" (\"scope\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_07a7e2f79cde1c82b5be2f4716\" ON \"notification\" (\"enabled\") `);\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_settings\" ADD CONSTRAINT \"FK_d58ed9fef5ec0b2875892cda12f\" FOREIGN KEY (\"notificationId\") REFERENCES \"notification\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_settings\" ADD CONSTRAINT \"FK_8704ffbe765e552c633f5c96588\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_settings\" ADD CONSTRAINT \"FK_a745cd57c268bf3728acbcfccb1\" FOREIGN KEY (\"channelId\") REFERENCES \"notification_channel\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_channel_settings\" ADD CONSTRAINT \"FK_773a8c01eb6d281590cdbcaabdf\" FOREIGN KEY (\"notificationId\") REFERENCES \"notification\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_channel_settings\" ADD CONSTRAINT \"FK_2e2c071fde8ee3f26724de7e678\" FOREIGN KEY (\"channelId\") REFERENCES \"notification_channel\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n\n    // set up channels\n    await queryRunner.query(`INSERT INTO notification_channel (id) VALUES ('email'),('telegram');`);\n\n    // setup first notifiactions\n    await queryRunner.query(\n      `INSERT INTO notification (id,name,scope) VALUES\n       ('mentorRegistrationApproval', 'Mentor registration approval', 'mentor'),\n       ('taskGrade', 'Task grade received', 'student');`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"notification_channel_settings\" DROP CONSTRAINT \"FK_2e2c071fde8ee3f26724de7e678\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_channel_settings\" DROP CONSTRAINT \"FK_773a8c01eb6d281590cdbcaabdf\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_settings\" DROP CONSTRAINT \"FK_a745cd57c268bf3728acbcfccb1\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_settings\" DROP CONSTRAINT \"FK_8704ffbe765e552c633f5c96588\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_settings\" DROP CONSTRAINT \"FK_d58ed9fef5ec0b2875892cda12f\"`,\n    );\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_2e2c071fde8ee3f26724de7e67\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_a745cd57c268bf3728acbcfccb\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_07a7e2f79cde1c82b5be2f4716\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_7d5f118233212829d30962ce3a\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_50802da9f1d09f275d964dd491\"`);\n    await queryRunner.query(`DROP TABLE \"notification\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_773a8c01eb6d281590cdbcaabd\"`);\n    await queryRunner.query(`DROP TABLE \"notification_channel_settings\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_d0b6bedfc9eb1243b01facefe1\"`);\n    await queryRunner.query(`DROP TABLE \"notification_user_settings\"`);\n    await queryRunner.query(`DROP TABLE \"notification_channel\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1644695410918-NotificationConnection.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class NotificationConnection1644695410918 implements MigrationInterface {\n  name = 'NotificationConnection1644695410918';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"notification_user_connection\" (\"userId\" integer NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"channelId\" character varying NOT NULL, \"externalId\" character varying NOT NULL, \"enabled\" boolean NOT NULL DEFAULT true, CONSTRAINT \"PK_e6e33c165209dbd9cd05f086b1c\" PRIMARY KEY (\"userId\", \"channelId\", \"externalId\"))`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" ADD CONSTRAINT \"FK_686acb0bbf9634ef2497e87582f\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" ADD CONSTRAINT \"FK_8cefc11aa24ba4e51162685196d\" FOREIGN KEY (\"channelId\") REFERENCES \"notification_channel\"(\"id\") ON DELETE NO ACTION ON UPDATE CASCADE`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" DROP CONSTRAINT \"FK_8cefc11aa24ba4e51162685196d\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" DROP CONSTRAINT \"FK_686acb0bbf9634ef2497e87582f\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"notification_user_connection\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1645364514538-RepositoryEvent.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class RepositoryEvent1645364514538 implements MigrationInterface {\n  name = 'RepositoryEvent1645364514538';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"repository_event\" ADD \"userId\" integer`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"repository_event\" DROP COLUMN \"userId\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1645654601903-Opportunitites.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Opportunitites1645654601903 implements MigrationInterface {\n  name = 'Opportunitites1645654601903';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" ADD \"uuid\" uuid DEFAULT uuid_generate_v4()`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ADD \"userId\" integer`);\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP CONSTRAINT \"UQ_ee6434baa5d6a66edf5c8fa1229\"`);\n\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"githubId\" TYPE character varying(256)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"name\" TYPE character varying(256)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"selfIntroLink\" TYPE character varying(256)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"startFrom\" TYPE character varying(32)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"militaryService\" TYPE character varying(32)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"englishLevel\" TYPE character varying(8)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"avatarLink\" TYPE character varying(512)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"desiredPosition\" TYPE character varying(256)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"phone\" TYPE character varying(32)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"email\" TYPE character varying(256)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"skype\" TYPE character varying(128)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"telegram\" TYPE character varying(128)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"linkedin\" TYPE character varying(512)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"locations\" TYPE character varying(512)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"githubUsername\" TYPE character varying(256)`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"website\" TYPE character varying(512)`);\n\n    await queryRunner.query(`CREATE INDEX \"IDX_6543e24d4d8714017acd1a1b39\" ON \"resume\" (\"userId\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_ee6434baa5d6a66edf5c8fa122\" ON \"resume\" (\"githubId\") `);\n    await queryRunner.query(\n      `ALTER TABLE \"resume\" ADD CONSTRAINT \"FK_6543e24d4d8714017acd1a1b392\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP CONSTRAINT \"FK_6543e24d4d8714017acd1a1b392\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_ee6434baa5d6a66edf5c8fa122\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_6543e24d4d8714017acd1a1b39\"`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ADD CONSTRAINT \"UQ_ee6434baa5d6a66edf5c8fa1229\" UNIQUE (\"githubId\")`);\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP COLUMN \"userId\"`);\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP COLUMN \"uuid\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1647103154082-CrossCheckScheduling.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CrossCheckScheduling1647103154082 implements MigrationInterface {\n  name = 'CrossCheckScheduling1647103154082';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"crossCheckEndDate\" TIMESTAMP WITH TIME ZONE`);\n    await queryRunner.query(\n      `ALTER TABLE \"course_task\" ADD \"crossCheckStatus\" character varying NOT NULL DEFAULT 'initial'`,\n    );\n    await queryRunner.query(`\n      UPDATE course_task AS c\n      SET \"crossCheckStatus\" = 'completed'\n      FROM task_result t\n      WHERE c.id = t.\"courseTaskId\"\n      AND c.checker = 'crossCheck'\n      AND (c.\"studentEndDate\" < CURRENT_DATE OR c.\"studentEndDate\" IS NULL)\n    `);\n    await queryRunner.query(`\n      UPDATE course_task AS ct\n      SET \"crossCheckStatus\" = 'distributed'\n      FROM (\n        SELECT c.id\n        FROM course_task AS c\n        LEFT JOIN task_result AS t ON t.\"courseTaskId\" = c.id\n        WHERE c.checker = 'crossCheck'\n        AND t.score IS NULL\n        AND c.\"studentEndDate\" < CURRENT_DATE\n        GROUP BY c.id\n      ) tt\n      WHERE ct.id = tt.id\n    `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"crossCheckStatus\"`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"crossCheckEndDate\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1647175301446-TaskSolutionConstraint.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TaskSolutionConstraint1647175301446 implements MigrationInterface {\n  name = 'TaskSolutionConstraint1647175301446';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"task_solution\" ADD CONSTRAINT \"FK_e2487265adac81bea6f085d2fa0\" FOREIGN KEY (\"courseTaskId\") REFERENCES \"course_task\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task_solution\" DROP CONSTRAINT \"FK_e2487265adac81bea6f085d2fa0\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1647550751147-NotificationType.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class NotificationType1647550751147 implements MigrationInterface {\n  name = 'NotificationType1647550751147';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"notification\" RENAME COLUMN \"scope\" TO \"type\"`);\n    await queryRunner.query(`ALTER TABLE \"notification\" ALTER COLUMN \"type\" SET DEFAULT 'event'`);\n    await queryRunner.query(`CREATE INDEX \"IDX_33f33cc8ef29d805a97ff4628b\" ON \"notification\" (\"type\") `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"notification\" ALTER COLUMN \"type\" SET DEFAULT 'general'`);\n    await queryRunner.query(`ALTER TABLE \"notification\" RENAME COLUMN \"type\" TO \"scope\"`);\n    await queryRunner.query(`CREATE INDEX \"IDX_7d5f118233212829d30962ce3a\" ON \"notification\" (\"scope\") `);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1647885219936-LoginStateUserId.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class LoginStateUserId1647885219936 implements MigrationInterface {\n  name = 'LoginStateUserId1647885219936';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_7d5f118233212829d30962ce3a\"`);\n    await queryRunner.query(`ALTER TABLE \"login_state\" ADD \"userId\" integer`);\n    await queryRunner.query(`CREATE INDEX \"IDX_79b102f1b191c731920e2ea486\" ON \"login_state\" (\"userId\") `);\n\n    await queryRunner.query(`ALTER TABLE \"login_state\" ADD \"expires\" TIMESTAMP`);\n    await queryRunner.query(`CREATE INDEX \"IDX_d2236f176c9281802d3ff00d3f\" ON \"login_state\" (\"expires\") `);\n\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" DROP CONSTRAINT \"PK_e6e33c165209dbd9cd05f086b1c\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" ADD CONSTRAINT \"PK_e7ab7a5154b15417e5ee0e31a3b\" PRIMARY KEY (\"userId\", \"channelId\")`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" ADD CONSTRAINT \"UQ_c1665f13b0eb372fcb8d48ccf6a\" UNIQUE (\"userId\", \"channelId\", \"externalId\")`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_79b102f1b191c731920e2ea486\"`);\n    await queryRunner.query(`ALTER TABLE \"login_state\" DROP COLUMN \"userId\"`);\n    await queryRunner.query(`CREATE INDEX \"IDX_7d5f118233212829d30962ce3a\" ON \"notification\" (\"type\") `);\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" DROP CONSTRAINT \"UQ_c1665f13b0eb372fcb8d48ccf6a\"`,\n    );\n\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_d2236f176c9281802d3ff00d3f\"`);\n    await queryRunner.query(`ALTER TABLE \"login_state\" DROP COLUMN \"expires\"`);\n\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" DROP CONSTRAINT \"PK_e7ab7a5154b15417e5ee0e31a3b\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"notification_user_connection\" ADD CONSTRAINT \"PK_e6e33c165209dbd9cd05f086b1c\" PRIMARY KEY (\"userId\", \"channelId\", \"externalId\")`,\n    );\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1649505252996-CourseLogo.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseLogo1649505252996 implements MigrationInterface {\n  name = 'CourseLogo1649505252996';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"courseActiveLogoUrl\" character varying`);\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"courseArchivedLogoUrl\" character varying`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"courseArchivedLogoUrl\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"courseActiveLogoUrl\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1649868994688-CourseLogo.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseLogo1649868994688 implements MigrationInterface {\n  name = 'CourseLogo1649868994688';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"courseActiveLogoUrl\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"courseArchivedLogoUrl\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"logo\" character varying`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"logo\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"courseArchivedLogoUrl\" character varying`);\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"courseActiveLogoUrl\" character varying`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1650652882300-DiscordChannel.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class DiscordChannel1650652882300 implements MigrationInterface {\n  name = 'DiscordChannel1650652882300';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`INSERT INTO \"notification_channel\" (id) VALUES('discord')`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DELETE FROM \"notification_channel\" WHERE \"id\" = 'discord'`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1652870756742-Resume.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Resume1652870756742 implements MigrationInterface {\n  name = 'Resume1652870756742';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" ADD \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now()`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP COLUMN \"updatedDate\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1656326258991-History.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class History1656326258991 implements MigrationInterface {\n  name = 'History1656326258991';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"history\" (\"id\" SERIAL NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"event\" character varying NOT NULL, \"entityId\" integer, \"operation\" character varying NOT NULL, \"update\" json, \"previous\" json, CONSTRAINT \"PK_9384942edf4804b38ca0ee51416\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(`CREATE INDEX \"IDX_a619e6e10deb16bf6519d204cf\" ON \"history\" (\"updatedDate\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_80735bc019562abb4e7099340e\" ON \"history\" (\"event\") `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_80735bc019562abb4e7099340e\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_a619e6e10deb16bf6519d204cf\"`);\n    await queryRunner.query(`DROP TABLE \"history\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1661034658479-Feedback.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Feedback1661034658479 implements MigrationInterface {\n  name = 'Feedback1661034658479';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"feedback\" DROP COLUMN \"heroesUrl\"`);\n    await queryRunner.query(`ALTER TABLE \"feedback\" DROP CONSTRAINT \"FK_bfea5673b7379b1adfa2036da3f\"`);\n    await queryRunner.query(`ALTER TABLE \"feedback\" DROP CONSTRAINT \"FK_fefc350f416e262e904dcf6b35e\"`);\n    await queryRunner.query(`ALTER TABLE \"feedback\" ALTER COLUMN \"fromUserId\" SET NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"feedback\" ALTER COLUMN \"toUserId\" SET NOT NULL`);\n    await queryRunner.query(\n      `ALTER TABLE \"feedback\" ADD CONSTRAINT \"FK_bfea5673b7379b1adfa2036da3f\" FOREIGN KEY (\"fromUserId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"feedback\" ADD CONSTRAINT \"FK_fefc350f416e262e904dcf6b35e\" FOREIGN KEY (\"toUserId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"feedback\" DROP CONSTRAINT \"FK_fefc350f416e262e904dcf6b35e\"`);\n    await queryRunner.query(`ALTER TABLE \"feedback\" DROP CONSTRAINT \"FK_bfea5673b7379b1adfa2036da3f\"`);\n    await queryRunner.query(`ALTER TABLE \"feedback\" ALTER COLUMN \"toUserId\" DROP NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"feedback\" ALTER COLUMN \"fromUserId\" DROP NOT NULL`);\n    await queryRunner.query(\n      `ALTER TABLE \"feedback\" ADD CONSTRAINT \"FK_fefc350f416e262e904dcf6b35e\" FOREIGN KEY (\"toUserId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"feedback\" ADD CONSTRAINT \"FK_bfea5673b7379b1adfa2036da3f\" FOREIGN KEY (\"fromUserId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(`ALTER TABLE \"feedback\" ADD \"heroesUrl\" character varying`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1661087975938-Discipline.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Discipline1661087975938 implements MigrationInterface {\n  name = 'Discipline1661087975938';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"discipline\" (\"id\" SERIAL NOT NULL, \"created_date\" TIMESTAMP NOT NULL DEFAULT now(), \"updated_date\" TIMESTAMP NOT NULL DEFAULT now(), \"deleted_date\" TIMESTAMP, \"name\" character varying NOT NULL, CONSTRAINT \"PK_139512aefbb11a5b2fa92696828\" PRIMARY KEY (\"id\"))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"discipline\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1661106736439-Disciplines.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Disciplines1661106736439 implements MigrationInterface {\n  name = 'Disciplines1661106736439';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task\" RENAME COLUMN \"discipline\" TO \"disciplineId\"`);\n    await queryRunner.query(`ALTER TABLE \"event\" RENAME COLUMN \"discipline\" TO \"disciplineId\"`);\n    await queryRunner.query(`ALTER TABLE \"task\" DROP COLUMN \"disciplineId\"`);\n    await queryRunner.query(`ALTER TABLE \"task\" ADD \"disciplineId\" integer`);\n    await queryRunner.query(`ALTER TABLE \"event\" DROP COLUMN \"disciplineId\"`);\n    await queryRunner.query(`ALTER TABLE \"event\" ADD \"disciplineId\" integer`);\n    await queryRunner.query(\n      `ALTER TABLE \"task\" ADD CONSTRAINT \"FK_9e32af93bbf4f4dcf66387b3073\" FOREIGN KEY (\"disciplineId\") REFERENCES \"discipline\"(\"id\") ON DELETE SET NULL ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"event\" ADD CONSTRAINT \"FK_868c8f954dd31217a7e0981b1d2\" FOREIGN KEY (\"disciplineId\") REFERENCES \"discipline\"(\"id\") ON DELETE SET NULL ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"event\" DROP CONSTRAINT \"FK_868c8f954dd31217a7e0981b1d2\"`);\n    await queryRunner.query(`ALTER TABLE \"task\" DROP CONSTRAINT \"FK_9e32af93bbf4f4dcf66387b3073\"`);\n    await queryRunner.query(`ALTER TABLE \"event\" DROP COLUMN \"disciplineId\"`);\n    await queryRunner.query(`ALTER TABLE \"event\" ADD \"disciplineId\" character varying`);\n    await queryRunner.query(`ALTER TABLE \"task\" DROP COLUMN \"disciplineId\"`);\n    await queryRunner.query(`ALTER TABLE \"task\" ADD \"disciplineId\" character varying`);\n    await queryRunner.query(`ALTER TABLE \"event\" RENAME COLUMN \"disciplineId\" TO \"discipline\"`);\n    await queryRunner.query(`ALTER TABLE \"task\" RENAME COLUMN \"disciplineId\" TO \"discipline\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1661107174477-Disciplines.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Disciplines1661107174477 implements MigrationInterface {\n  name = 'Disciplines1661107174477';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"disciplineId\" integer`);\n    await queryRunner.query(\n      `ALTER TABLE \"course\" ADD CONSTRAINT \"FK_7dc67e5ff23f9a74b7cb129a088\" FOREIGN KEY (\"disciplineId\") REFERENCES \"discipline\"(\"id\") ON DELETE SET NULL ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP CONSTRAINT \"FK_7dc67e5ff23f9a74b7cb129a088\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"disciplineId\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1661616212488-NotificationCategory.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class NotificationCategory1661616212488 implements MigrationInterface {\n  name = 'NotificationCategory1661616212488';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"notification\" ADD \"parentId\" character varying`);\n    await queryRunner.query(\n      `ALTER TABLE \"notification\" ADD CONSTRAINT \"FK_b7386b61afc53e6b82251e41b5c\" FOREIGN KEY (\"parentId\") REFERENCES \"notification\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"notification\" DROP CONSTRAINT \"FK_b7386b61afc53e6b82251e41b5c\"`);\n    await queryRunner.query(`ALTER TABLE \"notification\" DROP COLUMN \"parentId\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1662275601017-CourseTask.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseTask1662275601017 implements MigrationInterface {\n  name = 'CourseTask1662275601017';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"duration\"`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"special\"`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"submitText\" character varying(1024)`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"validations\" text`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"crossCheckStatus\"`);\n    await queryRunner.query(\n      `CREATE TYPE \"public\".\"course_task_crosscheckstatus_enum\" AS ENUM('initial', 'distributed', 'completed')`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"course_task\" ADD \"crossCheckStatus\" \"public\".\"course_task_crosscheckstatus_enum\" NOT NULL DEFAULT 'initial'`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"crossCheckStatus\"`);\n    await queryRunner.query(`DROP TYPE \"public\".\"course_task_crosscheckstatus_enum\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"course_task\" ADD \"crossCheckStatus\" character varying NOT NULL DEFAULT 'initial'`,\n    );\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"validations\"`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"submitText\"`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"special\" character varying NOT NULL DEFAULT ''`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"duration\" integer`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1664183799115-CourseEvent.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseEvent1664183799115 implements MigrationInterface {\n  name = 'CourseEvent1664183799115';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_event\" ADD \"endTime\" TIMESTAMP WITH TIME ZONE`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_event\" DROP COLUMN \"endTime\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1666348642811-TaskCriteria.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TaskCriteria1666348642811 implements MigrationInterface {\n  name = 'TaskCriteria1666348642811';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"task_criteria\" (\"taskId\" integer NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"criteria\" jsonb NOT NULL DEFAULT '[]', CONSTRAINT \"PK_6de018b8a8dbe8845ffe811ad20\" PRIMARY KEY (\"taskId\"))`,\n    );\n    await queryRunner.query(`ALTER TABLE \"task\" ADD \"criteriaId\" integer`);\n    await queryRunner.query(`ALTER TABLE \"task\" ADD CONSTRAINT \"UQ_91f8c79680ddb1486f56128a9d6\" UNIQUE (\"criteriaId\")`);\n    await queryRunner.query(\n      `ALTER TABLE \"task\" ADD CONSTRAINT \"FK_91f8c79680ddb1486f56128a9d6\" FOREIGN KEY (\"criteriaId\") REFERENCES \"task_criteria\"(\"taskId\") ON DELETE CASCADE ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task\" DROP CONSTRAINT \"FK_91f8c79680ddb1486f56128a9d6\"`);\n    await queryRunner.query(`ALTER TABLE \"task\" DROP CONSTRAINT \"UQ_91f8c79680ddb1486f56128a9d6\"`);\n    await queryRunner.query(`ALTER TABLE \"task\" DROP COLUMN \"criteriaId\"`);\n    await queryRunner.query(`DROP TABLE \"task_criteria\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1666621080327-TaskSolutionResult.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TaskSolutionResult1666621080327 implements MigrationInterface {\n  name = 'TaskSolutionResult1666621080327';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task_solution_result\" ADD \"messages\" json NOT NULL DEFAULT '[]'`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task_solution_result\" DROP COLUMN \"messages\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1671475396333-Tasks.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Tasks1671475396333 implements MigrationInterface {\n  name = 'Tasks1671475396333';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task\" ADD \"deletedDate\" TIMESTAMP`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task\" DROP COLUMN \"deletedDate\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1672142743107-TeamDistribution.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TeamDistribution1672142743107 implements MigrationInterface {\n  name = 'TeamDistribution1672142743107';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"team\" (\"id\" SERIAL NOT NULL, \"name\" character varying NOT NULL, \"description\" character varying NOT NULL, \"chatLink\" character varying NOT NULL, \"password\" character varying NOT NULL, \"teamDistributionId\" integer, CONSTRAINT \"PK_f57d8293406df4af348402e4b74\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"team_distribution\" (\"id\" SERIAL NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"courseId\" integer, \"startDate\" TIMESTAMP WITH TIME ZONE, \"endDate\" TIMESTAMP WITH TIME ZONE, \"name\" character varying NOT NULL, \"description\" character varying NOT NULL, \"minStudents\" integer NOT NULL DEFAULT '2', \"maxStudents\" integer NOT NULL DEFAULT '4', \"studentsCount\" integer NOT NULL DEFAULT '3', \"strictStudentsCount\" boolean NOT NULL DEFAULT true, \"minTotalScore\" integer NOT NULL DEFAULT '0', CONSTRAINT \"PK_432a4b1c8bfacae59140f6fcaf8\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(`CREATE INDEX \"IDX_951e2b89c3a2b4554516409cfb\" ON \"team_distribution\" (\"courseId\") `);\n    await queryRunner.query(\n      `CREATE TABLE \"student_team_distribution_team_distribution\" (\"studentId\" integer NOT NULL, \"teamDistributionId\" integer NOT NULL, CONSTRAINT \"PK_cd9ddb2e8a915b54f5ab2612bc2\" PRIMARY KEY (\"studentId\", \"teamDistributionId\"))`,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_5d15876da767ed2eef032144ca\" ON \"student_team_distribution_team_distribution\" (\"studentId\") `,\n    );\n    await queryRunner.query(\n      `CREATE INDEX \"IDX_a939c4402f9eb96a7c2b9b5663\" ON \"student_team_distribution_team_distribution\" (\"teamDistributionId\") `,\n    );\n    await queryRunner.query(\n      `CREATE TABLE \"student_teams_team\" (\"studentId\" integer NOT NULL, \"teamId\" integer NOT NULL, CONSTRAINT \"PK_61c7be2163ef7fd885c6d6c8afc\" PRIMARY KEY (\"studentId\", \"teamId\"))`,\n    );\n    await queryRunner.query(`CREATE INDEX \"IDX_5fbd9182fe89b2417f288c61f9\" ON \"student_teams_team\" (\"studentId\") `);\n    await queryRunner.query(`CREATE INDEX \"IDX_46ecfda37a00bdb0eb9853805e\" ON \"student_teams_team\" (\"teamId\") `);\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"teamDistributionId\" integer`);\n    await queryRunner.query(\n      `ALTER TABLE \"team\" ADD CONSTRAINT \"FK_79279baf9c5c6e3fb9baabbb5bd\" FOREIGN KEY (\"teamDistributionId\") REFERENCES \"team_distribution\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"team_distribution\" ADD CONSTRAINT \"FK_951e2b89c3a2b4554516409cfbd\" FOREIGN KEY (\"courseId\") REFERENCES \"course\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"course_task\" ADD CONSTRAINT \"FK_f45fe9bce062ecb8f59edf079e8\" FOREIGN KEY (\"teamDistributionId\") REFERENCES \"team_distribution\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_team_distribution_team_distribution\" ADD CONSTRAINT \"FK_5d15876da767ed2eef032144caf\" FOREIGN KEY (\"studentId\") REFERENCES \"student\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_team_distribution_team_distribution\" ADD CONSTRAINT \"FK_a939c4402f9eb96a7c2b9b56634\" FOREIGN KEY (\"teamDistributionId\") REFERENCES \"team_distribution\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_teams_team\" ADD CONSTRAINT \"FK_5fbd9182fe89b2417f288c61f9c\" FOREIGN KEY (\"studentId\") REFERENCES \"student\"(\"id\") ON DELETE CASCADE ON UPDATE CASCADE`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_teams_team\" ADD CONSTRAINT \"FK_46ecfda37a00bdb0eb9853805e3\" FOREIGN KEY (\"teamId\") REFERENCES \"team\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"student_teams_team\" DROP CONSTRAINT \"FK_46ecfda37a00bdb0eb9853805e3\"`);\n    await queryRunner.query(`ALTER TABLE \"student_teams_team\" DROP CONSTRAINT \"FK_5fbd9182fe89b2417f288c61f9c\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"student_team_distribution_team_distribution\" DROP CONSTRAINT \"FK_a939c4402f9eb96a7c2b9b56634\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"student_team_distribution_team_distribution\" DROP CONSTRAINT \"FK_5d15876da767ed2eef032144caf\"`,\n    );\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP CONSTRAINT \"FK_f45fe9bce062ecb8f59edf079e8\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP CONSTRAINT \"FK_951e2b89c3a2b4554516409cfbd\"`);\n    await queryRunner.query(`ALTER TABLE \"team\" DROP CONSTRAINT \"FK_79279baf9c5c6e3fb9baabbb5bd\"`);\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"teamDistributionId\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_46ecfda37a00bdb0eb9853805e\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_5fbd9182fe89b2417f288c61f9\"`);\n    await queryRunner.query(`DROP TABLE \"student_teams_team\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_a939c4402f9eb96a7c2b9b5663\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_5d15876da767ed2eef032144ca\"`);\n    await queryRunner.query(`DROP TABLE \"student_team_distribution_team_distribution\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_951e2b89c3a2b4554516409cfb\"`);\n    await queryRunner.query(`DROP TABLE \"team_distribution\"`);\n    await queryRunner.query(`DROP TABLE \"team\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1672386450861-TeamDistribution.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TeamDistribution1672386450861 implements MigrationInterface {\n  name = 'TeamDistribution1672386450861';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"team_distribution\" ADD \"descriptionUrl\" character varying NOT NULL DEFAULT ''`,\n    );\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ALTER COLUMN \"startDate\" SET NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ALTER COLUMN \"endDate\" SET NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ALTER COLUMN \"description\" SET DEFAULT ''`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ALTER COLUMN \"description\" DROP DEFAULT`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ALTER COLUMN \"endDate\" DROP NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ALTER COLUMN \"startDate\" DROP NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"descriptionUrl\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1673090827105-TaskVerification.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TaskVerification1673090827105 implements MigrationInterface {\n  name = 'TaskVerification1673090827105';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task_verification\" ADD \"answers\" json NOT NULL DEFAULT '[]'`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"task_verification\" DROP COLUMN \"answers\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1673692838338-User.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class User1673692838338 implements MigrationInterface {\n  name = 'User1673692838338';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" ADD \"contactsWhatsApp\" character varying`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" DROP COLUMN \"contactsWhatsApp\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1674128274839-Team.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Team1674128274839 implements MigrationInterface {\n  name = 'Team1674128274839';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"minStudents\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"studentsCount\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"maxStudents\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"strictStudentsCount\"`);\n    await queryRunner.query(`ALTER TABLE \"team\" ADD \"teamLeadId\" integer`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"minTeamSize\" integer NOT NULL DEFAULT '2'`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"maxTeamSize\" integer NOT NULL DEFAULT '4'`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"strictTeamSize\" integer NOT NULL DEFAULT '3'`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"strictTeamSizeMode\" boolean NOT NULL DEFAULT true`);\n    await queryRunner.query(`ALTER TABLE \"team\" DROP CONSTRAINT \"FK_79279baf9c5c6e3fb9baabbb5bd\"`);\n    await queryRunner.query(`ALTER TABLE \"team\" ALTER COLUMN \"teamDistributionId\" SET NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"team\" ALTER COLUMN \"description\" SET DEFAULT ''`);\n    await queryRunner.query(`ALTER TABLE \"team\" ALTER COLUMN \"chatLink\" DROP NOT NULL`);\n    await queryRunner.query(`CREATE INDEX \"IDX_79279baf9c5c6e3fb9baabbb5b\" ON \"team\" (\"teamDistributionId\") `);\n    await queryRunner.query(\n      `ALTER TABLE \"team\" ADD CONSTRAINT \"FK_79279baf9c5c6e3fb9baabbb5bd\" FOREIGN KEY (\"teamDistributionId\") REFERENCES \"team_distribution\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"team\" DROP CONSTRAINT \"FK_79279baf9c5c6e3fb9baabbb5bd\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_79279baf9c5c6e3fb9baabbb5b\"`);\n    await queryRunner.query(`ALTER TABLE \"team\" ALTER COLUMN \"chatLink\" SET NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"team\" ALTER COLUMN \"description\" DROP DEFAULT`);\n    await queryRunner.query(`ALTER TABLE \"team\" ALTER COLUMN \"teamDistributionId\" DROP NOT NULL`);\n    await queryRunner.query(\n      `ALTER TABLE \"team\" ADD CONSTRAINT \"FK_79279baf9c5c6e3fb9baabbb5bd\" FOREIGN KEY (\"teamDistributionId\") REFERENCES \"team_distribution\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"strictTeamSizeMode\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"strictTeamSize\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"maxTeamSize\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" DROP COLUMN \"minTeamSize\"`);\n    await queryRunner.query(`ALTER TABLE \"team\" DROP COLUMN \"teamLeadId\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"strictStudentsCount\" boolean NOT NULL DEFAULT true`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"maxStudents\" integer NOT NULL DEFAULT '4'`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"studentsCount\" integer NOT NULL DEFAULT '3'`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution\" ADD \"minStudents\" integer NOT NULL DEFAULT '2'`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1674377676805-TeamDistributionStudent.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class TeamDistributionStudent1674377676805 implements MigrationInterface {\n  name = 'TeamDistributionStudent1674377676805';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"team_distribution_student\" (\"id\" SERIAL NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"studentId\" integer NOT NULL, \"courseId\" integer, \"teamDistributionId\" integer NOT NULL, \"distributed\" boolean NOT NULL DEFAULT false, \"active\" boolean NOT NULL DEFAULT true, CONSTRAINT \"UQ_a1c39af9e449474f6495b634cd5\" UNIQUE (\"studentId\", \"courseId\", \"teamDistributionId\"), CONSTRAINT \"PK_8a75ed7468b867aef47a7320188\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"team_distribution_student\" ADD CONSTRAINT \"FK_552eb86c51b2449e2665ad7be0f\" FOREIGN KEY (\"studentId\") REFERENCES \"student\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"team_distribution_student\" ADD CONSTRAINT \"FK_5b0eb057a06b5fafb89edefd358\" FOREIGN KEY (\"courseId\") REFERENCES \"course\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"team_distribution_student\" ADD CONSTRAINT \"FK_92af6f1f2345cb39398cea4748a\" FOREIGN KEY (\"teamDistributionId\") REFERENCES \"team_distribution\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"team_distribution_student\" DROP CONSTRAINT \"FK_92af6f1f2345cb39398cea4748a\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution_student\" DROP CONSTRAINT \"FK_5b0eb057a06b5fafb89edefd358\"`);\n    await queryRunner.query(`ALTER TABLE \"team_distribution_student\" DROP CONSTRAINT \"FK_552eb86c51b2449e2665ad7be0f\"`);\n    await queryRunner.query(`DROP TABLE \"team_distribution_student\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1674755854609-Resume.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Resume1674755854609 implements MigrationInterface {\n  name = 'Resume1674755854609';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP CONSTRAINT \"FK_6543e24d4d8714017acd1a1b392\"`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"userId\" SET NOT NULL`);\n    await queryRunner.query(\n      `ALTER TABLE \"resume\" ADD CONSTRAINT \"FK_6543e24d4d8714017acd1a1b392\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"resume\" DROP CONSTRAINT \"FK_6543e24d4d8714017acd1a1b392\"`);\n    await queryRunner.query(`ALTER TABLE \"resume\" ALTER COLUMN \"userId\" DROP NOT NULL`);\n    await queryRunner.query(\n      `ALTER TABLE \"resume\" ADD CONSTRAINT \"FK_6543e24d4d8714017acd1a1b392\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1675245424426-UserGroup.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class UserGroup1675245424426 implements MigrationInterface {\n  name = 'UserGroup1675245424426';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_user\" ADD \"isDementor\" boolean NOT NULL DEFAULT false`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_user\" DROP COLUMN \"isDementor\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1675345245770-Course.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Course1675345245770 implements MigrationInterface {\n  name = 'Course1675345245770';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP CONSTRAINT \"UQ_fc5c908f913cd7188a018775f5f\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" ADD CONSTRAINT \"UQ_30d559218724a6d6e0cc4f26b0e\" UNIQUE (\"name\")`);\n    await queryRunner.query(`ALTER TABLE \"course\" ALTER COLUMN \"fullName\" SET NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"course\" ALTER COLUMN \"alias\" SET NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"course\" ADD CONSTRAINT \"UQ_8a167196d86062fa6abf6f0d546\" UNIQUE (\"alias\")`);\n    await queryRunner.query(`CREATE INDEX \"IDX_8a167196d86062fa6abf6f0d54\" ON \"course\" (\"alias\") `);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_8a167196d86062fa6abf6f0d54\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" DROP CONSTRAINT \"UQ_8a167196d86062fa6abf6f0d546\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" ALTER COLUMN \"alias\" DROP NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"course\" ALTER COLUMN \"fullName\" DROP NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"course\" DROP CONSTRAINT \"UQ_30d559218724a6d6e0cc4f26b0e\"`);\n    await queryRunner.query(\n      `ALTER TABLE \"course\" ADD CONSTRAINT \"UQ_fc5c908f913cd7188a018775f5f\" UNIQUE (\"name\", \"alias\")`,\n    );\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1676139987317-User.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class User1676139987317 implements MigrationInterface {\n  name = 'User1676139987317';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" ADD \"languages\" text array NOT NULL DEFAULT '{}'`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" DROP COLUMN \"languages\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1685197747051-MentorRegistry.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class MentorRegistry1685197747051 implements MigrationInterface {\n  name = 'MentorRegistry1685197747051';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"mentor_registry\" ADD \"comment\" character varying`);\n    await queryRunner.query(`ALTER TABLE \"mentor_registry\" ADD \"receivedDate\" TIMESTAMP`);\n    await queryRunner.query(`ALTER TABLE \"mentor_registry\" ADD \"sendDate\" TIMESTAMP`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"mentor_registry\" DROP COLUMN \"sendDate\"`);\n    await queryRunner.query(`ALTER TABLE \"mentor_registry\" DROP COLUMN \"receivedDate\"`);\n    await queryRunner.query(`ALTER TABLE \"mentor_registry\" DROP COLUMN \"comment\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1686657350908-InterviewScore.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class InterviewScore1686657350908 implements MigrationInterface {\n  name = 'InterviewScore1686657350908';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"stage_interview\" ADD \"score\" integer`);\n    await queryRunner.query(`ALTER TABLE \"stage_interview_feedback\" ADD \"version\" integer`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"stage_interview_feedback\" DROP COLUMN \"version\"`);\n    await queryRunner.query(`ALTER TABLE \"stage_interview\" DROP COLUMN \"score\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1687009744110-Prompt.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Prompt1687009744110 implements MigrationInterface {\n  name = 'Prompt1687009744110';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"prompt\" (\"id\" SERIAL NOT NULL, \"createdDate\" TIMESTAMP NOT NULL DEFAULT now(), \"updatedDate\" TIMESTAMP NOT NULL DEFAULT now(), \"type\" character varying(256) NOT NULL, \"text\" character varying NOT NULL, CONSTRAINT \"UQ_8b52c9f9bf5ffaba2f772c65456\" UNIQUE (\"type\"), CONSTRAINT \"PK_d8e3aa07a95560a445ad50fb931\" PRIMARY KEY (\"id\"))`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`DROP TABLE \"prompt\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1691520611773-Temperature.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Temperature1691520611773 implements MigrationInterface {\n  name = 'Temperature1691520611773';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"prompt\" ADD \"temperature\" integer NOT NULL`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"prompt\" DROP COLUMN \"temperature\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1691524327332-Temperature.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Temperature1691524327332 implements MigrationInterface {\n  name = 'Temperature1691524327332';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"prompt\" DROP COLUMN \"temperature\"`);\n    await queryRunner.query(`ALTER TABLE \"prompt\" ADD \"temperature\" double precision NOT NULL`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"prompt\" DROP COLUMN \"temperature\"`);\n    await queryRunner.query(`ALTER TABLE \"prompt\" ADD \"temperature\" integer NOT NULL`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1693930286280-CourseUsersActivist.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseUsersActivist1693930286280 implements MigrationInterface {\n  name = 'CourseUsersActivist1693930286280';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_user\" ADD \"isActivist\" boolean NOT NULL DEFAULT false`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_user\" DROP COLUMN \"isActivist\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1699808604000-AddMinStudentPerMentorColumnToCourse.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AddMinStudentPerMentorColumnToCourse1699808604000 implements MigrationInterface {\n  name = 'AddMinStudentPerMentorColumnToCourse1699808604000';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"minStudentsPerMentor\" integer DEFAULT '2'`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"minStudentsPerMentor\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1700391857109-Obfuscation.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Obfuscation1700391857109 implements MigrationInterface {\n  name = 'Obfuscation1700391857109';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" ADD \"obfuscated\" boolean NOT NULL DEFAULT false`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" DROP COLUMN \"obfuscated\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1712137476312-Course.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Course1712137476312 implements MigrationInterface {\n  name = 'Course1712137476312';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"certificateThreshold\" integer DEFAULT '70'`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"certificateThreshold\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1730926720293-CourseTask.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseTask1730926720293 implements MigrationInterface {\n  name = 'CourseTask1730926720293';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"studentRegistrationStartDate\" TIMESTAMP WITH TIME ZONE`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"studentRegistrationStartDate\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1734874453585-Contributor.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Contributor1734874453585 implements MigrationInterface {\n  name = 'Contributor1734874453585';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"contributor\" (\"id\" SERIAL NOT NULL, \"created_date\" TIMESTAMP NOT NULL DEFAULT now(), \"updated_date\" TIMESTAMP NOT NULL DEFAULT now(), \"deleted_date\" TIMESTAMP, \"user_id\" integer NOT NULL, \"description\" character varying NOT NULL, CONSTRAINT \"REL_16ce6a76e8a6808b976a61db48\" UNIQUE (\"user_id\"), CONSTRAINT \"PK_816afef005b8100becacdeb6e58\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(`CREATE INDEX \"IDX_16ce6a76e8a6808b976a61db48\" ON \"contributor\" (\"user_id\") `);\n    await queryRunner.query(`ALTER TABLE \"user\" ADD \"contributor_id\" integer`);\n    await queryRunner.query(\n      `ALTER TABLE \"user\" ADD CONSTRAINT \"UQ_aadfefeabbf834e1bb67c9fec0a\" UNIQUE (\"contributor_id\")`,\n    );\n    await queryRunner.query(`ALTER TABLE \"course\" ALTER COLUMN \"certificateThreshold\" SET NOT NULL`);\n    await queryRunner.query(\n      `ALTER TABLE \"contributor\" ADD CONSTRAINT \"FK_16ce6a76e8a6808b976a61db487\" FOREIGN KEY (\"user_id\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"user\" ADD CONSTRAINT \"FK_aadfefeabbf834e1bb67c9fec0a\" FOREIGN KEY (\"contributor_id\") REFERENCES \"contributor\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"user\" DROP CONSTRAINT \"FK_aadfefeabbf834e1bb67c9fec0a\"`);\n    await queryRunner.query(`ALTER TABLE \"contributor\" DROP CONSTRAINT \"FK_16ce6a76e8a6808b976a61db487\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" ALTER COLUMN \"certificateThreshold\" DROP NOT NULL`);\n    await queryRunner.query(`ALTER TABLE \"user\" DROP CONSTRAINT \"UQ_aadfefeabbf834e1bb67c9fec0a\"`);\n    await queryRunner.query(`ALTER TABLE \"user\" DROP COLUMN \"contributor_id\"`);\n    await queryRunner.query(`DROP INDEX \"public\".\"IDX_16ce6a76e8a6808b976a61db48\"`);\n    await queryRunner.query(`DROP TABLE \"contributor\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1736458672717-Course.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Course1736458672717 implements MigrationInterface {\n  name = 'Course1736458672717';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"wearecommunityUrl\" character varying`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"wearecommunityUrl\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1738250779923-CoursePersonalMentoringDates.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CoursePersonalMentoringDates1738250779923 implements MigrationInterface {\n  name = 'CoursePersonalMentoringDates1738250779923';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"personalMentoringStartDate\" TIMESTAMP WITH TIME ZONE`);\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"personalMentoringEndDate\" TIMESTAMP WITH TIME ZONE`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"personalMentoringStartDate\"`);\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"personalMentoringEndDate\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1746467689328-Course.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class Course1746467689328 implements MigrationInterface {\n  name = 'Course1746467689328';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" ADD \"certificateDisciplines\" text DEFAULT NULL`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course\" DROP COLUMN \"certificateDisciplines\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1747380525126-CourseTaskInterviewCreatingPairs.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class CourseTaskInterviewCreatingPairs1747380525126 implements MigrationInterface {\n  name = 'CourseTaskInterviewCreatingPairs1747380525126';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" ADD \"isCreatingInterviewPairs\" boolean NOT NULL DEFAULT false`);\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(`ALTER TABLE \"course_task\" DROP COLUMN \"isCreatingInterviewPairs\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/1760699701354-AddCourseLeaveSurveyResponse.ts",
    "content": "import { MigrationInterface, QueryRunner } from 'typeorm';\n\nexport class AddCourseLeaveSurveyResponse1760699701354 implements MigrationInterface {\n  name = 'AddCourseLeaveSurveyResponse1760699701354';\n\n  public async up(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `CREATE TABLE \"course_leave_survey_responses\" (\"id\" uuid NOT NULL DEFAULT uuid_generate_v4(), \"userId\" integer NOT NULL, \"courseId\" integer NOT NULL, \"reasonForLeaving\" text array, \"otherComment\" text, \"submittedAt\" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT \"PK_da291c26b3b92584820c0d02323\" PRIMARY KEY (\"id\"))`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"course_leave_survey_responses\" ADD CONSTRAINT \"FK_bb5c64d5636045dbeaf485b7c75\" FOREIGN KEY (\"userId\") REFERENCES \"user\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"course_leave_survey_responses\" ADD CONSTRAINT \"FK_c86a19923266180ab9ad289df04\" FOREIGN KEY (\"courseId\") REFERENCES \"course\"(\"id\") ON DELETE NO ACTION ON UPDATE NO ACTION`,\n    );\n  }\n\n  public async down(queryRunner: QueryRunner): Promise<void> {\n    await queryRunner.query(\n      `ALTER TABLE \"course_leave_survey_responses\" DROP CONSTRAINT \"FK_c86a19923266180ab9ad289df04\"`,\n    );\n    await queryRunner.query(\n      `ALTER TABLE \"course_leave_survey_responses\" DROP CONSTRAINT \"FK_bb5c64d5636045dbeaf485b7c75\"`,\n    );\n    await queryRunner.query(`DROP TABLE \"course_leave_survey_responses\"`);\n  }\n}\n"
  },
  {
    "path": "server/src/migrations/index.ts",
    "content": "import { UserMigration1630340371992 } from './1630340371992-UserMigration';\nimport { TaskResult1630341383942 } from './1630341383942-TaskResult';\nimport { StudentMigration1630342025950 } from './1630342025950-StudentMigration';\nimport { UserMigration1630342266002 } from './1630342266002-UserMigration';\nimport { StudentMigration1630347897950 } from './1630347897950-StudentMigration';\nimport { ResumeMigration1632333725126 } from './1632333725126-ResumeMigration';\nimport { User1635365797478 } from './1635365797478-User';\nimport { StageInterview1637591194886 } from './1637591194886-StageInterview';\nimport { Indicies1639418471577 } from './1639418471577-Indicies';\nimport { Student1639502600339 } from './1639502600339-Student';\nimport { CourseMigration1638302439645 } from './1638302439645-CourseMigration';\nimport { Update1639427578702 } from './1639427578702-Update';\nimport { ResumeSelectCourses1642884123347 } from './1642884123347-ResumeSelectCourses';\nimport { Task1643481312933 } from './1643481312933-Task';\nimport { LoginState1643550350939 } from './1643550350939-LoginState';\nimport { Notifications1643926895264 } from './1643926895264-Notifications';\nimport { NotificationConnection1644695410918 } from './1644695410918-NotificationConnection';\nimport { RepositoryEvent1645364514538 } from './1645364514538-RepositoryEvent';\nimport { CrossCheckScheduling1647103154082 } from './1647103154082-CrossCheckScheduling';\nimport { Opportunitites1645654601903 } from './1645654601903-Opportunitites';\nimport { TaskSolutionConstraint1647175301446 } from './1647175301446-TaskSolutionConstraint';\nimport { NotificationType1647550751147 } from './1647550751147-NotificationType';\nimport { LoginStateUserId1647885219936 } from './1647885219936-LoginStateUserId';\nimport { CourseLogo1649505252996 } from './1649505252996-CourseLogo';\nimport { CourseLogo1649868994688 } from './1649868994688-CourseLogo';\nimport { DiscordChannel1650652882300 } from './1650652882300-DiscordChannel';\nimport { Resume1652870756742 } from './1652870756742-Resume';\nimport { History1656326258991 } from './1656326258991-History';\nimport { Feedback1661034658479 } from './1661034658479-Feedback';\nimport { Discipline1661087975938 } from './1661087975938-Discipline';\nimport { Disciplines1661106736439 } from './1661106736439-Disciplines';\nimport { Disciplines1661107174477 } from './1661107174477-Disciplines';\nimport { NotificationCategory1661616212488 } from './1661616212488-NotificationCategory';\nimport { CourseTask1662275601017 } from './1662275601017-CourseTask';\nimport { CourseEvent1664183799115 } from './1664183799115-CourseEvent';\nimport { TaskCriteria1666348642811 } from './1666348642811-TaskCriteria';\nimport { TaskSolutionResult1666621080327 } from './1666621080327-TaskSolutionResult';\nimport { TeamDistribution1672142743107 } from './1672142743107-TeamDistribution';\nimport { Tasks1671475396333 } from './1671475396333-Tasks';\nimport { TeamDistribution1672386450861 } from './1672386450861-TeamDistribution';\nimport { TaskVerification1673090827105 } from './1673090827105-TaskVerification';\nimport { User1673692838338 } from './1673692838338-User';\nimport { Team1674128274839 } from './1674128274839-Team';\nimport { TeamDistributionStudent1674377676805 } from './1674377676805-TeamDistributionStudent';\nimport { Resume1674755854609 } from './1674755854609-Resume';\nimport { UserGroup1675245424426 } from './1675245424426-UserGroup';\nimport { User1676139987317 } from './1676139987317-User';\nimport { Course1675345245770 } from './1675345245770-Course';\nimport { MentorRegistry1685197747051 } from './1685197747051-MentorRegistry';\nimport { Prompt1687009744110 } from './1687009744110-Prompt';\nimport { Temperature1691520611773 } from './1691520611773-Temperature';\nimport { Temperature1691524327332 } from './1691524327332-Temperature';\nimport { InterviewScore1686657350908 } from './1686657350908-InterviewScore';\nimport { CourseUsersActivist1693930286280 } from './1693930286280-CourseUsersActivist';\nimport { AddMinStudentPerMentorColumnToCourse1699808604000 } from './1699808604000-AddMinStudentPerMentorColumnToCourse';\nimport { Obfuscation1700391857109 } from './1700391857109-Obfuscation';\nimport { Course1712137476312 } from './1712137476312-Course';\nimport { CourseTask1730926720293 } from './1730926720293-CourseTask';\nimport { Contributor1734874453585 } from './1734874453585-Contributor';\nimport { Course1736458672717 } from './1736458672717-Course';\nimport { CoursePersonalMentoringDates1738250779923 } from './1738250779923-CoursePersonalMentoringDates';\nimport { CourseTaskInterviewCreatingPairs1747380525126 } from './1747380525126-CourseTaskInterviewCreatingPairs';\nimport { Course1746467689328 } from './1746467689328-Course';\nimport { AddCourseLeaveSurveyResponse1760699701354 } from './1760699701354-AddCourseLeaveSurveyResponse';\n\nexport const migrations = [\n  UserMigration1630340371992,\n  TaskResult1630341383942,\n  StudentMigration1630342025950,\n  UserMigration1630342266002,\n  StudentMigration1630347897950,\n  ResumeMigration1632333725126,\n  User1635365797478,\n  StageInterview1637591194886,\n  Indicies1639418471577,\n  Student1639502600339,\n  CourseMigration1638302439645,\n  Update1639427578702,\n  ResumeSelectCourses1642884123347,\n  Task1643481312933,\n  LoginState1643550350939,\n  Notifications1643926895264,\n  NotificationConnection1644695410918,\n  RepositoryEvent1645364514538,\n  CrossCheckScheduling1647103154082,\n  Opportunitites1645654601903,\n  TaskSolutionConstraint1647175301446,\n  NotificationType1647550751147,\n  LoginStateUserId1647885219936,\n  CourseLogo1649505252996,\n  CourseLogo1649868994688,\n  DiscordChannel1650652882300,\n  Resume1652870756742,\n  History1656326258991,\n  Feedback1661034658479,\n  Discipline1661087975938,\n  Disciplines1661106736439,\n  Disciplines1661107174477,\n  NotificationCategory1661616212488,\n  CourseTask1662275601017,\n  CourseEvent1664183799115,\n  TaskCriteria1666348642811,\n  TaskSolutionResult1666621080327,\n  TeamDistribution1672142743107,\n  TeamDistribution1672386450861,\n  Tasks1671475396333,\n  TaskVerification1673090827105,\n  User1673692838338,\n  Team1674128274839,\n  TeamDistributionStudent1674377676805,\n  Resume1674755854609,\n  UserGroup1675245424426,\n  User1676139987317,\n  Course1675345245770,\n  MentorRegistry1685197747051,\n  Prompt1687009744110,\n  Temperature1691520611773,\n  Temperature1691524327332,\n  InterviewScore1686657350908,\n  CourseUsersActivist1693930286280,\n  AddMinStudentPerMentorColumnToCourse1699808604000,\n  Obfuscation1700391857109,\n  Course1712137476312,\n  CourseTask1730926720293,\n  Contributor1734874453585,\n  Course1736458672717,\n  CoursePersonalMentoringDates1738250779923,\n  CourseTaskInterviewCreatingPairs1747380525126,\n  Course1746467689328,\n  AddCourseLeaveSurveyResponse1760699701354,\n];\n"
  },
  {
    "path": "server/src/models/alert.ts",
    "content": "import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';\n\nexport enum AlertType {\n  INFO = 'info',\n  ERROR = 'error',\n  WARN = 'warn',\n}\n\n@Entity()\nexport class Alert {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @Column()\n  text: string;\n\n  @Column({ nullable: true })\n  courseId: number;\n\n  @Column({ default: false })\n  enabled: boolean;\n\n  @Column({ default: AlertType.INFO })\n  type: AlertType;\n}\n"
  },
  {
    "path": "server/src/models/certificate.ts",
    "content": "import {\n  Entity,\n  JoinColumn,\n  Column,\n  CreateDateColumn,\n  OneToOne,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n} from 'typeorm';\nimport { Student } from './student';\n\n@Entity()\nexport class Certificate {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @Column()\n  publicId: string;\n\n  @Column()\n  studentId: number;\n\n  @OneToOne(() => Student, student => student.certificate)\n  @JoinColumn()\n  student: Student;\n\n  @Column({ default: 'rsschool-certificates' })\n  s3Bucket: string;\n\n  @Column()\n  s3Key: string;\n\n  @Column({ type: 'timestamptz' })\n  issueDate: Date;\n}\n"
  },
  {
    "path": "server/src/models/contributor.ts",
    "content": "import {\n  Column,\n  CreateDateColumn,\n  DeleteDateColumn,\n  Entity,\n  Index,\n  JoinColumn,\n  OneToOne,\n  PrimaryGeneratedColumn,\n  UpdateDateColumn,\n} from 'typeorm';\nimport { User } from './user';\n\n@Entity()\nexport class Contributor {\n  @PrimaryGeneratedColumn({ name: 'id' })\n  public id: number;\n\n  @CreateDateColumn({ name: 'created_date' })\n  public createdDate: string;\n\n  @UpdateDateColumn({ name: 'updated_date' })\n  public updatedDate: string;\n\n  @DeleteDateColumn({ name: 'deleted_date' })\n  public deletedDate: string;\n\n  @OneToOne(() => User, user => user.contributor)\n  @JoinColumn({ name: 'user_id' })\n  public user: User;\n\n  @Column({ name: 'user_id' })\n  @Index()\n  public userId: number;\n\n  @Column({ name: 'description' })\n  public description: string;\n}\n"
  },
  {
    "path": "server/src/models/course-leave-survey-response.entity.ts",
    "content": "import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';\nimport { Course, User } from '../../../server/src/models';\n\n@Entity('course_leave_survey_responses')\nexport class CourseLeaveSurveyResponse {\n  @PrimaryGeneratedColumn('uuid')\n  id: string;\n\n  @Column({ type: 'integer' })\n  userId: number;\n\n  @ManyToOne(() => User)\n  @JoinColumn({ name: 'userId' })\n  user: User;\n\n  @Column({ type: 'integer' })\n  courseId: number;\n\n  @ManyToOne(() => Course)\n  @JoinColumn({ name: 'courseId' })\n  course: Course;\n\n  @Column({ type: 'text', array: true, nullable: true })\n  reasonForLeaving: string[];\n\n  @Column({ type: 'text', nullable: true })\n  otherComment: string;\n\n  @CreateDateColumn({ type: 'timestamp' })\n  submittedAt: Date;\n}\n"
  },
  {
    "path": "server/src/models/course.ts",
    "content": "import {\n  Entity,\n  Column,\n  CreateDateColumn,\n  OneToMany,\n  ManyToOne,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n  Index,\n  JoinColumn,\n} from 'typeorm';\nimport { DiscordServer } from './discordServer';\nimport { Student } from './student';\nimport { Mentor } from './mentor';\nimport { Registry } from './registry';\nimport { Discipline } from './discipline';\n\n@Entity()\nexport class Course {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: Date;\n\n  @UpdateDateColumn()\n  updatedDate: Date;\n\n  @Column({ unique: true })\n  name: string;\n\n  @Column()\n  fullName: string;\n\n  @Index()\n  @Column({ unique: true })\n  alias: string;\n\n  @Column({ nullable: true })\n  description: string;\n\n  @Column({ nullable: true })\n  descriptionUrl: string;\n\n  @Column({ nullable: true })\n  year: number;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  startDate: Date;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  endDate: Date;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  registrationEndDate: Date | null;\n\n  @Column({ nullable: true })\n  primarySkillId: string;\n\n  @Column({ nullable: true })\n  primarySkillName: string;\n\n  @Column({ nullable: true })\n  locationName: string;\n\n  @OneToMany(_ => Student, (student: Student) => student.course)\n  students: Student[];\n\n  @OneToMany(_ => Mentor, (mentor: Mentor) => mentor.course)\n  mentors: Mentor[];\n\n  @OneToMany(_ => Registry, (registry: Registry) => registry.course, { nullable: true })\n  registries: Registry[] | null;\n\n  @ManyToOne(_ => DiscordServer, (discordServer: DiscordServer) => discordServer.courses, { nullable: true })\n  discordServer: DiscordServer;\n\n  @Index()\n  @Column({ nullable: true })\n  discordServerId: number;\n\n  @Column({ default: false })\n  completed: boolean;\n\n  @Column({ default: false })\n  planned: boolean;\n\n  @Column({ default: false })\n  inviteOnly: boolean;\n\n  @Column({ nullable: true })\n  certificateIssuer: string;\n\n  @Column({ default: true })\n  usePrivateRepositories: boolean;\n\n  @Column({ default: true })\n  personalMentoring: boolean;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  personalMentoringStartDate: Date | null;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  personalMentoringEndDate: Date | null;\n\n  @Column({ nullable: true })\n  logo: string;\n\n  @ManyToOne(() => Discipline, { nullable: true, onDelete: 'SET NULL' })\n  @JoinColumn({ name: 'disciplineId' })\n  discipline: Discipline | null;\n\n  @Column({ nullable: true })\n  disciplineId: number | null;\n\n  @Column({ default: 2, nullable: true })\n  minStudentsPerMentor: number;\n\n  @Column({ default: 70 })\n  certificateThreshold: number;\n\n  @Column({ nullable: true, type: 'varchar' })\n  wearecommunityUrl: string | null;\n\n  @Column({ type: 'simple-array', default: null, nullable: true })\n  certificateDisciplines: string[] | null;\n}\n"
  },
  {
    "path": "server/src/models/courseEvent.ts",
    "content": "import { Entity, ManyToOne, CreateDateColumn, UpdateDateColumn, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { Event } from './event';\nimport { Course } from './course';\nimport { User } from './user';\n\n@Entity()\nexport class CourseEvent {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @ManyToOne(_ => Event, (lecture: Event) => lecture.courseEvents)\n  event: Event;\n\n  @Column()\n  eventId: number;\n\n  @ManyToOne(_ => Course)\n  course: Course;\n\n  @Column()\n  courseId: number;\n\n  @Column({ nullable: true })\n  stageId: number;\n\n  @Column({ type: 'date', nullable: true })\n  date: string | null;\n\n  @Column({ type: 'time with time zone', nullable: true })\n  time: string | null;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  dateTime: string | Date | null;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  endTime: string | Date | null;\n\n  @Column({ nullable: true })\n  place: string;\n\n  @Column({ nullable: true })\n  coordinator: string;\n\n  @Column({ nullable: true })\n  comment: string;\n\n  @ManyToOne(_ => User, { nullable: true })\n  organizer: User;\n\n  @Column({ nullable: true })\n  organizerId: number;\n\n  @Column({ nullable: true })\n  detailsUrl: string;\n\n  @Column({ nullable: true })\n  broadcastUrl: string;\n\n  @Column({ default: '' })\n  special: string;\n\n  @Column({ nullable: true, type: 'int' })\n  duration: number;\n}\n"
  },
  {
    "path": "server/src/models/courseManager.ts",
    "content": "import { CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';\nimport { Course } from './course';\nimport { User } from './user';\n\n@Entity()\nexport class CourseManager {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => Course)\n  course: Course;\n\n  @ManyToOne(_ => User)\n  user: User;\n}\n"
  },
  {
    "path": "server/src/models/courseTask.ts",
    "content": "import {\n  Entity,\n  OneToMany,\n  ManyToOne,\n  CreateDateColumn,\n  UpdateDateColumn,\n  Column,\n  PrimaryGeneratedColumn,\n  Index,\n  JoinColumn,\n} from 'typeorm';\nimport { Task, TaskType } from './task';\nimport { TaskChecker } from './taskChecker';\nimport { TaskResult } from './taskResult';\nimport { User } from './user';\nimport { Course } from './course';\nimport { TaskSolution } from './taskSolution';\nimport { TeamDistribution } from './teamDistribution';\n\nexport enum Checker {\n  AutoTest = 'auto-test',\n  Assigned = 'assigned',\n  Mentor = 'mentor',\n  TaskOwner = 'taskOwner',\n  CrossCheck = 'crossCheck',\n}\n\nexport enum CrossCheckStatus {\n  Initial = 'initial',\n  Distributed = 'distributed',\n  Completed = 'completed',\n}\n\nexport enum CourseTaskValidation {\n  githubIdInUrl = 'githubIdInUrl',\n  githubPrInUrl = 'githubPrInUrl',\n}\n\n@Entity()\nexport class CourseTask {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @ManyToOne(_ => Task, (task: Task) => task.courseTasks)\n  task: Task;\n\n  @Column()\n  @Index()\n  taskId: number;\n\n  @OneToMany(_ => TaskChecker, (checker: TaskChecker) => checker.courseTaskId, { nullable: true })\n  taskCheckers: TaskChecker[] | null;\n\n  @OneToMany(_ => TaskResult, (taskResult: TaskResult) => taskResult.courseTask, { nullable: true })\n  taskResults: TaskResult[] | null;\n\n  @OneToMany(_ => TaskSolution, (taskSolution: TaskSolution) => taskSolution.courseTask, { nullable: true })\n  taskSolutions: TaskSolution[] | null;\n\n  @ManyToOne(_ => Course, { nullable: true })\n  course: Course;\n\n  @Column({ nullable: true })\n  @Index()\n  courseId: number;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  studentStartDate: null | Date | string;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  studentEndDate: null | Date | string;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  studentRegistrationStartDate: null | Date | string;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  crossCheckEndDate: null | Date | string;\n\n  @Column({ type: 'timestamp', nullable: true })\n  mentorStartDate: null | Date | string;\n\n  @Column({ type: 'timestamp', nullable: true })\n  mentorEndDate: null | Date | string;\n\n  @Column({ nullable: true })\n  maxScore: number;\n\n  @Column({ nullable: true, type: 'float', default: 1 })\n  scoreWeight: number;\n\n  @Column({ default: 'mentor' })\n  @Index()\n  checker: Checker;\n\n  @ManyToOne(_ => User, { nullable: true })\n  taskOwner: User | null;\n\n  @Column({ nullable: true })\n  @Index()\n  @JoinColumn({ name: 'taskOwnerId' })\n  taskOwnerId: number | null;\n\n  @Column({ nullable: true, type: 'int' })\n  pairsCount: number | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  type: TaskType;\n\n  @Column({ default: false, type: 'boolean' })\n  @Index()\n  disabled: boolean;\n\n  @Column({ type: 'enum', enum: CrossCheckStatus, default: CrossCheckStatus.Initial })\n  crossCheckStatus: CrossCheckStatus;\n\n  @Column({ type: 'varchar', length: 1024, nullable: true })\n  submitText: string | null;\n\n  @Column({ type: 'simple-json', nullable: true })\n  validations: Record<CourseTaskValidation, boolean> | null;\n\n  @ManyToOne(() => TeamDistribution, teamDistribution => teamDistribution.courseTasks)\n  @JoinColumn()\n  teamDistribution: TeamDistribution;\n\n  @Column({ default: false, type: 'boolean' })\n  isCreatingInterviewPairs: boolean;\n}\n"
  },
  {
    "path": "server/src/models/courseUser.ts",
    "content": "import { Entity, Index, ManyToOne, CreateDateColumn, UpdateDateColumn, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { Course } from './course';\nimport { User } from './user';\n\n@Entity()\nexport class CourseUser {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => Course)\n  course: Course;\n\n  @Column()\n  @Index()\n  courseId: number;\n\n  @ManyToOne(_ => User)\n  user: User;\n\n  @Column()\n  @Index()\n  userId: number;\n\n  @Column({ default: false })\n  isManager: boolean;\n\n  @Column({ default: false })\n  isSupervisor: boolean;\n\n  @Column({ default: false })\n  isJuryActivist: boolean;\n\n  @Column({ default: false })\n  isDementor: boolean;\n\n  @Column({ default: false })\n  isActivist: boolean;\n}\n"
  },
  {
    "path": "server/src/models/data/available-languages.data.ts",
    "content": "export enum AvailableLanguages {\n  EN = 'EN',\n  ZH = 'ZH',\n  HI = 'HI',\n  ES = 'ES',\n  FR = 'FR',\n  AR = 'AR',\n  BN = 'BN',\n  RU = 'RU',\n  PT = 'PT',\n  ID = 'ID',\n  UR = 'UR',\n  JA = 'JA',\n  DE = 'DE',\n  PA = 'PA',\n  TE = 'TE',\n  TR = 'TR',\n  KO = 'KO',\n  MR = 'MR',\n  KY = 'KY',\n  KK = 'KK',\n  UZ = 'UZ',\n  KA = 'KA',\n  PL = 'PL',\n  LT = 'LT',\n  LV = 'LV',\n  BE = 'BE',\n  UK = 'UK',\n}\n"
  },
  {
    "path": "server/src/models/data/index.ts",
    "content": "export * from './language-levels.data';\nexport * from './available-languages.data';\n"
  },
  {
    "path": "server/src/models/data/language-levels.data.ts",
    "content": "export enum LanguageLevel {\n  Unkwown = 'unknown',\n  A0 = 'a0',\n  A1 = 'a1',\n  A2 = 'a2',\n  B1 = 'b1',\n  B2 = 'b2',\n  C1 = 'c1',\n  C2 = 'c2',\n}\n"
  },
  {
    "path": "server/src/models/discipline.ts",
    "content": "import { Column, CreateDateColumn, DeleteDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';\n\n@Entity()\nexport class Discipline {\n  @PrimaryGeneratedColumn({ name: 'id' })\n  public id: number;\n\n  @CreateDateColumn({ name: 'created_date' })\n  public createdDate: string;\n\n  @UpdateDateColumn({ name: 'updated_date' })\n  public updatedDate: string;\n\n  @DeleteDateColumn({ name: 'deleted_date' })\n  public deletedDate: string;\n\n  @Column({ name: 'name' })\n  public name: string;\n}\n"
  },
  {
    "path": "server/src/models/discordServer.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, OneToMany } from 'typeorm';\nimport { Course } from './course';\n\n@Entity()\nexport class DiscordServer {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column()\n  name: string;\n\n  @Column()\n  gratitudeUrl: string;\n\n  @Column({ nullable: true, type: 'text' })\n  mentorsChatUrl: string | null;\n\n  @OneToMany(_ => Course, (course: Course) => course.discordServer, { nullable: true })\n  courses: Course[] | null;\n}\n"
  },
  {
    "path": "server/src/models/event.ts",
    "content": "import {\n  Entity,\n  Column,\n  OneToMany,\n  CreateDateColumn,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n  JoinColumn,\n  ManyToOne,\n} from 'typeorm';\nimport { CourseEvent } from './courseEvent';\nimport { Discipline } from './discipline';\n\n@Entity()\nexport class Event {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: Date;\n\n  @UpdateDateColumn()\n  updatedDate: Date;\n\n  @Column()\n  name: string;\n\n  @Column({ nullable: true })\n  descriptionUrl: string;\n\n  @Column({ nullable: true })\n  description: string;\n\n  @Column({ default: 'regular' })\n  type: string;\n\n  @OneToMany(_ => CourseEvent, (courseLecture: CourseEvent) => courseLecture.event, { nullable: true })\n  courseEvents: CourseEvent[] | null;\n\n  @ManyToOne(() => Discipline, { nullable: true, onDelete: 'SET NULL' })\n  @JoinColumn({ name: 'disciplineId' })\n  discipline: Discipline | null;\n\n  @Column({ nullable: true })\n  disciplineId: number | null;\n}\n"
  },
  {
    "path": "server/src/models/feedback.ts",
    "content": "import { Entity, CreateDateColumn, ManyToOne, UpdateDateColumn, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { User } from './user';\nimport { Course } from './course';\n\n@Entity()\nexport class Feedback {\n  @PrimaryGeneratedColumn() id: number;\n\n  @ManyToOne(_ => Course, { nullable: true })\n  course?: Course;\n\n  @Column({ nullable: true })\n  courseId: number;\n\n  @ManyToOne(_ => User)\n  fromUser: User;\n\n  @Column()\n  fromUserId: number;\n\n  @ManyToOne(_ => User)\n  toUser: User;\n\n  @Column()\n  toUserId: number;\n\n  @Column({ nullable: true, type: 'varchar' })\n  comment: string | null;\n\n  @Column({ nullable: true, default: 'Thank_you' })\n  badgeId: string;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n}\n"
  },
  {
    "path": "server/src/models/history.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm';\n\n@Entity()\nexport class History {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  @Index()\n  updatedDate: number;\n\n  @Column()\n  @Index()\n  event: string;\n\n  @Column({ nullable: true })\n  entityId?: number;\n\n  @Column({ enum: ['insert', 'update', 'remove'] })\n  operation: 'insert' | 'update' | 'remove';\n\n  @Column({ type: 'json', nullable: true })\n  update?: unknown;\n\n  @Column({ type: 'json', nullable: true })\n  previous?: unknown;\n}\n"
  },
  {
    "path": "server/src/models/index.ts",
    "content": "import { CourseLeaveSurveyResponse } from './course-leave-survey-response.entity';\nimport { Alert, AlertType } from './alert';\nimport { Certificate } from './certificate';\nimport { Contributor } from './contributor';\nimport { Course } from './course';\nimport { CourseEvent } from './courseEvent';\nimport { CourseManager } from './courseManager';\nimport { CourseTask } from './courseTask';\nimport { CourseUser } from './courseUser';\nimport { Discipline } from './discipline';\nimport { DiscordServer } from './discordServer';\nimport { Event } from './event';\nimport { ExternalAccount, User } from './user';\nimport { Feedback } from './feedback';\nimport { History } from './history';\nimport { LoginState } from './loginState';\nimport { Mentor } from './mentor';\nimport { MentorRegistry, PreferredStudentsLocation } from './mentorRegistry';\nimport { Notification } from './notification';\nimport { NotificationChannel } from './notificationChannel';\nimport { NotificationChannelSettings } from './notificationChannelSettings';\nimport { NotificationUserConnection } from './notificationUserConnection';\nimport { NotificationUserSettings } from './notificationUserSettings';\nimport { PrivateFeedback } from './privateFeedback';\nimport { ProfilePermissions } from './profilePermissions';\nimport { Prompt } from './prompt';\nimport { Registry } from './registry';\nimport { RepositoryEvent } from './repositoryEvent';\nimport { Resume } from './resume';\nimport { StageInterview } from './stageInterview';\nimport { StageInterviewFeedback } from './stageInterviewFeedback';\nimport { StageInterviewStudent } from './stageInterviewStudent';\nimport { Student } from './student';\nimport { StudentFeedback } from './student-feedback';\nimport { Task } from './task';\nimport { TaskArtefact } from './taskArtefact';\nimport { TaskChecker } from './taskChecker';\nimport { TaskCriteria } from './taskCriteria';\nimport { TaskInterviewResult } from './taskInterviewResult';\nimport { TaskInterviewStudent } from './taskInterviewStudent';\nimport { TaskResult } from './taskResult';\nimport { TaskSolution } from './taskSolution';\nimport { TaskSolutionChecker } from './taskSolutionChecker';\nimport { TaskSolutionResult } from './taskSolutionResult';\nimport { TaskVerification } from './taskVerification';\nimport { UserGroup } from './userGroup';\nimport { TeamDistribution } from './teamDistribution';\nimport { Team } from './team';\nimport { TeamDistributionStudent } from './teamDistributionStudent';\n\nexport * from './session';\nexport {\n  Alert,\n  AlertType as AlertTypes,\n  Certificate,\n  Contributor,\n  Course,\n  CourseEvent,\n  CourseManager,\n  CourseTask,\n  CourseUser,\n  CourseLeaveSurveyResponse,\n  Discipline,\n  DiscordServer,\n  Event,\n  type ExternalAccount,\n  Feedback,\n  LoginState,\n  Mentor,\n  MentorRegistry,\n  PreferredStudentsLocation,\n  Notification,\n  NotificationChannel,\n  NotificationChannelSettings,\n  NotificationUserConnection,\n  NotificationUserSettings,\n  PrivateFeedback,\n  ProfilePermissions,\n  Prompt,\n  Registry,\n  RepositoryEvent,\n  Resume,\n  StageInterview,\n  StageInterviewFeedback,\n  StageInterviewStudent,\n  Student,\n  StudentFeedback,\n  Task,\n  TaskArtefact,\n  TaskChecker,\n  TaskInterviewResult,\n  TaskInterviewStudent,\n  TaskResult,\n  TaskSolution,\n  TaskSolutionChecker,\n  TaskSolutionResult,\n  TaskVerification,\n  Team,\n  TeamDistribution,\n  TeamDistributionStudent,\n  User,\n  UserGroup,\n};\n\nexport const models = [\n  Alert,\n  Certificate,\n  Contributor,\n  Course,\n  CourseEvent,\n  CourseManager,\n  CourseTask,\n  CourseUser,\n  CourseLeaveSurveyResponse,\n  Discipline,\n  DiscordServer,\n  Event,\n  Feedback,\n  History,\n  LoginState,\n  Mentor,\n  MentorRegistry,\n  Notification,\n  NotificationChannel,\n  NotificationChannelSettings,\n  NotificationUserConnection,\n  NotificationUserSettings,\n  PrivateFeedback,\n  ProfilePermissions,\n  Prompt,\n  Registry,\n  RepositoryEvent,\n  Resume,\n  StageInterview,\n  StageInterviewFeedback,\n  StageInterviewStudent,\n  Student,\n  StudentFeedback,\n  Task,\n  TaskArtefact,\n  TaskChecker,\n  TaskCriteria,\n  TaskInterviewResult,\n  TaskInterviewStudent,\n  TaskResult,\n  TaskSolution,\n  TaskSolutionChecker,\n  TaskSolutionResult,\n  TaskVerification,\n  Team,\n  TeamDistribution,\n  TeamDistributionStudent,\n  User,\n  UserGroup,\n];\n\nexport interface IApiResponse<T> {\n  data: T | T[] | null;\n  error?: {\n    message: string;\n  };\n}\n"
  },
  {
    "path": "server/src/models/loginState.ts",
    "content": "import { Entity, Column, CreateDateColumn, PrimaryColumn, Index } from 'typeorm';\nimport { NotificationChannelId } from './notificationChannel';\n\n@Entity()\nexport class LoginState {\n  @PrimaryColumn()\n  id: string;\n\n  @CreateDateColumn()\n  @Index()\n  createdDate: number;\n\n  @Index()\n  @Column({ nullable: true })\n  userId: number;\n\n  @Column({ type: 'simple-json' })\n  data: LoginData;\n\n  @Index()\n  @Column({ type: 'timestamp', nullable: true })\n  expires?: string;\n}\n\nexport type LoginData = Partial<{\n  redirectUrl: string;\n\n  channelId?: NotificationChannelId;\n  externalId?: string;\n}>;\n"
  },
  {
    "path": "server/src/models/mentor.ts",
    "content": "import {\n  Entity,\n  Column,\n  CreateDateColumn,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n  OneToMany,\n  ManyToOne,\n  Index,\n  Unique,\n} from 'typeorm';\nimport { User } from './user';\nimport { Student } from './student';\nimport { Course } from './course';\nimport { TaskChecker } from './taskChecker';\nimport { StageInterview } from './stageInterview';\nimport { TaskInterviewResult } from './taskInterviewResult';\n\n@Entity()\n@Index(['courseId'])\n@Unique(['courseId', 'userId'])\nexport class Mentor {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => Course, (course: Course) => course.mentors, { nullable: true })\n  course: Course;\n\n  @Column({ nullable: true })\n  courseId: number;\n\n  @ManyToOne(_ => User)\n  user: User;\n\n  @Column()\n  userId: number;\n\n  @Column({ default: false })\n  isExpelled: boolean;\n\n  @OneToMany(_ => Student, student => student.mentor, { nullable: true })\n  students: Student[] | null;\n\n  @OneToMany(_ => TaskChecker, (taskChecker: TaskChecker) => taskChecker.mentor, { nullable: true })\n  taskChecker: TaskChecker[] | null;\n\n  @Column({ nullable: true })\n  maxStudentsLimit: number;\n\n  @Column({ nullable: true, type: 'varchar' })\n  studentsPreference: 'any' | 'city' | 'country';\n\n  @OneToMany(_ => StageInterview, stageInterview => stageInterview.mentor, { nullable: true })\n  stageInterviews: StageInterview[] | null;\n\n  @OneToMany(_ => TaskInterviewResult, (result: TaskInterviewResult) => result.mentor)\n  interviewResults: TaskInterviewResult[] | null;\n}\n"
  },
  {
    "path": "server/src/models/mentorRegistry.ts",
    "content": "import { Entity, CreateDateColumn, UpdateDateColumn, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';\nimport { User } from './user';\n\nexport enum PreferredStudentsLocation {\n  ANY = 'any',\n  COUNTRY = 'country',\n  CITY = 'city',\n}\n@Entity()\nexport class MentorRegistry {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @ManyToOne(_ => User)\n  user: User;\n\n  @Column({ unique: true })\n  userId: number;\n\n  @Column({ type: 'simple-array', default: '' })\n  preferedCourses: string[];\n\n  @Column({ type: 'simple-array', default: '' })\n  preselectedCourses: string[];\n\n  @Column({ type: 'simple-array', default: '' })\n  technicalMentoring: string[];\n\n  @Column()\n  maxStudentsLimit: number;\n\n  @Column()\n  englishMentoring: boolean;\n\n  @Column({ type: 'simple-array', default: '' })\n  languagesMentoring: string[];\n\n  @Column('varchar')\n  preferedStudentsLocation: PreferredStudentsLocation;\n\n  @CreateDateColumn()\n  createdDate: Date;\n\n  @UpdateDateColumn()\n  updatedDate: Date;\n\n  @Column({ default: false, type: 'boolean' })\n  canceled: boolean;\n\n  @Column({ nullable: true })\n  comment: string;\n\n  @Column({ nullable: true })\n  receivedDate: Date;\n\n  @Column({ nullable: true })\n  sendDate: Date;\n}\n"
  },
  {
    "path": "server/src/models/notification.ts",
    "content": "import {\n  Entity,\n  Column,\n  CreateDateColumn,\n  PrimaryColumn,\n  UpdateDateColumn,\n  OneToMany,\n  Index,\n  ManyToOne,\n  JoinColumn,\n} from 'typeorm';\nimport { NotificationChannelSettings } from '.';\n\nexport enum NotificationType {\n  event = 'event',\n  message = 'message',\n}\n\nexport type NotificationId =\n  | 'mentorRegistrationApproval'\n  | 'mentorRegistrationApproval:submit'\n  | 'taskGrade'\n  | 'courseCertificate'\n  | 'courseScheduleChange'\n  | 'taskDeadline'\n  | 'interviewerAssigned'\n  | 'emailConfirmation'\n  | 'mentor:assigned'\n  | 'messages'\n  | 'mentorsInvitation';\n\n@Entity()\nexport class Notification {\n  @PrimaryColumn()\n  id: NotificationId;\n\n  @Column()\n  @Index()\n  name: string;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column({ default: NotificationType.event })\n  @Index()\n  type: NotificationType;\n\n  @Column({ default: false })\n  @Index()\n  enabled: boolean;\n\n  @OneToMany(_ => NotificationChannelSettings, channelSettings => channelSettings.notification, {\n    cascade: true,\n  })\n  channels: NotificationChannelSettings[];\n\n  @ManyToOne(_ => Notification, { nullable: true })\n  @JoinColumn({ name: 'parentId' })\n  parent?: Notification | null;\n}\n"
  },
  {
    "path": "server/src/models/notificationChannel.ts",
    "content": "import { Entity, CreateDateColumn, PrimaryColumn, UpdateDateColumn } from 'typeorm';\n\n@Entity()\nexport class NotificationChannel {\n  @PrimaryColumn()\n  id: NotificationChannelId;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n}\n\nexport type NotificationChannelId = 'telegram' | 'email' | 'discord';\n"
  },
  {
    "path": "server/src/models/notificationChannelSettings.ts",
    "content": "import { Entity, Column, CreateDateColumn, PrimaryColumn, UpdateDateColumn, ManyToOne, Index } from 'typeorm';\nimport { Notification } from '.';\nimport { NotificationChannel, NotificationChannelId } from './notificationChannel';\n\n@Entity()\nexport class NotificationChannelSettings {\n  @ManyToOne(_ => Notification, notification => notification.channels, {\n    onDelete: 'CASCADE',\n    onUpdate: 'CASCADE',\n  })\n  notification: Notification;\n\n  @PrimaryColumn()\n  @Index()\n  notificationId: string;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => NotificationChannel, { cascade: true, onDelete: 'CASCADE', onUpdate: 'CASCADE' })\n  channel: NotificationChannel;\n\n  @PrimaryColumn()\n  @Index()\n  channelId: NotificationChannelId;\n  @Column({ type: 'simple-json', nullable: true })\n  template: EmailTemplate | TelegramTemplate | DiscordTemplate;\n}\n\nexport type EmailTemplate = {\n  subject: string;\n  body: string;\n};\n\nexport type TelegramTemplate = {\n  body: string;\n};\n\nexport type DiscordTemplate = {\n  body: string;\n};\n"
  },
  {
    "path": "server/src/models/notificationUserConnection.ts",
    "content": "import {\n  Entity,\n  Column,\n  CreateDateColumn,\n  PrimaryColumn,\n  ManyToOne,\n  JoinColumn,\n  UpdateDateColumn,\n  Unique,\n} from 'typeorm';\nimport { NotificationChannel, User } from '.';\nimport { NotificationChannelId } from './notificationChannel';\n\n@Entity()\n@Unique(['userId', 'channelId', 'externalId'])\nexport class NotificationUserConnection {\n  @ManyToOne(() => User, user => user.notificationConnections, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })\n  @JoinColumn()\n  user: User;\n\n  @PrimaryColumn()\n  userId: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate?: number;\n\n  @ManyToOne(() => NotificationChannel, { onUpdate: 'CASCADE' })\n  channel: NotificationChannel;\n\n  @PrimaryColumn()\n  channelId: NotificationChannelId;\n\n  @Column()\n  externalId: string;\n\n  @Column({ default: true })\n  enabled: boolean;\n}\n"
  },
  {
    "path": "server/src/models/notificationUserSettings.ts",
    "content": "import {\n  Entity,\n  Column,\n  CreateDateColumn,\n  PrimaryColumn,\n  UpdateDateColumn,\n  ManyToOne,\n  JoinColumn,\n  Index,\n} from 'typeorm';\nimport { Notification } from './notification';\nimport { User } from './user';\nimport { NotificationChannel, NotificationChannelId } from './notificationChannel';\n\n@Entity()\n@Index(['notificationId', 'userId'])\nexport class NotificationUserSettings {\n  @ManyToOne(_ => Notification, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })\n  @JoinColumn([{ name: 'notificationId', referencedColumnName: 'id' }])\n  notification: Notification;\n\n  @PrimaryColumn()\n  notificationId: string;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column()\n  enabled: boolean;\n\n  @ManyToOne(_ => User, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })\n  @JoinColumn()\n  user: User;\n\n  @PrimaryColumn()\n  userId: number;\n\n  @ManyToOne(_ => NotificationChannel, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })\n  @JoinColumn()\n  channel: NotificationChannel;\n\n  @PrimaryColumn()\n  @Index()\n  channelId: NotificationChannelId;\n}\n"
  },
  {
    "path": "server/src/models/privateFeedback.ts",
    "content": "import { Entity, CreateDateColumn, ManyToOne, UpdateDateColumn, Column, PrimaryGeneratedColumn } from 'typeorm';\nimport { User } from './user';\nimport { Course } from './course';\n\n@Entity()\nexport class PrivateFeedback {\n  @PrimaryGeneratedColumn() id: number;\n\n  @ManyToOne(_ => Course, { nullable: true })\n  course?: Course | number;\n\n  @ManyToOne(_ => User)\n  fromUser: User | number;\n\n  @ManyToOne(_ => User)\n  toUser: User | number;\n\n  @Column({ nullable: true })\n  comment: string;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n}\n"
  },
  {
    "path": "server/src/models/profilePermissions.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, OneToOne } from 'typeorm';\nimport { User } from './user';\nimport {\n  PublicVisibilitySettings,\n  VisibilitySettings,\n  ConfigurableProfilePermissions,\n} from '../../../common/models/profile';\n\nexport const defaultPublicVisibilitySettings = {\n  all: false,\n};\n\nexport const defaultPartialStudentVisibilitySettings = {\n  student: false,\n  all: false,\n};\n\nexport const defaultContactsVisibilitySettings = {\n  student: true,\n  all: false,\n};\n\nexport const defaultVisibilitySettings = {\n  mentor: false,\n  student: false,\n  all: false,\n};\n\nexport const defaultProfilePermissionsSettings: ConfigurableProfilePermissions = {\n  isProfileVisible: defaultPublicVisibilitySettings,\n  isAboutVisible: defaultVisibilitySettings,\n  isEducationVisible: defaultVisibilitySettings,\n  isEnglishVisible: defaultPartialStudentVisibilitySettings,\n  isEmailVisible: defaultContactsVisibilitySettings,\n  isTelegramVisible: defaultContactsVisibilitySettings,\n  isSkypeVisible: defaultContactsVisibilitySettings,\n  isPhoneVisible: defaultContactsVisibilitySettings,\n  isContactsNotesVisible: defaultContactsVisibilitySettings,\n  isLinkedInVisible: defaultVisibilitySettings,\n  isPublicFeedbackVisible: defaultVisibilitySettings,\n  isMentorStatsVisible: defaultVisibilitySettings,\n  isStudentStatsVisible: defaultPartialStudentVisibilitySettings,\n};\n\n@Entity()\nexport class ProfilePermissions {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column({ unique: true })\n  userId: number;\n\n  @OneToOne(() => User, user => user.profilePermissions)\n  user: User;\n\n  @Column({ type: 'json', default: defaultPublicVisibilitySettings })\n  isProfileVisible: PublicVisibilitySettings;\n\n  @Column({ type: 'json', default: defaultVisibilitySettings })\n  isAboutVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultVisibilitySettings })\n  isEducationVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultPartialStudentVisibilitySettings })\n  isEnglishVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultContactsVisibilitySettings })\n  isEmailVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultContactsVisibilitySettings })\n  isTelegramVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultContactsVisibilitySettings })\n  isSkypeVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultContactsVisibilitySettings })\n  isPhoneVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultContactsVisibilitySettings })\n  isContactsNotesVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultVisibilitySettings })\n  isLinkedInVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultVisibilitySettings })\n  isPublicFeedbackVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultVisibilitySettings })\n  isMentorStatsVisible: VisibilitySettings;\n\n  @Column({ type: 'json', default: defaultPartialStudentVisibilitySettings })\n  isStudentStatsVisible: VisibilitySettings;\n}\n"
  },
  {
    "path": "server/src/models/prompt.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, Unique } from 'typeorm';\n\n@Entity()\n@Unique(['type'])\nexport class Prompt {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column({ type: 'varchar', length: 256 })\n  type: string;\n\n  @Column({ type: 'float' })\n  temperature: number;\n\n  @Column()\n  text: string;\n}\n"
  },
  {
    "path": "server/src/models/registry.ts",
    "content": "import { Entity, CreateDateColumn, UpdateDateColumn, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';\nimport { User } from './user';\nimport { Course } from './course';\n\ntype ParticipantType = 'mentor' | 'student';\n\nexport type RegistryStatus = 'inactive' | 'pending' | 'approved' | 'rejected';\n\n@Entity()\nexport class Registry {\n  @PrimaryGeneratedColumn() id: number;\n\n  @ManyToOne(_ => User, (user: User) => user.registries, { nullable: true })\n  user: User | number;\n\n  @Column()\n  userId: number;\n\n  @ManyToOne(_ => Course, (course: Course) => course.registries, { nullable: true })\n  course: Course;\n\n  @Column()\n  courseId: number;\n\n  @Column({ name: 'type' })\n  type: ParticipantType;\n\n  @Column({ name: 'status', default: 'pending' })\n  status: RegistryStatus;\n\n  @Column({ type: 'json', default: {} })\n  attributes: {\n    maxStudentsLimit: number;\n    experienceInYears: string;\n  };\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n}\n"
  },
  {
    "path": "server/src/models/repositoryEvent.ts",
    "content": "import { Column, Index, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';\n\n@Entity()\n@Index(['repositoryUrl'])\n@Index(['githubId'])\nexport class RepositoryEvent {\n  @PrimaryGeneratedColumn() id: number;\n\n  @Column()\n  repositoryUrl: string;\n\n  @Column()\n  action: string;\n\n  @Column()\n  githubId: string;\n\n  @Column({ nullable: true })\n  userId?: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n}\n"
  },
  {
    "path": "server/src/models/resume.ts",
    "content": "import {\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  Index,\n  Generated,\n  UpdateDateColumn,\n  JoinColumn,\n  ManyToOne,\n} from 'typeorm';\nimport { LanguageLevel } from './data';\nimport { User } from './user';\n\n@Entity()\nexport class Resume {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Generated('uuid')\n  @Column({ nullable: true })\n  uuid: string;\n\n  @ManyToOne(_ => User)\n  @JoinColumn({ name: 'userId' })\n  user: User;\n\n  @Column()\n  @Index()\n  userId: number | null;\n\n  @Column({ type: 'varchar', length: 256 })\n  @Index()\n  githubId: string;\n\n  @Column({ nullable: true, type: 'varchar', length: 256 })\n  name: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 256 })\n  selfIntroLink: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 32 })\n  startFrom: string | null;\n\n  @Column({ default: false })\n  fullTime: boolean;\n\n  @Column({ nullable: true, type: 'numeric' })\n  expires: number | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 32 })\n  militaryService: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 8 })\n  englishLevel: LanguageLevel | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 512 })\n  avatarLink: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 256 })\n  desiredPosition: string | null;\n\n  @Column({ nullable: true, type: 'text' })\n  notes: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 32 })\n  phone: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 256 })\n  email: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 128 })\n  skype: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 128 })\n  telegram: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 512 })\n  linkedin: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 512 })\n  locations: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 256 })\n  githubUsername: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 512 })\n  website: string | null;\n\n  @Column({ default: false })\n  isHidden: boolean;\n\n  @Column('int', { array: true, default: [] })\n  visibleCourses: number[];\n}\n"
  },
  {
    "path": "server/src/models/session.ts",
    "content": "export interface JwtToken {\n  id: number;\n  githubId: string;\n  isAdmin: boolean;\n  isHirer: boolean;\n}\n\nexport interface IUserSession {\n  id: number;\n  isAdmin: boolean;\n  isHirer: boolean;\n  githubId: string;\n  courses: Record<number, CourseInfo>;\n}\n\nexport interface CourseInfo {\n  mentorId?: number;\n  studentId?: number;\n  roles: CourseRole[];\n}\n\nexport enum CourseRole {\n  TaskOwner = 'taskOwner',\n  Manager = 'manager',\n  Supervisor = 'supervisor',\n  Student = 'student',\n  Mentor = 'mentor',\n  Dementor = 'dementor',\n  Activist = 'activist',\n}\n\nfunction hasRole(user?: IUserSession, courseId?: number, role?: CourseRole) {\n  return courseId && role ? (user?.courses?.[courseId]?.roles.includes(role) ?? false) : false;\n}\n\nfunction hasRoleInAny(user?: IUserSession, role?: CourseRole) {\n  return Object.keys(user?.courses ?? {}).some(courseId => hasRole(user, Number(courseId), role));\n}\n\nexport const isAdmin = (user?: IUserSession) => user?.isAdmin ?? false;\nexport const isHirer = (user?: IUserSession) => user?.isHirer ?? false;\n\nexport const isAnyManager = (user?: IUserSession) => hasRoleInAny(user, CourseRole.Manager);\nexport const isAnySupervisor = (user?: IUserSession) => hasRoleInAny(user, CourseRole.Supervisor);\nexport const isManager = (user?: IUserSession, courseId?: number) =>\n  isAdmin(user) || hasRole(user, courseId, CourseRole.Manager);\nexport const isDementor = (user?: IUserSession, courseId?: number) =>\n  isAdmin(user) || hasRole(user, courseId, CourseRole.Dementor);\nexport const isMentor = (user?: IUserSession, courseId?: number) => hasRole(user, courseId, CourseRole.Mentor);\nexport const isAnyMentor = (user?: IUserSession) => hasRoleInAny(user, CourseRole.Mentor);\nexport const isStudent = (user?: IUserSession, courseId?: number) => hasRole(user, courseId, CourseRole.Student);\nexport const isTaskOwner = (user?: IUserSession, courseId?: number) => hasRole(user, courseId, CourseRole.TaskOwner);\nexport const isSupervisor = (user?: IUserSession, courseId?: number) => hasRole(user, courseId, CourseRole.Supervisor);\n"
  },
  {
    "path": "server/src/models/stageInterview.ts",
    "content": "import {\n  Entity,\n  Column,\n  CreateDateColumn,\n  ManyToOne,\n  OneToMany,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n  Index,\n} from 'typeorm';\nimport { StageInterviewFeedback } from './stageInterviewFeedback';\nimport { Mentor } from './mentor';\nimport { Student } from './student';\nimport { Course, CourseTask } from '.';\n\n@Entity()\nexport class StageInterview {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => Student, (student: Student) => student.stageInterviews)\n  student: Student;\n\n  @OneToMany(\n    _ => StageInterviewFeedback,\n    (StageInterviewFeedback: StageInterviewFeedback) => StageInterviewFeedback.stageInterview,\n  )\n  stageInterviewFeedbacks: StageInterviewFeedback[];\n\n  @Column()\n  @Index()\n  studentId: number;\n\n  @ManyToOne(_ => Mentor)\n  mentor: Mentor;\n\n  @Column()\n  @Index()\n  mentorId: number;\n\n  @Column({ nullable: true })\n  stageId: number;\n\n  @ManyToOne(_ => CourseTask, { nullable: true })\n  courseTask: CourseTask;\n\n  @Column({ nullable: true })\n  courseTaskId: number;\n\n  @ManyToOne(_ => Course, { nullable: true })\n  course: Course;\n\n  @Column({ nullable: true })\n  courseId: number;\n\n  @Column({ default: false })\n  isCompleted: boolean;\n\n  @Column({ default: false })\n  isCanceled: boolean;\n\n  @Column({ nullable: true })\n  decision: string;\n\n  @Column({ nullable: true })\n  isGoodCandidate: boolean;\n\n  /**\n   * stores interview pre-calculated score\n   */\n  @Column({ nullable: true })\n  score: number;\n}\n"
  },
  {
    "path": "server/src/models/stageInterviewFeedback.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';\nimport { StageInterview } from './stageInterview';\n\n@Entity()\nexport class StageInterviewFeedback {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => StageInterview)\n  stageInterview: StageInterview;\n\n  @Column()\n  stageInterviewId: number;\n\n  @Column()\n  json: string;\n\n  /**\n   * defines the version of free-form `json` feedback.\n   */\n  @Column({ nullable: true })\n  version: number;\n}\n"
  },
  {
    "path": "server/src/models/stageInterviewStudent.ts",
    "content": "import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, Unique } from 'typeorm';\nimport { Course } from '.';\nimport { Student } from './student';\n\n@Entity()\n@Unique(['studentId', 'courseId'])\nexport class StageInterviewStudent {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  studentId: number;\n\n  @ManyToOne(_ => Course)\n  course: Course;\n\n  @Column({ nullable: true })\n  courseId: number;\n}\n"
  },
  {
    "path": "server/src/models/student-feedback.ts",
    "content": "import { LanguageLevel } from './data';\nimport {\n  Column,\n  CreateDateColumn,\n  Entity,\n  Index,\n  JoinColumn,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n  UpdateDateColumn,\n  DeleteDateColumn,\n} from 'typeorm';\nimport { User } from './user';\nimport { Mentor } from './mentor';\nimport { Student } from './student';\n\nexport interface StudentFeedbackContent {\n  suggestions: string;\n  recommendationComment: string;\n  softSkills: { id: SoftSkill; value: Rate }[];\n}\n\nexport enum SoftSkill {\n  Responsible = 'skill.soft.responsible',\n  TeamPlayer = 'skill.soft.team-player',\n  Communicable = 'skill.soft.communicable',\n}\n\nexport enum Rate {\n  None = 'None',\n  Poor = 'Poor',\n  Fair = 'Fair',\n  Good = 'Good',\n  Great = 'Great',\n  Excellent = 'Excellent',\n}\n\nexport enum Recommendation {\n  Hire = 'hire',\n  NotHire = 'not-hire',\n}\n\n@Entity()\nexport class StudentFeedback {\n  @PrimaryGeneratedColumn({ name: 'id' })\n  public id: number;\n\n  @CreateDateColumn({ name: 'created_date' })\n  public createdDate: string;\n\n  @UpdateDateColumn({ name: 'updated_date' })\n  public updatedDate: string;\n\n  @DeleteDateColumn({ name: 'deleted_date' })\n  public deletedDate: string;\n\n  @ManyToOne(_ => Student)\n  @JoinColumn({ name: 'student_id' })\n  public student: Student;\n\n  @Column({ name: 'student_id' })\n  @Index()\n  public studentId: number;\n\n  @ManyToOne(_ => Mentor, { nullable: true })\n  @JoinColumn({ name: 'mentor_id' })\n  public mentor: Mentor;\n\n  @Column({ name: 'mentor_id', nullable: true })\n  @Index()\n  public mentorId: number;\n\n  @Column({ name: 'content', type: 'json' })\n  public content: StudentFeedbackContent;\n\n  @Column({ name: 'recommendation', type: 'varchar', length: 64 })\n  public recommendation: Recommendation;\n\n  @Column({ name: 'english_level', type: 'varchar', length: 8, nullable: true })\n  public englishLevel?: LanguageLevel;\n\n  @Column({ name: 'author_id' })\n  public auhtorId: number;\n\n  @ManyToOne(_ => User)\n  @JoinColumn({ name: 'author_id' })\n  public author: Pick<User, 'id' | 'firstName' | 'lastName'>;\n}\n"
  },
  {
    "path": "server/src/models/student.ts",
    "content": "import {\n  Entity,\n  OneToMany,\n  Column,\n  CreateDateColumn,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n  ManyToOne,\n  OneToOne,\n  Index,\n  Unique,\n  ManyToMany,\n  JoinTable,\n} from 'typeorm';\nimport { User } from './user';\nimport { Course } from './course';\nimport { Mentor } from './mentor';\nimport { Certificate } from './certificate';\nimport { TaskResult } from './taskResult';\nimport { TaskChecker } from './taskChecker';\nimport { TaskInterviewResult } from './taskInterviewResult';\nimport { StudentFeedback } from './student-feedback';\nimport { StageInterview } from './stageInterview';\nimport { Team } from './team';\nimport { TeamDistributionStudent } from './teamDistributionStudent';\n\n@Entity()\n@Unique(['courseId', 'userId'])\nexport class Student {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => Course, (course: Course) => course.students, { nullable: true })\n  course: Course;\n\n  @Column({ nullable: true })\n  @Index()\n  courseId: number;\n\n  @ManyToOne(_ => User)\n  user: User;\n\n  @Column()\n  @Index()\n  userId: number;\n\n  @ManyToOne(_ => Mentor, (mentor: Mentor) => mentor.students, { nullable: true })\n  mentor: Mentor | null;\n\n  @Column({ nullable: true })\n  @Index()\n  mentorId: number | null;\n\n  @OneToMany(_ => TaskResult, (taskResult: TaskResult) => taskResult.student, { nullable: true })\n  taskResults: TaskResult[] | null;\n\n  @OneToMany(_ => StageInterview, (stageInterview: StageInterview) => stageInterview.student, { nullable: true })\n  stageInterviews: StageInterview[] | null;\n\n  @OneToMany(_ => TaskChecker, (taskChecker: TaskChecker) => taskChecker.student, { nullable: true })\n  taskChecker: TaskChecker[] | null;\n\n  @OneToMany(_ => TaskInterviewResult, (taskInterviewResult: TaskInterviewResult) => taskInterviewResult.student, {\n    nullable: true,\n  })\n  taskInterviewResults: TaskInterviewResult[] | null;\n\n  @Column({ default: false })\n  @Index()\n  isFailed: boolean;\n\n  @Column({ default: false })\n  @Index()\n  isExpelled: boolean;\n\n  @Column({ nullable: true })\n  expellingReason: string;\n\n  @Column({ default: false })\n  courseCompleted: boolean;\n\n  @Column({ default: false })\n  isTopPerformer: boolean;\n\n  @Column({ nullable: true })\n  preferedMentorGithubId: string;\n\n  @Column({ nullable: true })\n  readyFullTime: boolean;\n\n  @Column({ nullable: true })\n  cvUrl: string;\n\n  @Column({ nullable: true })\n  repository: string;\n\n  @Column({ nullable: true, type: 'timestamptz' })\n  repositoryLastActivityDate: Date;\n\n  @Column({ nullable: true })\n  hiredById: string;\n\n  @Column({ nullable: true })\n  hiredByName: string;\n\n  @Column({ default: 0, type: 'float' })\n  totalScore: number;\n\n  @Column({ default: 0, type: 'float' })\n  crossCheckScore: number;\n\n  @Column({ default: 999999 })\n  rank: number;\n\n  @Column({ nullable: true, type: 'timestamptz' })\n  totalScoreChangeDate: Date;\n\n  @OneToMany(_ => StudentFeedback, (studentFeedback: StudentFeedback) => studentFeedback.student, { nullable: true })\n  feedbacks: StudentFeedback[] | null;\n\n  @Column({ default: () => \"'1970-01-01 00:00:00+00'\", type: 'timestamptz' })\n  startDate: Date;\n\n  @Column({ type: 'timestamptz', nullable: true })\n  endDate: Date | null;\n\n  @Column({ type: 'text', nullable: true })\n  unassigningComment: string;\n\n  @OneToOne(() => Certificate, certificate => certificate.student)\n  certificate: Certificate | null;\n\n  @Column({ nullable: true, default: true })\n  mentoring: boolean;\n\n  @ManyToMany(() => Team, team => team.students)\n  @JoinTable()\n  teams: Team[];\n\n  @OneToMany(() => TeamDistributionStudent, teamDistributionStudent => teamDistributionStudent.student)\n  teamDistributionStudents: TeamDistributionStudent[];\n}\n"
  },
  {
    "path": "server/src/models/task.ts",
    "content": "import {\n  Entity,\n  Column,\n  OneToMany,\n  CreateDateColumn,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n  JoinColumn,\n  ManyToOne,\n  OneToOne,\n  DeleteDateColumn,\n} from 'typeorm';\nimport { CourseTask } from './courseTask';\nimport { Discipline } from './discipline';\nimport { TaskCriteria } from './taskCriteria';\n\nexport enum TaskType {\n  JSTask = 'jstask',\n  KotlinTask = 'kotlintask',\n  ObjcTask = 'objctask',\n  HtmlTask = 'htmltask',\n  IPynb = 'ipynb',\n  SelfEducation = 'selfeducation',\n  Codewars = 'codewars',\n  Test = 'test',\n  CodeJam = 'codejam',\n  Interview = 'interview',\n  StageInterview = 'stage-interview',\n  CVHtml = 'cv:html',\n  CVMarkdown = 'cv:markdown',\n}\n\n@Entity()\nexport class Task {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @DeleteDateColumn()\n  deletedDate: string;\n\n  @Column()\n  name: string;\n\n  @Column({ nullable: true })\n  descriptionUrl: string;\n\n  @Column({ nullable: true })\n  description: string;\n\n  @Column({ nullable: true })\n  githubPrRequired: boolean;\n\n  @OneToMany(_ => CourseTask, (courseTask: CourseTask) => courseTask.task, { nullable: true })\n  courseTasks: CourseTask[] | null;\n\n  @Column({ nullable: true })\n  verification: 'auto' | 'manual';\n\n  @Column({ nullable: true })\n  githubRepoName: string;\n\n  @Column({ nullable: true })\n  sourceGithubRepoUrl: string;\n\n  @Column({ nullable: true })\n  type: TaskType;\n\n  @Column({ default: false })\n  useJury: boolean;\n\n  @Column({ default: false })\n  allowStudentArtefacts: boolean;\n\n  @Column({ type: 'simple-array', default: '' })\n  tags: string[];\n\n  @Column({ type: 'simple-array', default: '' })\n  skills: string[];\n\n  @ManyToOne(() => Discipline, { nullable: true, onDelete: 'SET NULL' })\n  @JoinColumn({ name: 'disciplineId' })\n  discipline: Discipline | null;\n\n  @Column({ nullable: true })\n  disciplineId: number | null;\n\n  @Column({ type: 'json', default: {} })\n  attributes: Record<string, any>;\n\n  @OneToOne(() => TaskCriteria, (taskCriteria: TaskCriteria) => taskCriteria.taskId, { onDelete: 'CASCADE' })\n  @JoinColumn({ name: 'criteriaId' })\n  criteria: TaskCriteria;\n}\n"
  },
  {
    "path": "server/src/models/taskArtefact.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';\nimport { CourseTask } from './courseTask';\nimport { Student } from './student';\n\n@Entity()\nexport class TaskArtefact {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @Column()\n  courseTaskId: number;\n\n  @Column()\n  studentId: number;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @ManyToOne(_ => CourseTask)\n  courseTask: CourseTask;\n\n  @Column({ nullable: true })\n  videoUrl?: string;\n\n  @Column({ nullable: true })\n  presentationUrl?: string;\n\n  @Column({ nullable: true })\n  comment?: string;\n}\n"
  },
  {
    "path": "server/src/models/taskChecker.ts",
    "content": "import { Entity, CreateDateColumn, ManyToOne, Column, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm';\nimport { Mentor } from './mentor';\nimport { Student } from './student';\nimport { CourseTask } from './courseTask';\n\n@Entity()\nexport class TaskChecker {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => CourseTask)\n  courseTask: CourseTask;\n\n  @Column()\n  courseTaskId: number;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  studentId: number;\n\n  @ManyToOne(_ => Mentor)\n  mentor: Mentor;\n\n  @Column()\n  mentorId: number;\n}\n"
  },
  {
    "path": "server/src/models/taskCriteria.ts",
    "content": "import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm';\n\nexport enum CrossCheckCriteriaType {\n  Title = 'title',\n  Subtask = 'subtask',\n  Penalty = 'penalty',\n}\n\ninterface Criteria {\n  max?: number;\n  type: CrossCheckCriteriaType;\n  text: string;\n  key: string;\n  index: number;\n}\n\n@Entity()\nexport class TaskCriteria {\n  constructor(taskId: number, criteria: Criteria[] = []) {\n    this.taskId = taskId;\n    this.criteria = criteria;\n  }\n  @PrimaryColumn()\n  taskId: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column({ type: 'jsonb', default: [] })\n  criteria: Criteria[];\n}\n"
  },
  {
    "path": "server/src/models/taskInterviewResult.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, ManyToOne, Index } from 'typeorm';\nimport { CourseTask } from './courseTask';\nimport { Student } from './student';\nimport { Mentor } from './mentor';\n\ninterface FormRecords {\n  questionId: string;\n  questionText: string;\n  answer: string;\n}\n\n@Entity()\nexport class TaskInterviewResult {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @Column()\n  @Index()\n  courseTaskId: number;\n\n  @Column()\n  @Index()\n  studentId: number;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @ManyToOne(_ => Mentor)\n  mentor: Mentor;\n\n  @Column()\n  @Index()\n  mentorId: number;\n\n  @ManyToOne(_ => CourseTask)\n  courseTask: CourseTask;\n\n  @Column({ type: 'json', default: [] })\n  formAnswers: FormRecords[];\n\n  @Column({ nullable: true })\n  score?: number;\n\n  @Column({ nullable: true })\n  comment?: string;\n}\n"
  },
  {
    "path": "server/src/models/taskInterviewStudent.ts",
    "content": "import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, Unique } from 'typeorm';\nimport { Course } from './course';\nimport { Student } from './student';\nimport { CourseTask } from './courseTask';\n\n@Entity()\n@Unique(['studentId', 'courseId', 'courseTaskId'])\nexport class TaskInterviewStudent {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  studentId: number;\n\n  @ManyToOne(_ => Course)\n  course: Course;\n\n  @Column({ nullable: true })\n  courseId: number;\n\n  @ManyToOne(_ => CourseTask)\n  courseTask: CourseTask;\n\n  @Column()\n  courseTaskId: number;\n}\n"
  },
  {
    "path": "server/src/models/taskResult.ts",
    "content": "import {\n  Entity,\n  Index,\n  Column,\n  CreateDateColumn,\n  Unique,\n  UpdateDateColumn,\n  PrimaryGeneratedColumn,\n  ManyToOne,\n} from 'typeorm';\nimport { Student } from './student';\nimport { CourseTask } from './courseTask';\nimport { User } from './user';\n\ntype ScoreRecord = {\n  score: number;\n  dateTime: number;\n  comment: string;\n  authorId: number;\n};\n\n@Entity()\n@Index(['courseTaskId'])\n@Index(['studentId'])\n@Unique(['courseTaskId', 'studentId'])\nexport class TaskResult {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  studentId: number;\n\n  @Column()\n  courseTaskId: number;\n\n  @ManyToOne(_ => CourseTask, courseTask => courseTask.taskResults)\n  courseTask: CourseTask;\n\n  @Column({ nullable: true })\n  githubPrUrl: string;\n\n  @Column({ nullable: true })\n  githubRepoUrl: string;\n\n  @Column()\n  score: number;\n\n  @Column({ type: 'json', default: [] })\n  historicalScores: ScoreRecord[] = [];\n\n  @Column({ type: 'json', default: [] })\n  juryScores: ScoreRecord[] = [];\n\n  @Column({ nullable: true })\n  comment?: string;\n\n  @ManyToOne(_ => User)\n  lastChecker?: User;\n\n  @Column({ nullable: true })\n  lastCheckerId?: number;\n}\n"
  },
  {
    "path": "server/src/models/taskSolution.ts",
    "content": "import { Entity, CreateDateColumn, ManyToOne, Column, UpdateDateColumn, PrimaryGeneratedColumn, Unique } from 'typeorm';\nimport { CourseTask } from './courseTask';\nimport { Student } from './student';\n\nexport interface TaskSolutionComment {\n  text: string;\n  criteriaId: string;\n  timestamp: number;\n  authorId: number;\n  recipientId?: number;\n}\n\nexport interface TaskSolutionReview {\n  percentage: number;\n  criteriaId: string;\n}\n\n@Entity()\n@Unique(['courseTaskId', 'studentId'])\nexport class TaskSolution {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column()\n  courseTaskId: number;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  studentId: number;\n\n  @Column()\n  url: string;\n\n  @Column({ type: 'json', default: [] })\n  review: TaskSolutionReview[];\n\n  @Column({ type: 'json', default: [] })\n  comments: TaskSolutionComment[];\n\n  @ManyToOne(_ => CourseTask, courseTask => courseTask.taskSolutions)\n  courseTask: CourseTask;\n}\n"
  },
  {
    "path": "server/src/models/taskSolutionChecker.ts",
    "content": "import { Entity, CreateDateColumn, ManyToOne, Column, UpdateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm';\nimport { Student } from './student';\nimport { TaskSolution } from '.';\n\n@Entity()\nexport class TaskSolutionChecker {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column()\n  @Index()\n  courseTaskId: number;\n\n  @ManyToOne(_ => TaskSolution)\n  taskSolution: TaskSolution;\n\n  @Column()\n  @Index()\n  taskSolutionId: number;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  studentId: number;\n\n  @ManyToOne(_ => Student)\n  checker: Student;\n\n  @Column()\n  @Index()\n  checkerId: number;\n}\n"
  },
  {
    "path": "server/src/models/taskSolutionResult.ts",
    "content": "import {\n  Column,\n  Unique,\n  Index,\n  CreateDateColumn,\n  Entity,\n  ManyToOne,\n  PrimaryGeneratedColumn,\n  UpdateDateColumn,\n} from 'typeorm';\nimport { Student } from './student';\nimport { CourseTask } from './courseTask';\nimport { TaskSolutionReview } from './taskSolution';\nimport { CrossCheckCriteriaType } from './taskCriteria';\n\nexport interface CrossCheckCriteriaData {\n  key: string;\n  max?: number;\n  text: string;\n  type: CrossCheckCriteriaType;\n  point?: number;\n  comment?: string;\n}\n\nexport type ScoreRecord = {\n  score: number;\n  dateTime: number;\n  comment: string;\n  authorId: number;\n  criteria?: CrossCheckCriteriaData[];\n};\n\nexport interface CrossCheckMessageAuthor {\n  id: number;\n  githubId: string;\n}\n\nexport enum CrossCheckMessageAuthorRole {\n  Reviewer = 'reviewer',\n  Student = 'student',\n}\n\nexport interface CrossCheckMessage {\n  timestamp: string;\n  content: string;\n  author: CrossCheckMessageAuthor | null;\n  role: CrossCheckMessageAuthorRole;\n  isReviewerRead: boolean;\n  isStudentRead: boolean;\n}\n\n@Entity()\n@Unique(['courseTaskId', 'studentId', 'checkerId'])\nexport class TaskSolutionResult {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: string;\n\n  @ManyToOne(_ => CourseTask)\n  courseTask: CourseTask;\n\n  @Column()\n  @Index()\n  courseTaskId: number;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  @Index()\n  studentId: number;\n\n  @ManyToOne(_ => Student)\n  checker: Student;\n\n  @Column()\n  @Index()\n  checkerId: number;\n\n  @Column()\n  score: number;\n\n  @Column({ type: 'json', default: [] })\n  historicalScores: ScoreRecord[] = [];\n\n  @Column({ nullable: true })\n  comment?: string;\n\n  @Column({ default: true, type: 'boolean' })\n  anonymous: boolean;\n\n  @Column({ type: 'json', default: [] })\n  review: TaskSolutionReview[];\n\n  @Column({ type: 'json', default: [] })\n  messages: CrossCheckMessage[];\n}\n"
  },
  {
    "path": "server/src/models/taskVerification.ts",
    "content": "import { Entity, CreateDateColumn, ManyToOne, Column, UpdateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm';\nimport { Student } from './student';\nimport { CourseTask } from './courseTask';\n\n@Entity()\nexport class TaskVerification {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: Date;\n\n  @UpdateDateColumn()\n  @Index()\n  updatedDate: Date;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  @Index()\n  studentId: number;\n\n  @ManyToOne(_ => CourseTask)\n  courseTask: CourseTask;\n\n  @Column()\n  @Index()\n  courseTaskId: number;\n\n  @Column({ nullable: true })\n  details: string;\n\n  @Column({ default: 'pending' })\n  status: 'completed' | 'error' | 'pending' | 'cancelled';\n\n  @Column()\n  score: number;\n\n  @Column({ type: 'json', default: [] })\n  metadata: { path: string; md5: string }[];\n\n  @Column({ type: 'json', default: [], select: false })\n  answers: {\n    index: number;\n    value: number | number[];\n    isCorrect: boolean;\n  }[];\n}\n"
  },
  {
    "path": "server/src/models/team.ts",
    "content": "import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, ManyToMany, Index } from 'typeorm';\nimport { Student } from './student';\nimport { TeamDistribution } from './teamDistribution';\n\n@Entity()\nexport class Team {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @Column()\n  name: string;\n\n  @ManyToOne(() => TeamDistribution)\n  teamDistribution: TeamDistribution;\n\n  @Column()\n  @Index()\n  teamDistributionId: number;\n\n  @Column({ default: '' })\n  description: string;\n\n  @ManyToMany(() => Student, student => student.teams, { nullable: true })\n  students: Student[];\n\n  @Column({ nullable: true })\n  teamLeadId: number;\n\n  @Column({ nullable: true })\n  chatLink: string;\n\n  @Column()\n  password: string;\n}\n"
  },
  {
    "path": "server/src/models/teamDistribution.ts",
    "content": "import {\n  Entity,\n  Column,\n  PrimaryGeneratedColumn,\n  CreateDateColumn,\n  UpdateDateColumn,\n  Index,\n  ManyToOne,\n  OneToMany,\n} from 'typeorm';\nimport { Course } from './course';\nimport { CourseTask } from './courseTask';\nimport { Team } from './team';\nimport { TeamDistributionStudent } from './teamDistributionStudent';\n\n@Entity()\nexport class TeamDistribution {\n  @PrimaryGeneratedColumn()\n  id: number;\n\n  @CreateDateColumn()\n  createdDate: Date;\n\n  @UpdateDateColumn()\n  updatedDate: Date;\n\n  @ManyToOne(_ => Course, { nullable: true })\n  course: Course;\n\n  @Column({ nullable: true })\n  @Index()\n  courseId: number;\n\n  @Column({ type: 'timestamptz' })\n  startDate: Date;\n\n  @Column({ type: 'timestamptz' })\n  endDate: Date;\n\n  @Column()\n  name: string;\n\n  @Column({ default: '' })\n  description: string;\n\n  @Column({ default: '' })\n  descriptionUrl: string;\n\n  @OneToMany(_ => CourseTask, courseTask => courseTask.teamDistribution)\n  courseTasks: CourseTask[];\n\n  @Column({ default: 2 })\n  minTeamSize: number;\n\n  @Column({ default: 4 })\n  maxTeamSize: number;\n\n  @Column({ default: 3 })\n  strictTeamSize: number;\n\n  /* if strict mode is true the number of participants in the team is strictly equal to the strictTeamSize\nif strict mode is false the number of participants in the team from minTeamSize to maxTeamSize\n*/\n  @Column({ default: true })\n  strictTeamSizeMode: boolean;\n\n  @Column({ default: 0 })\n  minTotalScore: number;\n\n  @OneToMany(() => Team, team => team.teamDistribution)\n  teams: Team[];\n\n  @OneToMany(() => TeamDistributionStudent, teamDistributionStudent => teamDistributionStudent.teamDistribution)\n  teamDistributionStudents: TeamDistributionStudent[];\n}\n"
  },
  {
    "path": "server/src/models/teamDistributionStudent.ts",
    "content": "import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn, Unique } from 'typeorm';\nimport { Course } from './course';\nimport { Student } from './student';\nimport { TeamDistribution } from './teamDistribution';\n\n@Entity()\n@Unique(['studentId', 'courseId', 'teamDistributionId'])\nexport class TeamDistributionStudent {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: string;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @ManyToOne(_ => Student)\n  student: Student;\n\n  @Column()\n  studentId: number;\n\n  @ManyToOne(_ => Course)\n  course: Course;\n\n  @Column({ nullable: true })\n  courseId: number;\n\n  @ManyToOne(_ => TeamDistribution)\n  teamDistribution: TeamDistribution;\n\n  @Column()\n  teamDistributionId: number;\n\n  @Column({ default: false })\n  distributed: boolean;\n\n  @Column({ default: true })\n  active: boolean;\n}\n"
  },
  {
    "path": "server/src/models/user.ts",
    "content": "import {\n  BeforeInsert,\n  BeforeUpdate,\n  Column,\n  CreateDateColumn,\n  Entity,\n  PrimaryGeneratedColumn,\n  UpdateDateColumn,\n  OneToMany,\n  OneToOne,\n  Index,\n  JoinColumn,\n  Unique,\n} from 'typeorm';\nimport { Student } from './student';\nimport { Mentor } from './mentor';\nimport { ProfilePermissions } from './profilePermissions';\nimport { Feedback } from './feedback';\nimport { Registry } from './registry';\nimport { Discord } from '../../../common/models/profile';\nimport { CourseUser } from './courseUser';\nimport { NotificationUserConnection, Resume } from '.';\nimport { Contributor } from './contributor';\n\nexport interface EducationRecord {\n  graduationYear: number;\n  faculty: string;\n  university: string;\n}\n\nexport interface EmploymentRecord {\n  title: string;\n  dateTo: string;\n  dateFrom: string;\n  companyName: string;\n  toPresent: boolean;\n}\n\ntype EnglishLevel = 'a0' | 'a1' | 'a1+' | 'a2' | 'a2+' | 'b1' | 'b1+' | 'b2' | 'b2+' | 'c1' | 'c1+' | 'c2';\n\ntype TshirtSize = 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl';\n\ntype TshirtFashion = 'male' | 'female' | 'unisex';\n\nexport interface ExternalAccount {\n  service: 'htmlacademy' | 'codeacademy' | 'codewars';\n  username: string;\n}\n\n@Entity()\n@Unique(['providerUserId', 'provider'])\nexport class User {\n  @PrimaryGeneratedColumn() id: number;\n\n  @Column({ nullable: true, type: 'varchar' })\n  primaryEmail?: string | null;\n\n  @Column({ name: 'githubId', unique: true })\n  @Index({ unique: true })\n  githubId: string;\n\n  @Column({ nullable: true, type: 'varchar', length: 64 })\n  @Index()\n  providerUserId?: string | null;\n\n  @Column({ nullable: true, type: 'varchar', length: 32 })\n  provider?: string;\n\n  @Column({ nullable: true })\n  firstName: string;\n\n  @Column({ nullable: true })\n  lastName: string;\n\n  @CreateDateColumn()\n  createdDate?: number;\n\n  @UpdateDateColumn()\n  updatedDate?: number;\n\n  @Column({ nullable: true, type: 'varchar' })\n  firstNameNative?: string | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  lastNameNative?: string | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  tshirtSize?: TshirtSize | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  tshirtFashion?: TshirtFashion | null;\n\n  @Column({ nullable: true, type: 'date' })\n  dateOfBirth?: string | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  locationName?: string | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  locationId?: string | null;\n\n  @Column({ default: false })\n  opportunitiesConsent: boolean;\n\n  @Column({ nullable: true, type: 'text' })\n  cvLink?: string | null;\n\n  @Column({ nullable: true, type: 'text' })\n  militaryService?: string | null;\n\n  @Column({ type: String, nullable: true })\n  englishLevel: EnglishLevel | null;\n\n  @Column({ type: 'text', array: true, default: [] })\n  languages: string[];\n\n  @Column({\n    type: 'json',\n    default: [],\n  })\n  educationHistory: EducationRecord[] = [];\n\n  @Column({\n    type: 'json',\n    default: [],\n  })\n  employmentHistory: EmploymentRecord[] = [];\n\n  @Column({ type: String, nullable: true })\n  epamApplicantId: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsEpamEmail: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsPhone: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsEmail: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsTelegram: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsNotes: string | null;\n\n  @Column({ type: 'json', nullable: true })\n  discord: Discord | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  countryName: string | null;\n\n  @Column({ nullable: true, type: 'varchar' })\n  cityName: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsSkype: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsLinkedIn: string | null;\n\n  @Column({ type: String, nullable: true })\n  contactsWhatsApp: string | null;\n\n  @Column({ type: String, nullable: true })\n  aboutMyself: string | null;\n\n  @Column({\n    type: 'json',\n    default: [],\n  })\n  externalAccounts: ExternalAccount[] = [];\n\n  @OneToMany(_ => Mentor, (mentor: Mentor) => mentor.user, { nullable: true })\n  mentors: Mentor[] | null;\n\n  @OneToMany(() => Resume, (resume: Resume) => resume.user, { nullable: true })\n  resume: Resume[] | null;\n\n  @OneToMany(_ => Student, (student: Student) => student.user, { nullable: true })\n  students: Student[] | null;\n\n  @OneToMany(_ => Feedback, (feedback: Feedback) => feedback.fromUser, { nullable: true })\n  givenFeedback: Feedback[] | null;\n\n  @OneToMany(_ => Feedback, (feedback: Feedback) => feedback.toUser, { nullable: true })\n  receivedFeedback: Feedback[] | null;\n\n  @OneToMany(_ => Registry, (registry: Registry) => registry.course, { nullable: true })\n  registries: Registry[] | null;\n\n  @Column({ type: Boolean, nullable: true })\n  activist: boolean | null;\n\n  @Column({ default: 0, type: 'bigint' })\n  lastActivityTime: number;\n\n  @Column({ default: true })\n  isActive: boolean;\n\n  @Column({ default: false })\n  obfuscated: boolean;\n\n  @OneToOne(() => ProfilePermissions, profilePermissions => profilePermissions.user)\n  @JoinColumn()\n  profilePermissions: ProfilePermissions | null;\n\n  @OneToMany(_ => CourseUser, (courseUser: CourseUser) => courseUser.user, { nullable: true })\n  courseUsers: CourseUser[] | null;\n\n  @OneToOne(() => Contributor, contributor => contributor.user)\n  @JoinColumn({ name: 'contributor_id' })\n  contributor: Contributor | null;\n\n  @Column({ nullable: true, name: 'contributor_id' })\n  contributorId: number | null;\n\n  @BeforeInsert()\n  beforeInsert?() {\n    this.githubId = this.githubId.toLowerCase();\n  }\n\n  @BeforeUpdate()\n  beforeUpdate?() {\n    this.githubId = this.githubId.toLowerCase();\n  }\n\n  @OneToMany(\n    () => NotificationUserConnection,\n    (noitificationConnection: NotificationUserConnection) => noitificationConnection.user,\n    { nullable: true },\n  )\n  notificationConnections: NotificationUserConnection[] | null;\n}\n"
  },
  {
    "path": "server/src/models/userGroup.ts",
    "content": "import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm';\nimport { CourseRole } from './session';\n\n@Entity()\nexport class UserGroup {\n  @PrimaryGeneratedColumn() id: number;\n\n  @CreateDateColumn()\n  createdDate: number;\n\n  @UpdateDateColumn()\n  updatedDate: number;\n\n  @Column()\n  name: string;\n\n  @Column('int', { array: true })\n  users: number[];\n\n  @Column('text', { array: true })\n  roles: CourseRole[];\n}\n"
  },
  {
    "path": "server/src/repositories/courseTask.repository.ts",
    "content": "import { AbstractRepository, EntityRepository, getRepository } from 'typeorm';\nimport {\n  CourseTask,\n  TaskResult,\n  Task,\n  TaskInterviewResult,\n  StageInterview,\n  StageInterviewFeedback,\n  Student,\n} from '../models';\n\ntype Status = 'started' | 'inprogress' | 'finished';\n@EntityRepository(CourseTask)\nexport class CourseTaskRepository extends AbstractRepository<CourseTask> {\n  public async findWithDetails(courseId: number) {\n    const courseTasks = await getRepository(CourseTask)\n      .createQueryBuilder('ct')\n      .addSelect('COUNT(tr.id)', 'taskResultCount')\n      .addSelect('COUNT(tir.id)', 'taskInterviewResultCount')\n      .innerJoinAndSelect('ct.task', 'task')\n      .leftJoin(TaskResult, 'tr', 'tr.courseTaskId = ct.id')\n      .leftJoin(TaskInterviewResult, 'tir', 'tir.courseTaskId = ct.id')\n      .leftJoin('ct.taskOwner', 'to')\n      .addSelect(['to.githubId', 'to.id', 'to.firstName', 'to.lastName'])\n      .where(`ct.courseId = :courseId`, { courseId })\n      .andWhere('ct.disabled = :disabled', { disabled: false })\n      .groupBy('ct.id')\n      .addGroupBy('task.id')\n      .addGroupBy('to.id')\n      .getRawAndEntities();\n\n    const data = courseTasks.entities.map(item => {\n      const raw = courseTasks.raw.find(t => t.ct_id === item.id);\n      return {\n        id: item.id,\n        courseTaskId: item.id,\n        taskId: (item.task as Task).id,\n        name: (item.task as Task).name,\n        maxScore: item.maxScore,\n        scoreWeight: item.scoreWeight,\n        githubPrRequired: !!(item.task as Task).githubPrRequired,\n        description: (item.task as Task).description,\n        descriptionUrl: (item.task as Task).descriptionUrl,\n        studentStartDate: item.studentStartDate,\n        studentEndDate: item.studentEndDate,\n        crossCheckEndDate: item.crossCheckEndDate,\n        crossCheckStatus: item.crossCheckStatus,\n        resultsCount: raw ? Number(raw.taskResultCount) || Number(raw.taskInterviewResultCount) : 0,\n        allowStudentArtefacts: (item.task as Task).allowStudentArtefacts,\n        checker: item.checker,\n        taskOwner: item.taskOwner\n          ? {\n              id: item.taskOwner.id,\n              githubId: item.taskOwner.githubId,\n              name: `${item.taskOwner.firstName} ${item.taskOwner.lastName}`,\n            }\n          : null,\n        taskCheckers: [],\n        githubRepoName: (item.task as Task).githubRepoName,\n        sourceGithubRepoUrl: (item.task as Task).sourceGithubRepoUrl,\n        type: item.type || (item.task as Task).type,\n        pairsCount: item.pairsCount,\n      };\n    });\n\n    return data;\n  }\n\n  public async findForSchedule(courseId: number, userId: number) {\n    const student = await getRepository(Student)\n      .createQueryBuilder('student')\n      .innerJoin('student.user', 'user')\n      .where('student.\"courseId\" = :courseId', { courseId })\n      .andWhere('\"user\".\"id\" = :userId', { userId })\n      .getOne();\n\n    const studentId = student?.id ?? 0;\n    const courseTasks = await getRepository(CourseTask)\n      .createQueryBuilder('courseTask')\n      .addSelect('COUNT(taskResult.id)', 'taskResultCount')\n      .leftJoin(TaskResult, 'taskResult', '\"taskResult\".\"courseTaskId\" = \"courseTask\".\"id\"')\n      .leftJoin(TaskInterviewResult, 'tir', 'tir.courseTaskId = courseTask.id AND \"tir\".\"studentId\" = :studentId', {\n        studentId,\n      })\n      .leftJoin(TaskResult, 'tr', '\"tr\".\"courseTaskId\" = \"courseTask\".\"id\" AND \"tr\".\"studentId\" = :studentId', {\n        studentId,\n      })\n      .leftJoin(StageInterview, 'si', '\"si\".\"studentId\" = :studentId AND \"si\".\"courseTaskId\" = \"courseTask\".\"id\"', {\n        studentId,\n      })\n      .leftJoin(StageInterviewFeedback, 'sif', '\"sif\".\"stageInterviewId\" = \"si\".\"id\"')\n      .addSelect(`COALESCE(tr.score, tir.score, (\"sif\".\"json\"::json -> 'resume' ->> 'score')::int)`, 'score')\n      .innerJoinAndSelect('courseTask.task', 'task')\n      .leftJoin('courseTask.taskOwner', 'taskOwner')\n      .addSelect(['taskOwner.githubId', 'taskOwner.id', 'taskOwner.firstName', 'taskOwner.lastName'])\n      .where(`courseTask.courseId = :courseId`, { courseId })\n      .andWhere('courseTask.disabled = :disabled', { disabled: false })\n      .addGroupBy('courseTask.id')\n      .addGroupBy('task.id')\n      .addGroupBy('taskOwner.id')\n      .addGroupBy('tr.score')\n      .addGroupBy('tir.score')\n      .addGroupBy('tr.id')\n      .addGroupBy('sif.json')\n      .getRawAndEntities();\n\n    const data = courseTasks.entities.map(item => {\n      const raw = courseTasks.raw.find(t => t.courseTask_id === item.id);\n\n      return {\n        id: item.id,\n        courseTaskId: item.id,\n        taskId: (item.task as Task).id,\n        name: (item.task as Task).name,\n        maxScore: item.maxScore,\n        scoreWeight: item.scoreWeight,\n        githubPrRequired: !!(item.task as Task).githubPrRequired,\n        description: (item.task as Task).description,\n        descriptionUrl: (item.task as Task).descriptionUrl,\n        studentStartDate: item.studentStartDate,\n        studentEndDate: item.studentEndDate,\n        resultsCount: raw ? Number(raw.taskResultCount) : 0,\n        allowStudentArtefacts: (item.task as Task).allowStudentArtefacts,\n        checker: item.checker,\n        taskOwner: item.taskOwner\n          ? {\n              id: item.taskOwner.id,\n              githubId: item.taskOwner.githubId,\n              name: `${item.taskOwner.firstName} ${item.taskOwner.lastName}`,\n            }\n          : null,\n        taskCheckers: [],\n        githubRepoName: (item.task as Task).githubRepoName,\n        sourceGithubRepoUrl: (item.task as Task).sourceGithubRepoUrl,\n        type: item.type || (item.task as Task).type,\n        pairsCount: item.pairsCount,\n        score: student && raw ? Number(raw.score) : null,\n      };\n    });\n\n    return data;\n  }\n\n  public async findByCourseId(courseId: number, status?: Status) {\n    const now = new Date().toISOString();\n    const courseTasksQuery = getRepository(CourseTask)\n      .createQueryBuilder('courseTask')\n      .innerJoinAndSelect('courseTask.task', 'task')\n      .where(`courseTask.courseId = :courseId`, { courseId })\n      .andWhere('courseTask.disabled = :disabled', { disabled: false })\n      .orderBy('courseTask.studentEndDate', 'ASC');\n\n    if (status === 'started') {\n      courseTasksQuery.andWhere('courseTask.studentStartDate <= :now', { now });\n    }\n\n    if (status === 'inprogress') {\n      courseTasksQuery.andWhere('courseTask.studentStartDate <= :now', { now });\n      courseTasksQuery.andWhere('courseTask.studentEndDate > :now', { now });\n    }\n\n    if (status === 'finished') {\n      courseTasksQuery.andWhere('courseTask.studentEndDate < :now', { now });\n    }\n\n    const courseTasks = await courseTasksQuery.getMany();\n\n    const data = courseTasks.map(item => ({\n      id: item.id,\n      taskId: item.task.id,\n      name: item.task.name,\n      maxScore: item.maxScore,\n      scoreWeight: item.scoreWeight,\n      githubPrRequired: !!item.task.githubPrRequired,\n      descriptionUrl: item.task.descriptionUrl,\n      studentStartDate: item.studentStartDate,\n      studentEndDate: item.studentEndDate,\n      checker: item.checker,\n      taskOwnerId: item.taskOwnerId,\n      githubRepoName: item.task.githubRepoName,\n      sourceGithubRepoUrl: item.task.sourceGithubRepoUrl,\n      type: item.type || item.task.type,\n      pairsCount: item.pairsCount,\n      publicAttributes: item.task.attributes?.public,\n    }));\n\n    return data;\n  }\n}\n"
  },
  {
    "path": "server/src/repositories/crossCheck.repository.ts",
    "content": "import { EntityRepository, AbstractRepository, getRepository } from 'typeorm';\nimport { TaskSolution, TaskSolutionResult } from '../models';\n\n@EntityRepository(TaskSolution)\nexport class CrossCheckRepository extends AbstractRepository<TaskSolution> {\n  public async findReviewResult(\n    courseTaskId: number,\n    studentId: number,\n    checkerId: number,\n  ): Promise<TaskSolutionResult | null> {\n    const item = await getRepository(TaskSolutionResult)\n      .createQueryBuilder('result')\n      .where('result.\"studentId\" = :studentId', { studentId })\n      .andWhere('result.\"checkerId\" = :checkerId', { checkerId })\n      .andWhere('result.\"courseTaskId\" = :courseTaskId', { courseTaskId })\n      .getOne();\n    return item;\n  }\n\n  public async findSolution(courseTaskId: number, studentId: number): Promise<TaskSolution | null> {\n    const item = await getRepository(TaskSolution)\n      .createQueryBuilder('solution')\n      .where('solution.\"studentId\" = :studentId', { studentId })\n      .andWhere('solution.\"courseTaskId\" = :courseTaskId', { courseTaskId })\n      .getOne();\n    return item;\n  }\n}\n"
  },
  {
    "path": "server/src/repositories/feedback.repository.ts",
    "content": "import { AbstractRepository, EntityRepository, getManager } from 'typeorm';\nimport { Feedback, User } from '../models';\n\nexport type GetGratitudeQuery = {\n  name?: string;\n  githubId?: string;\n  courseId?: number;\n  pageSize?: number;\n  current?: number;\n};\n\n@EntityRepository(Feedback)\nexport class FeedbackRepository extends AbstractRepository<Feedback> {\n  public async getGratitude({ courseId, githubId, name, pageSize = 20, current = 1 }: GetGratitudeQuery) {\n    const queryCount = getManager()\n      .createQueryBuilder()\n      .select('COUNT(*)')\n      .from(Feedback, 'feedback')\n      .innerJoin('feedback.toUser', 'user')\n      .innerJoin('feedback.fromUser', 'fromUser');\n\n    if (githubId) {\n      queryCount.andWhere('\"user\".\"githubId\" ILIKE :githubId', {\n        githubId: `%${githubId}%`,\n      });\n    }\n\n    if (name) {\n      queryCount.andWhere(\n        '\"user\".\"firstName\" ILIKE :searchText OR \"user\".\"lastName\" ILIKE :searchText OR CONCAT(\"user\".\"firstName\", \\' \\', \"user\".\"lastName\") ILIKE :searchText',\n        {\n          searchText: `%${name}%`,\n        },\n      );\n    }\n\n    if (courseId) {\n      queryCount.andWhere('\"feedback\".\"courseId\" = :courseId', {\n        courseId,\n      });\n    }\n\n    const count = await queryCount.getRawOne();\n\n    if (!count.count || Number(count.count) === 0) {\n      return {\n        count: 0,\n        content: [],\n      };\n    }\n\n    const query = this.createQueryBuilder('feedback')\n      .select('feedback.badgeId', 'badgeId')\n      .addSelect('feedback.updatedDate', 'date')\n      .addSelect('feedback.comment', 'comment')\n      .addSelect('feedback.id', 'id')\n      .innerJoin('feedback.toUser', 'user')\n      .addSelect('user.githubId', 'githubId')\n      .addSelect('user.firstName', 'firstName')\n      .addSelect('user.lastName', 'lastName')\n      .addSelect('user.countryName', 'countryName')\n      .addSelect('user.cityName', 'cityName')\n      .addSelect('user.activist', 'activist')\n      .addSelect('user.id', 'user_id')\n      .innerJoin('feedback.fromUser', 'fromUser')\n      .addSelect(\n        'json_build_object(\\'githubId\\', \"fromUser\".\"githubId\", \\'firstName\\', \"fromUser\".\"firstName\", \\'lastName\\', \"fromUser\".\"lastName\")',\n        'from',\n      );\n\n    if (githubId) {\n      query.andWhere('\"user\".\"githubId\" ILIKE :githubId', {\n        githubId: `%${githubId}%`,\n      });\n    }\n\n    if (name) {\n      query.andWhere(\n        '\"user\".\"firstName\" ILIKE :searchText OR \"user\".\"lastName\" ILIKE :searchText OR CONCAT(\"user\".\"firstName\", \\' \\', \"user\".\"lastName\") ILIKE :searchText',\n        {\n          searchText: `%${name}%`,\n        },\n      );\n    }\n\n    if (courseId) {\n      query.addSelect('\"feedback\".\"courseId\"', 'courseId').andWhere('\"feedback\".\"courseId\" = :courseId', {\n        courseId,\n      });\n    }\n\n    query\n      .orderBy('\"feedback\".\"updatedDate\"', 'DESC')\n      .limit(pageSize)\n      .offset((current - 1) * pageSize);\n\n    const content = await query.getRawMany();\n\n    return {\n      content,\n      count: count.count,\n    };\n  }\n\n  public async getResumeFeedback(githubId: string) {\n    return await this.createQueryBuilder('feedback')\n      .select('\"feedback\".\"updatedDate\" AS \"feedbackDate\"')\n      .addSelect('\"feedback\".\"comment\" AS \"comment\"')\n      .leftJoin(User, 'user', '\"user\".\"id\" = \"feedback\".\"toUserId\"')\n      .where('\"user\".\"githubId\" = :githubId', { githubId })\n      .orderBy('\"feedback\".\"updatedDate\"', 'DESC')\n      .getRawMany();\n  }\n\n  public async getApplicantFeedback(githubId: string) {\n    return await this.createQueryBuilder('feedback')\n      .select('\"feedback\".\"badgeId\" AS \"badgeId\"')\n      .leftJoin(User, 'user', '\"user\".\"id\" = \"feedback\".\"toUserId\"')\n      .where('\"user\".\"githubId\" = :githubId', { githubId })\n      .orderBy('\"feedback\".\"updatedDate\"', 'DESC')\n      .getMany();\n  }\n}\n"
  },
  {
    "path": "server/src/repositories/interview.repository.ts",
    "content": "import { EntityRepository, AbstractRepository, getRepository, Brackets } from 'typeorm';\nimport { TaskChecker, TaskInterviewResult, TaskInterviewStudent } from '../models';\nimport { courseService, userService } from '../services';\nimport { InterviewStatus, InterviewDetails, InterviewPair } from '../../../common/models/interview';\n\n@EntityRepository(TaskChecker)\nexport class InterviewRepository extends AbstractRepository<TaskChecker> {\n  public findByInterviewer(courseId: number, githubId: string) {\n    return this.getInterviews(courseId, githubId, 'mentor');\n  }\n\n  public findByStudent(courseId: number, githubId: string): Promise<InterviewDetails[]> {\n    return this.getInterviews(courseId, githubId, 'student');\n  }\n\n  public async addStudent(courseId: number, courseTaskId: number, studentId: number) {\n    const repository = await getRepository(TaskInterviewStudent);\n    let record = await repository.findOne({ where: { courseId, studentId, courseTaskId } });\n    if (record == null) {\n      record = await repository.save({ courseId, studentId, courseTaskId });\n    }\n    return { id: record.id };\n  }\n\n  public async cancelById(id: number) {\n    await getRepository(TaskChecker).delete(id);\n  }\n\n  public async addPair(courseId: number, courseTaskId: number, interviewerGithubId: string, studentGithubId: string) {\n    const [interviewer, student] = await Promise.all([\n      courseService.queryMentorByGithubId(courseId, interviewerGithubId),\n      courseService.queryStudentByGithubId(courseId, studentGithubId),\n    ]);\n    if (interviewer && student) {\n      const record = {\n        courseTaskId: courseTaskId,\n        studentId: student.id,\n        mentorId: interviewer.id,\n      };\n      const current = await this.repository.findOne({\n        where: record,\n      });\n      if (!current) {\n        const {\n          identifiers: [{ id }],\n        } = await this.repository.insert(record);\n        return { id: Number(id) };\n      }\n      return { id: current.id };\n    }\n    return null;\n  }\n\n  public async findRegisteredStudent(courseId: number, courseTaskId: number, studentId: number) {\n    const repository = await getRepository(TaskInterviewStudent);\n    const record = await repository.findOne({ where: { courseId, studentId, courseTaskId } });\n    return record ? { id: record.id } : null;\n  }\n\n  public async findRegisteredStudents(courseId: number, courseTaskId: number) {\n    const repository = await getRepository(TaskInterviewStudent);\n    const records = await repository\n      .createQueryBuilder('is')\n      .innerJoin('is.student', 'student')\n      .innerJoin('student.user', 'user')\n      .addSelect([\n        'student.id',\n        'student.totalScore',\n        'student.mentorId',\n        ...courseService.getPrimaryUserFields('user'),\n      ])\n      .where('is.courseId = :courseId', { courseId })\n      .andWhere('is.courseTaskId = :courseTaskId', { courseTaskId })\n      .andWhere('student.isExpelled = false')\n      .orderBy('student.totalScore', 'DESC')\n      .getMany();\n\n    return records.map(record => ({\n      id: record.student.id,\n      name: userService.createName(record.student.user),\n      githubId: record.student.user.githubId,\n      mentor: record.student.mentorId ? { id: record.student.mentorId } : null,\n      totalScore: record.student.totalScore,\n    }));\n  }\n\n  private async getInterviews(\n    courseId: number,\n    githubId: string,\n    type: 'mentor' | 'student',\n  ): Promise<InterviewDetails[]> {\n    const person =\n      type === 'mentor'\n        ? await courseService.queryMentorByGithubId(courseId, githubId)\n        : await courseService.queryStudentByGithubId(courseId, githubId);\n\n    if (person == null) {\n      return [];\n    }\n\n    const interviews = await this.createQueryBuilder('taskChecker')\n      .innerJoin('taskChecker.courseTask', 'courseTask')\n      .innerJoin('courseTask.task', 'task')\n      .innerJoin('taskChecker.mentor', 'mentor')\n      .innerJoin('mentor.user', 'mUser')\n      .innerJoin('taskChecker.student', 'student')\n      .innerJoin('student.user', 'sUser')\n      .addSelect([\n        'courseTask.id',\n        'courseTask.studentStartDate',\n        'courseTask.studentEndDate',\n        'mentor.id',\n        'student.id',\n        'task.id',\n        'task.name',\n        'task.descriptionUrl',\n        'task.attributes',\n        ...courseService.getPrimaryUserFields('mUser'),\n        ...courseService.getPrimaryUserFields('sUser'),\n      ])\n      .where(`taskChecker.${type === 'mentor' ? 'mentorId' : 'studentId'} = :id`, { id: person.id })\n      .andWhere('task.type = :type', { type: 'interview' })\n      .getMany();\n\n    if (interviews.length === 0) {\n      return [];\n    }\n\n    const taskResults = await getRepository(TaskInterviewResult)\n      .createQueryBuilder('tir')\n      .where(\n        new Brackets(qb => {\n          interviews.forEach((i, index) => {\n            qb.orWhere(`(tir.courseTaskId = :courseTaskId${index} AND tir.mentorId = :mentorId${index})`, {\n              [`courseTaskId${index}`]: i.courseTaskId,\n              [`mentorId${index}`]: i.mentor.id,\n            });\n          });\n        }),\n      )\n      .getMany();\n\n    const students = interviews.map(record => {\n      const { courseTask } = record;\n      const taskResult = taskResults.find(\n        taskResult => taskResult.courseTaskId === record.courseTaskId && record.student.id === taskResult.studentId,\n      );\n      return {\n        id: courseTask.id,\n        name: courseTask.task.name,\n        descriptionUrl: courseTask.task.descriptionUrl,\n        startDate: courseTask.studentStartDate as string,\n        endDate: courseTask.studentEndDate as string,\n        completed: !!taskResult,\n        status: taskResult ? InterviewStatus.Completed : InterviewStatus.NotCompleted,\n        interviewer: {\n          githubId: record.mentor.user.githubId,\n          name: userService.createName(record.mentor.user),\n        },\n        student: {\n          id: record.student.id,\n          githubId: record.student.user.githubId,\n          name: userService.createName(record.student.user),\n        },\n        result: taskResult?.score?.toString() ?? null,\n      };\n    });\n    return students;\n  }\n}\n\nexport interface InterviewInfo extends InterviewPair {\n  name: string;\n  completed: boolean;\n  interviewer: {\n    id: number;\n    name: string;\n    cityName?: string;\n    githubId: string;\n    preference: string;\n  };\n  student: {\n    id: number;\n    name: string;\n    totalScore: number;\n    githubId: string;\n    cityName?: string;\n  };\n}\n"
  },
  {
    "path": "server/src/repositories/mentor.repository.ts",
    "content": "import { AbstractRepository, EntityRepository, getRepository } from 'typeorm';\nimport { MentorBasic } from '../../../common/models';\nimport { Mentor } from '../models';\nimport { userService } from '../services';\n\n@EntityRepository(Mentor)\nexport class MentorRepository extends AbstractRepository<Mentor> {\n  public async findActive(courseId: number, selectStudents = false) {\n    let query = this.getPreparedMentorQuery(selectStudents)\n      .where('mentor.courseId = :courseId', { courseId })\n      .andWhere('mentor.isExpelled = false');\n    if (selectStudents) {\n      query = query.andWhere('students.isExpelled = false');\n    }\n    const result = await query.getMany();\n    return result.map(transformMentor);\n  }\n\n  public async findByGithubId(courseId: number, githubId: string) {\n    const query = this.getPreparedMentorQuery()\n      .where('mentor.courseId = :courseId', { courseId })\n      .andWhere('mUser.githubId = :githubId', { githubId });\n    const result = await query.getOne();\n    return result ? transformMentor(result) : null;\n  }\n\n  public async findActiveWithStudentsLimit(courseId: number) {\n    const query = this.getPreparedMentorQuery(true)\n      .addSelect('mentor.maxStudentsLimit')\n      .where('mentor.courseId = :courseId', { courseId })\n      .andWhere('mentor.isExpelled = false')\n      .andWhere('(students.isExpelled = false OR students IS NULL)');\n    const result = await query.getMany();\n    return result.map(transformMentorWithStudentsLimit);\n  }\n\n  private getPrimaryUserFields(modelName = 'user') {\n    return [\n      `${modelName}.id`,\n      `${modelName}.firstName`,\n      `${modelName}.lastName`,\n      `${modelName}.githubId`,\n      `${modelName}.cityName`,\n      `${modelName}.countryName`,\n    ];\n  }\n\n  private getPreparedMentorQuery(selectStudents = false) {\n    let query = getRepository(Mentor)\n      .createQueryBuilder('mentor')\n      .select(['mentor.id', 'mentor.isExpelled', 'mentor.userId'])\n      .innerJoin('mentor.user', 'mUser')\n      .leftJoin('mentor.students', 'students')\n      .addSelect([...this.getPrimaryUserFields('mUser')]);\n    if (selectStudents) {\n      query = query.addSelect(['students.id', 'students.courseId']);\n    }\n    return query;\n  }\n}\n\nfunction transformMentor(record: Mentor): MentorBasic {\n  return {\n    id: record.id,\n    name: userService.createName(record.user),\n    githubId: record.user.githubId,\n    cityName: record.user.cityName ?? 'Unknown',\n    countryName: record.user.countryName ?? 'Unknown',\n    isActive: !record.isExpelled,\n    students: record.students?.map(s => ({ id: s.id })) ?? [],\n  };\n}\n\nfunction transformMentorWithStudentsLimit(record: Mentor): MentorBasic & { maxStudentsLimit: number } {\n  return {\n    ...transformMentor(record),\n    maxStudentsLimit: record.maxStudentsLimit ?? 0,\n  };\n}\n"
  },
  {
    "path": "server/src/repositories/mentorRegistry.repository.ts",
    "content": "import { AbstractRepository, EntityRepository, getRepository } from 'typeorm';\nimport { MentorRegistry } from '../models';\nimport { AvailableLanguages } from '../models/data';\nimport { userService } from '../services';\n\n@EntityRepository(MentorRegistry)\nexport class MentorRegistryRepository extends AbstractRepository<MentorRegistry> {\n  public async findAll() {\n    const data = await this.getPreparedMentorRegistriesQuery().where('mentorRegistry.canceled = false').getMany();\n    return data.map(transformMentorRegistry);\n  }\n\n  public async register(githubId: string, updateData: Partial<MentorRegistry>) {\n    const user = await userService.getUserByGithubId(githubId);\n\n    if (user == null) {\n      return;\n    }\n    const {\n      maxStudentsLimit,\n      technicalMentoring,\n      preferedStudentsLocation,\n      preferedCourses,\n      languagesMentoring = [],\n    } = updateData;\n\n    const mentorData: Partial<MentorRegistry> = {\n      maxStudentsLimit,\n      preferedStudentsLocation,\n      englishMentoring: languagesMentoring.some(language => language === AvailableLanguages.EN),\n      languagesMentoring,\n      preferedCourses,\n      technicalMentoring,\n      canceled: false,\n    };\n\n    const mentorRegistry = await getRepository(MentorRegistry).findOne({ where: { userId: user.id } });\n    if (mentorRegistry == null) {\n      await getRepository(MentorRegistry).insert({ userId: user.id, ...mentorData });\n    } else {\n      await getRepository(MentorRegistry).update(mentorRegistry.id, { ...mentorData, preselectedCourses: [] });\n    }\n  }\n\n  public async update(githubId: string, updateData: { preselectedCourses: string[] }) {\n    const data: Partial<MentorRegistry> = updateData;\n    const user = await userService.getUserByGithubId(githubId);\n    if (user == null) {\n      return;\n    }\n    await getRepository(MentorRegistry).update({ userId: user.id }, data);\n  }\n\n  private getPreparedMentorRegistriesQuery() {\n    return getRepository(MentorRegistry)\n      .createQueryBuilder('mentorRegistry')\n      .innerJoin('mentorRegistry.user', 'user')\n      .addSelect([\n        'user.id',\n        'user.firstName',\n        'user.lastName',\n        'user.githubId',\n        'user.primaryEmail',\n        'user.cityName',\n        'user.contactsEpamEmail',\n      ])\n      .leftJoin('user.mentors', 'mentor')\n      .leftJoin('user.students', 'student')\n      .leftJoin('student.certificate', 'certificate')\n      .addSelect(['mentor.id', 'mentor.courseId', 'student.id', 'certificate.id'])\n      .orderBy('\"mentorRegistry\".\"updatedDate\"', 'DESC');\n  }\n}\n\nfunction transformMentorRegistry(mentorRegistry: MentorRegistry) {\n  const user = mentorRegistry.user;\n  return {\n    id: mentorRegistry.id,\n    englishMentoring: mentorRegistry.englishMentoring,\n    languagesMentoring: mentorRegistry.languagesMentoring,\n    githubId: user.githubId,\n    primaryEmail: user.primaryEmail,\n    contactsEpamEmail: user.contactsEpamEmail,\n    cityName: user.cityName,\n    maxStudentsLimit: mentorRegistry.maxStudentsLimit,\n    name: `${user.firstName} ${user.lastName}`,\n    preferedCourses: mentorRegistry.preferedCourses?.map(id => Number(id)),\n    preselectedCourses: mentorRegistry.preselectedCourses?.map(id => Number(id)),\n    preferedStudentsLocation: mentorRegistry.preferedStudentsLocation,\n    technicalMentoring: mentorRegistry.technicalMentoring,\n    updatedDate: mentorRegistry.updatedDate,\n    courses: mentorRegistry.user.mentors?.map(m => m.courseId),\n    hasCertificate: mentorRegistry.user.students?.some(s => s.certificate?.id),\n  };\n}\n"
  },
  {
    "path": "server/src/repositories/repositoryEvent.repository.ts",
    "content": "import { AbstractRepository, EntityRepository, getRepository } from 'typeorm';\nimport { RepositoryEvent, User } from '../models';\n\n@EntityRepository(RepositoryEvent)\nexport class RepositoryEventRepository extends AbstractRepository<RepositoryEvent> {\n  public async save(events: Pick<RepositoryEvent, 'repositoryUrl' | 'action' | 'githubId'>[]): Promise<void> {\n    const repository = getRepository(RepositoryEvent);\n    const userRepository = getRepository(User);\n    const cache = new Map<string, number>();\n\n    for (const event of events) {\n      let userId: number | undefined;\n      if (cache.has(event.githubId)) {\n        userId = cache.get(event.githubId);\n      } else {\n        const [user] = await userRepository.find({\n          select: ['id', 'githubId'],\n          where: { githubId: event.githubId },\n        });\n        userId = user?.id;\n        cache.set(event.githubId, userId);\n      }\n      await repository.save({ ...event, userId });\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/repositories/stageInterview.repository.ts",
    "content": "import { EntityRepository, AbstractRepository, getRepository } from 'typeorm';\nimport { InterviewStatus, InterviewDetails } from '../../../common/models/interview';\nimport { StageInterview, CourseTask, StageInterviewStudent } from '../models';\nimport { courseService, userService } from '../services';\nimport { InterviewInfo } from './interview.repository';\nimport { createInterviews } from '../rules/interviews';\nimport { queryMentorByGithubId, queryStudentByGithubId } from '../services/course.service';\nimport { TaskType } from '../models/task';\n\n@EntityRepository(StageInterview)\nexport class StageInterviewRepository extends AbstractRepository<StageInterview> {\n  public findByInterviewer(courseId: number, githubId: string) {\n    return this.find(courseId, githubId, 'mentor');\n  }\n\n  public findByStudent(courseId: number, githubId: string) {\n    return this.find(courseId, githubId, 'student');\n  }\n\n  public async findMany(courseId: number): Promise<InterviewInfo[]> {\n    const stageInterviews = await this.createQueryBuilder('si')\n      .innerJoin('si.courseTask', 'courseTask')\n      .innerJoin('courseTask.task', 'task')\n      .innerJoin('si.mentor', 'mentor')\n      .innerJoin('si.student', 'student')\n      .innerJoin('mentor.user', 'mUser')\n      .innerJoin('student.user', 'sUser')\n      .addSelect([\n        'courseTask.id',\n        'courseTask.studentStartDate',\n        'courseTask.studentEndDate',\n        'task.id',\n        'task.name',\n        'mentor.id',\n        'mentor.studentsPreference',\n        'student.id',\n        'student.totalScore',\n        ...courseService.getPrimaryUserFields('mUser'),\n        ...courseService.getPrimaryUserFields('sUser'),\n      ])\n      .where('si.courseId = :courseId', { courseId })\n      .andWhere(`si.isCanceled <> :canceled`, { canceled: true })\n      .orderBy('si.updatedDate', 'DESC')\n      .getMany();\n\n    const result = stageInterviews.map(it => {\n      return {\n        id: it.id,\n        name: it.courseTask.task.name,\n        startDate: it.courseTask.studentStartDate,\n        endDate: it.courseTask.studentEndDate,\n        completed: it.isCompleted,\n        result: null,\n        status: it.isCompleted\n          ? InterviewStatus.Completed\n          : it.isCanceled\n            ? InterviewStatus.Canceled\n            : InterviewStatus.NotCompleted,\n        student: {\n          id: it.student.id,\n          totalScore: it.student.totalScore,\n          cityName: it.student.user.cityName ?? undefined,\n          countryName: it.student.user.countryName ?? undefined,\n          githubId: it.student.user.githubId,\n          name: userService.createName(it.student.user),\n        },\n        interviewer: {\n          id: it.mentor.id,\n          cityName: it.mentor.user.cityName ?? undefined,\n          countryName: it.mentor.user.countryName ?? undefined,\n          githubId: it.mentor.user.githubId,\n          name: userService.createName(it.mentor.user),\n          preference: it.mentor.studentsPreference ?? 'any',\n        },\n      };\n    });\n    return result;\n  }\n\n  public async create(courseId: number, studentGithubId: string, interviewerGithubId: string) {\n    const courseTask = await getRepository(CourseTask).findOne({\n      where: { courseId, type: TaskType.StageInterview, disabled: false },\n    });\n\n    const [student, interviewer] = await Promise.all([\n      courseService.queryStudentByGithubId(courseId, studentGithubId),\n      courseService.queryMentorByGithubId(courseId, interviewerGithubId),\n    ]);\n\n    if (courseTask == null || student == null || interviewer == null) {\n      return null;\n    }\n\n    const interview = await getRepository(StageInterview).save({\n      courseId,\n      mentorId: interviewer.id,\n      studentId: student.id,\n      courseTaskId: courseTask.id,\n    });\n\n    return interview;\n  }\n\n  public async updateInterviewer(id: number, githubId: string) {\n    const interview = await getRepository(StageInterview).findOneBy({ id });\n    if (interview) {\n      const mentor = await courseService.queryMentorByGithubId(interview?.courseId, githubId);\n      if (mentor) {\n        getRepository(StageInterview).update(id, { mentorId: mentor.id });\n      }\n    }\n  }\n\n  public async findStudent(courseId: number, studentId: number) {\n    const repository = getRepository(StageInterviewStudent);\n    const record = await repository.findOne({ where: { courseId, studentId } });\n    return record ? { id: record.id } : null;\n  }\n\n  public async findStudents(courseId: number) {\n    const repository = getRepository(StageInterviewStudent);\n    const records = await repository\n      .createQueryBuilder('sis')\n      .innerJoin('sis.student', 'student')\n      .innerJoin('student.user', 'user')\n      .addSelect([\n        'student.id',\n        'student.totalScore',\n        'student.mentorId',\n        ...courseService.getPrimaryUserFields('user'),\n      ])\n      .where('sis.courseId = :courseId AND student.isExpelled = false AND student.mentorId IS NULL', { courseId })\n      .getMany();\n\n    return records.map(record => ({\n      id: record.student.id,\n      name: userService.createName(record.student.user),\n      githubId: record.student.user.githubId,\n      cityName: record.student.user.cityName ?? 'Other',\n      countryName: record.student.user.countryName ?? 'Other',\n      mentor: record.student.mentorId ? { id: record.student.mentorId } : null,\n      totalScore: record.student.totalScore,\n    }));\n  }\n\n  public async createAutomatically(courseId: number, noRegistration: boolean = false) {\n    const courseTasks = await getRepository(CourseTask).find({\n      where: { courseId, type: TaskType.StageInterview, disabled: false },\n    });\n    if (courseTasks.length === 0) {\n      return [];\n    }\n    if (courseTasks.length > 1) {\n      throw new Error('More than one stage interview task');\n    }\n    const [courseTask] = courseTasks;\n    const mentors = await courseService.getMentorsWithStudents(courseId);\n\n    const students = noRegistration\n      ? await courseService.getStudents(courseId, true)\n      : await this.findStudents(courseId);\n    const interviews = await this.findMany(courseId);\n\n    const distibution = createInterviews(mentors, students, interviews);\n\n    const result = await getRepository(StageInterview).save(\n      distibution.map(pair => ({\n        courseTaskId: courseTask?.id,\n        courseId,\n        mentorId: pair.mentor.id,\n        studentId: pair.student.id,\n      })),\n    );\n\n    return result;\n  }\n\n  public async cancelByMentor(courseId: number, githubId: string) {\n    const mentor = await queryMentorByGithubId(courseId, githubId);\n    if (mentor) {\n      const interviews = await getRepository(StageInterview)\n        .createQueryBuilder('s')\n        .select(['s.id'])\n        .leftJoin('s.stageInterviewFeedbacks', 'f')\n        .addSelect(['f.id'])\n        .where('f.id IS NULL')\n        .andWhere('s.mentorId = :mentorId', { mentorId: mentor.id })\n        .getMany();\n      if (interviews.length > 0) {\n        await getRepository(StageInterview).update(\n          interviews.map(i => i.id),\n          { isCanceled: true },\n        );\n      }\n    }\n  }\n\n  public async cancelByStudent(courseId: number, githubId: string) {\n    const student = await queryStudentByGithubId(courseId, githubId);\n    if (student == null) {\n      return;\n    }\n    await getRepository(StageInterview).update(\n      {\n        studentId: student.id,\n        isCompleted: false,\n      },\n      { isCanceled: true },\n    );\n  }\n\n  private async find(courseId: number, githubId: string, userType: 'student' | 'mentor') {\n    const userKey = userType === 'student' ? 'sUser' : 'mUser';\n\n    const stageInterviews = await getRepository(StageInterview)\n      .createQueryBuilder('stageInterview')\n      .innerJoin('stageInterview.courseTask', 'courseTask')\n      .innerJoin('courseTask.task', 'task')\n      .innerJoin('stageInterview.mentor', 'mentor')\n      .innerJoin('stageInterview.student', 'student')\n      .innerJoin('mentor.user', 'mUser')\n      .innerJoin('student.user', 'sUser')\n      .addSelect([\n        'courseTask.id',\n        'task.id',\n        'task.name',\n        'task.descriptionUrl',\n        'courseTask.studentStartDate',\n        'courseTask.studentEndDate',\n        'student.id',\n        'mentor.id',\n        ...courseService.getPrimaryUserFields('mUser'),\n        ...courseService.getPrimaryUserFields('sUser'),\n      ])\n      .where(`stageInterview.courseId = :courseId AND ${userKey}.githubId = :githubId`, { courseId, githubId })\n      .andWhere(`stageInterview.isCanceled <> :canceled`, { canceled: true })\n      .andWhere(`${userType === 'student' ? 'mentor' : 'student'}.isExpelled = false`)\n      .getMany();\n\n    const result = stageInterviews.map(it => {\n      return {\n        id: it.id,\n        name: it.courseTask.task.name,\n        completed: it.isCompleted,\n        status: it.isCompleted\n          ? InterviewStatus.Completed\n          : it.isCanceled\n            ? InterviewStatus.Canceled\n            : InterviewStatus.NotCompleted,\n        descriptionUrl: it.courseTask.task.descriptionUrl,\n        startDate: it.courseTask.studentStartDate,\n        endDate: it.courseTask.studentEndDate,\n        result: it.decision ?? null,\n        interviewer: { githubId: it.mentor.user.githubId, name: userService.createName(it.mentor.user) },\n        decision: it.decision,\n        student: {\n          id: it.student.id,\n          githubId: it.student.user.githubId,\n          name: userService.createName(it.student.user),\n        },\n      };\n    });\n    return result as InterviewDetails[];\n  }\n}\n"
  },
  {
    "path": "server/src/repositories/stageInterviewFeedback.repository.ts",
    "content": "import { EntityRepository, AbstractRepository, getRepository } from 'typeorm';\nimport { StageInterview, StageInterviewFeedback } from '../models';\n\n@EntityRepository(StageInterviewFeedback)\nexport class StageInterviewFeedbackRepository extends AbstractRepository<StageInterviewFeedback> {\n  public find(interviewId: number, interviewerGithubId: string) {\n    return getRepository(StageInterviewFeedback)\n      .createQueryBuilder('sif')\n      .innerJoin('sif.stageInterview', 'stageInterview')\n      .innerJoin('stageInterview.mentor', 'mentor')\n      .innerJoin('mentor.user', 'user')\n      .where('sif.stageInterviewId = :id', { id: interviewId })\n      .andWhere('user.githubId = :userId', { userId: interviewerGithubId })\n      .getOne();\n  }\n\n  public findByStudent(courseId: number, githubId: string, mentorGithubId: string) {\n    return getRepository(StageInterviewFeedback)\n      .createQueryBuilder('sif')\n      .innerJoin('sif.stageInterview', 'stageInterview')\n      .innerJoin('stageInterview.mentor', 'mentor')\n      .innerJoin('stageInterview.student', 'student')\n      .innerJoin('mentor.user', 'mUser')\n      .innerJoin('student.user', 'sUser')\n      .where('stageInterview.courseId = :courseId', { courseId })\n      .andWhere('sUser.githubId = :githubId', { githubId })\n      .andWhere('mUser.githubId = :userId', { userId: mentorGithubId })\n      .getOne();\n  }\n\n  /**\n   * @deprecated. should be removed after feedbacks are migrated to new template\n   */\n  public async create(\n    stageInterviewId: number,\n    data: { json: any; decision: string | null; isGoodCandidate: boolean | null; isCompleted: boolean },\n  ) {\n    const repository = getRepository(StageInterviewFeedback);\n    const feedback = await repository.findOne({ where: { stageInterviewId } });\n    const newFeedback = { stageInterviewId, json: data.json };\n\n    if (feedback) {\n      await repository.update(feedback.id, newFeedback);\n    } else {\n      await repository.insert(newFeedback);\n    }\n\n    const interview: any = { isCompleted: data.isCompleted };\n\n    if (data.decision) {\n      interview.decision = data.decision;\n    }\n\n    if (data.isGoodCandidate) {\n      interview.isGoodCandidate = data.isGoodCandidate;\n    }\n\n    await getRepository(StageInterview).update(stageInterviewId, interview);\n  }\n}\n"
  },
  {
    "path": "server/src/repositories/student.repository.ts",
    "content": "import { EntityRepository, AbstractRepository, getRepository, getCustomRepository } from 'typeorm';\nimport { Course, Mentor, Student, User, Certificate } from '../models';\nimport { userService } from '../services';\nimport { StudentBasic, UserBasic } from '../../../common/models';\nimport { StageInterviewRepository } from './stageInterview.repository';\nimport { getFullName } from '../rules';\n\n@EntityRepository(Student)\nexport class StudentRepository extends AbstractRepository<Student> {\n  public async expel(courseId: number, githubId: string, comment = '') {\n    const student = await this.findByGithubId(courseId, githubId);\n    if (student == null) {\n      return;\n    }\n    await getRepository(Student).update(student.id, {\n      mentorId: null,\n      isExpelled: true,\n      expellingReason: comment || '',\n      endDate: new Date(),\n    });\n\n    const repo = getCustomRepository(StageInterviewRepository);\n    await repo.cancelByStudent(courseId, githubId);\n  }\n\n  public async setSelfStudy(courseId: number, githubId: string, comment = '') {\n    const student = await this.findByGithubId(courseId, githubId);\n    if (student == null) {\n      return;\n    }\n    await getRepository(Student).update(student.id, {\n      mentorId: null,\n      mentoring: false,\n      expellingReason: comment || '',\n    });\n  }\n\n  public async restore(courseId: number, githubId: string) {\n    const student = await this.findByGithubId(courseId, githubId);\n    if (student == null) {\n      return;\n    }\n    await getRepository(Student).update(student.id, {\n      isExpelled: false,\n      expellingReason: '',\n      endDate: null,\n    });\n  }\n\n  public async setMentor(courseId: number, studentGithubId: string, mentorId?: number) {\n    const student = await this.findByGithubId(courseId, studentGithubId);\n    if (student == null) {\n      return;\n    }\n\n    await getRepository(Student).update(student.id, { mentorId: mentorId ?? null });\n  }\n\n  public async setMentorsBatch(pairs: { mentor: { id: number }; student: { id: number } }[]) {\n    const records = pairs.map(({ student, mentor }) => ({ id: student.id, mentorId: mentor.id }));\n    await getRepository(Student).save(records);\n  }\n\n  public async search(\n    courseId: number,\n    searchText: string,\n    onlyStudentsWithoutMentorShown: boolean,\n  ): Promise<(UserBasic & { mentor: UserBasic | null })[]> {\n    const searchQuery = `${searchText}%`;\n\n    const query = getRepository(Student)\n      .createQueryBuilder('student')\n      .select([`student.id`, 'mentor.id'])\n      .addSelect(this.getBasicUserFields('user'))\n      .addSelect(this.getBasicUserFields('mUser'))\n      .leftJoin('student.user', 'user')\n      .leftJoin('student.mentor', 'mentor')\n      .leftJoin('mentor.user', 'mUser')\n      .where('student.courseId = :courseId')\n      .andWhere('student.isExpelled = false')\n      .andWhere(\n        `(\n          user.githubId ILIKE :searchQuery OR\n          user.firstName ILIKE :searchQuery OR\n          user.lastName ILIKE :searchQuery\n        )`,\n        { courseId, searchQuery },\n      );\n\n    if (onlyStudentsWithoutMentorShown) {\n      query.andWhere('mentor.id IS NULL');\n    }\n\n    const entities = await query.limit(20).getMany();\n\n    return entities.map(entity => ({\n      id: entity.id,\n      githubId: entity.user.githubId,\n      name: userService.createName(entity.user),\n      mentor: entity.mentor?.user\n        ? {\n            id: entity.mentor.id,\n            githubId: entity.mentor.user.githubId,\n            name: userService.createName(entity.mentor.user),\n          }\n        : null,\n    }));\n  }\n\n  public async findAndIncludeMentor(courseId: number, githubId: string): Promise<StudentBasic | null> {\n    const record = await this.getPreparedStudentQuery()\n      .where('sUser.githubId = :githubId', { githubId })\n      .andWhere('student.courseId = :courseId', { courseId })\n      .getOne();\n\n    if (record == null) {\n      return null;\n    }\n\n    return transformStudent(record);\n  }\n\n  public async findAndIncludeDetails(courseId: number, githubId: string) {\n    const query = getRepository(Student)\n      .createQueryBuilder('student')\n      .innerJoin('student.user', 'sUser')\n      .leftJoin('student.mentor', 'mentor')\n      .leftJoin('mentor.user', 'mUser')\n      .addSelect([\n        'mentor.id',\n        'mentor.isExpelled',\n        'mentor.userId',\n        ...this.getPrimaryUserFields('sUser'),\n        ...this.getPrimaryUserFields('mUser'),\n      ])\n      .where('sUser.githubId = :githubId', { githubId })\n      .andWhere('student.courseId = :courseId', { courseId });\n\n    const record = await query.getOne();\n    if (record == null) {\n      return null;\n    }\n\n    return {\n      ...transformStudent(record),\n      expellingReason: record.expellingReason,\n      totalScore: record.totalScore,\n    };\n  }\n\n  public async findByMentor(courseId: number, githubId: string): Promise<StudentBasic[]> {\n    const records = await this.getPreparedStudentQuery()\n      .where('mUser.githubId = :githubId', { githubId })\n      .andWhere('student.isExpelled = false')\n      .andWhere('student.courseId = :courseId ', { courseId })\n      .getMany();\n\n    return records.map(transformStudent);\n  }\n\n  public async findActiveByCourseId(courseId: number) {\n    const records = await this.getPreparedStudentQuery()\n      .where('student.courseId = :courseId ', { courseId })\n      .andWhere('student.isExpelled = false')\n      .getMany();\n\n    return records.map(transformStudent);\n  }\n\n  public async findAndIncludeRepository(courseId: number) {\n    const query = await getRepository(Student)\n      .createQueryBuilder('student')\n      .innerJoin('student.user', 'sUser')\n      .addSelect(['student.id', 'sUser.githubId'])\n      .where('student.courseId = :courseId', { courseId })\n      .andWhere('student.isExpelled = false AND student.isFailed = false')\n      .andWhere('student.repository IS NOT NULL');\n    const items = await query.getMany();\n    return items.map(m => m.user.githubId);\n  }\n\n  public async findForExpel(\n    courseId: number,\n    criteria: {\n      courseTaskIds: number[];\n      minScore: number | null;\n    },\n    options: { keepWithMentor?: boolean },\n  ): Promise<{ id: number }[]> {\n    let query = getRepository(Student).createQueryBuilder('student').select(['student.id']);\n\n    if (criteria.courseTaskIds.length > 0) {\n      query = query.leftJoin(\n        'student.taskResults',\n        'tr',\n        'tr.studentId = student.id AND tr.score > 0 AND tr.courseTaskId IN (:...requiredCourseTaskIds)',\n        {\n          requiredCourseTaskIds: criteria.courseTaskIds,\n        },\n      );\n    }\n\n    query = query.where('student.courseId = :courseId', { courseId }).andWhere('student.isExpelled = false');\n\n    if (options.keepWithMentor) {\n      query = query.andWhere('student.mentorId IS NULL');\n    }\n\n    if (criteria.minScore != null) {\n      query = query.andWhere('student.totalScore < :minScore', { minScore: criteria.minScore });\n    }\n\n    if (criteria.courseTaskIds.length > 0) {\n      query = query.andWhere('tr.id IS NULL');\n    }\n\n    return query.getMany();\n  }\n\n  public async findAndIncludeStatsForResume(githubId: string) {\n    const query = await getRepository(Student)\n      .createQueryBuilder('student')\n      .addSelect('\"course\".\"id\" AS \"courseId\"')\n      .addSelect('\"course\".\"name\" AS \"courseName\"')\n      .addSelect('\"course\".\"locationName\" AS \"locationName\"')\n      .addSelect('\"course\".\"fullName\" AS \"courseFullName\"')\n      .addSelect('\"course\".\"completed\" AS \"isCourseCompleted\"')\n      .addSelect('\"student\".\"totalScore\" AS \"totalScore\"')\n      .addSelect('\"userMentor\".\"firstName\" AS \"mentorFirstName\"')\n      .addSelect('\"userMentor\".\"lastName\" AS \"mentorLastName\"')\n      .addSelect('\"userMentor\".\"githubId\" AS \"mentorGithubId\"')\n      .addSelect('\"certificate\".\"publicId\" AS \"certificateId\"');\n\n    query\n      .leftJoin(User, 'user', '\"user\".\"id\" = \"student\".\"userId\"')\n      .leftJoin(Certificate, 'certificate', '\"certificate\".\"studentId\" = \"student\".\"id\"')\n      .leftJoin(Course, 'course', '\"course\".\"id\" = \"student\".\"courseId\"')\n      .leftJoin(Mentor, 'mentor', '\"mentor\".\"id\" = \"student\".\"mentorId\"')\n      .leftJoin(User, 'userMentor', '\"userMentor\".\"id\" = \"mentor\".\"userId\"');\n\n    query\n      .where('\"user\".\"githubId\" = :githubId', { githubId })\n      .andWhere('\"student\".\"isExpelled\" != :expelled', { expelled: true })\n      .groupBy('\"course\".\"id\", \"student\".\"id\", \"userMentor\".\"id\", \"certificate\".\"publicId\"')\n      .orderBy('\"course\".\"endDate\"', 'DESC');\n\n    const rawStats = await query.getRawMany();\n\n    const studentStats = rawStats.map(\n      ({\n        courseId,\n        courseName,\n        locationName,\n        courseFullName,\n        isCourseCompleted,\n        totalScore,\n        mentorFirstName,\n        mentorLastName,\n        mentorGithubId,\n        certificateId,\n        student_rank,\n      }: any) => {\n        return {\n          courseId,\n          courseName,\n          locationName,\n          courseFullName,\n          isCourseCompleted,\n          totalScore,\n          certificateId,\n          rank: student_rank,\n          mentor: {\n            githubId: mentorGithubId,\n            name: getFullName(mentorFirstName, mentorLastName, mentorGithubId),\n          },\n        };\n      },\n    );\n\n    return studentStats;\n  }\n\n  public async findStudentCourses(githubId: string) {\n    const query = await getRepository(Student)\n      .createQueryBuilder('student')\n      .addSelect('\"course\".\"id\" AS \"courseId\"')\n      .addSelect('\"course\".\"fullName\" AS \"courseFullName\"');\n\n    query\n      .leftJoin(User, 'user', '\"user\".\"id\" = \"student\".\"userId\"')\n      .leftJoin(Course, 'course', '\"course\".\"id\" = \"student\".\"courseId\"');\n\n    query\n      .where('\"user\".\"githubId\" = :githubId', { githubId })\n      .andWhere('\"student\".\"isExpelled\" != :expelled', { expelled: true })\n      .orderBy('\"course\".\"endDate\"', 'DESC');\n\n    const rawStats = await query.getRawMany();\n\n    const studentStats = rawStats.map(({ courseId, courseFullName }: any) => {\n      return {\n        courseId,\n        courseFullName,\n      };\n    });\n\n    return studentStats;\n  }\n\n  public async save(students: Partial<Student>[]) {\n    await getRepository(Student).save(students);\n  }\n\n  public async updateRepositoryActivityDate(repositoryUrl: string) {\n    await getRepository(Student).update(\n      {\n        repository: repositoryUrl,\n      },\n      {\n        repositoryLastActivityDate: new Date(),\n      },\n    );\n  }\n\n  public async updateMentoringAvailability(studentId: number, value: boolean) {\n    await getRepository(Student).update(studentId, { mentoring: value });\n  }\n\n  private async findByGithubId(courseId: number, githubId: string): Promise<UserBasic | null> {\n    const record = await getRepository(Student)\n      .createQueryBuilder('student')\n      .select(['student.id'])\n      .innerJoin('student.user', 'user')\n      .addSelect(['user.firstName', 'user.lastName', 'user.githubId'])\n      .where('user.githubId = :githubId', { githubId })\n      .andWhere('student.courseId = :courseId', { courseId })\n      .getOne();\n    if (record == null) {\n      return null;\n    }\n    return {\n      id: record.id,\n      name: userService.createName(record.user),\n      githubId: record.user.githubId,\n    };\n  }\n\n  private getBasicUserFields(modelName = 'user') {\n    return [`${modelName}.id`, `${modelName}.firstName`, `${modelName}.lastName`, `${modelName}.githubId`];\n  }\n\n  private getPrimaryUserFields(modelName = 'user') {\n    return [\n      `${modelName}.id`,\n      `${modelName}.firstName`,\n      `${modelName}.lastName`,\n      `${modelName}.githubId`,\n      `${modelName}.cityName`,\n      `${modelName}.countryName`,\n    ];\n  }\n\n  private getPreparedStudentQuery() {\n    return getRepository(Student)\n      .createQueryBuilder('student')\n      .select(['student.id', 'student.isExpelled', 'student.mentorId', 'student.isFailed'])\n      .innerJoin('student.user', 'sUser')\n      .leftJoin('student.mentor', 'mentor')\n      .leftJoin('mentor.user', 'mUser')\n      .addSelect([\n        'mentor.id',\n        'mentor.isExpelled',\n        'mentor.userId',\n        ...this.getPrimaryUserFields('sUser'),\n        ...this.getPrimaryUserFields('mUser'),\n      ]);\n  }\n\n  public async findByCriteria(\n    courseId: number,\n    criteria: {\n      courseTaskIds: number[];\n      minScore: number | null;\n      minTotalScore: number | null;\n    },\n  ): Promise<number[]> {\n    const tasksCount = criteria.courseTaskIds.length;\n\n    let query = getRepository(Student).createQueryBuilder('student').select(['student.id']);\n    if (tasksCount > 0) {\n      query = query\n        .leftJoin(\n          'student.taskResults',\n          'tr',\n          'tr.studentId = student.id AND tr.score >= :minScore AND tr.courseTaskId IN (:...requiredCourseTaskIds)',\n          {\n            requiredCourseTaskIds: criteria.courseTaskIds,\n            minScore: criteria.minScore ? criteria.minScore : 1,\n          },\n        )\n        .addSelect('array_remove(ARRAY_AGG (DISTINCT \"tr\".\"courseTaskId\"), NULL) AS \"tasks\"');\n\n      query = query\n        .leftJoin(\n          'student.taskInterviewResults',\n          'interviewResults',\n          'interviewResults.studentId = student.id AND interviewResults.score >= :minScore AND interviewResults.courseTaskId IN (:...requiredCourseTaskIds)',\n          {\n            requiredCourseTaskIds: criteria.courseTaskIds,\n            minScore: criteria.minScore ? criteria.minScore : 1,\n          },\n        )\n        .addSelect('array_remove(ARRAY_AGG (DISTINCT \"interviewResults\".\"courseTaskId\"), NULL) AS \"interviews\"');\n    }\n\n    query = query.where('student.courseId = :courseId', { courseId }).andWhere('student.isExpelled = false');\n\n    if (criteria.minTotalScore != null) {\n      query = query.andWhere('student.totalScore >= :minTotalScore', {\n        minTotalScore: typeof criteria.minTotalScore === 'number' ? criteria.minTotalScore : 1,\n      });\n    }\n\n    if (tasksCount > 0) {\n      query = query.andWhere('(tr.id IS NOT NULL OR interviewResults.id IS NOT NULL)');\n    }\n    query = query.groupBy('\"student\".\"id\"');\n\n    const rawCertificates = await query.getRawMany();\n    return rawCertificates\n      .map(({ student_id, tasks = [], interviews = [] }) => {\n        if (!tasksCount) {\n          return student_id;\n        }\n        if (tasks.length + interviews.length === tasksCount) {\n          return student_id;\n        }\n        return undefined;\n      })\n      .filter(Boolean);\n  }\n}\n\nfunction transformStudent(record: Student): StudentBasic {\n  return {\n    id: record.id,\n    name: userService.createName(record.user),\n    githubId: record.user.githubId,\n    cityName: record.user.cityName ?? 'Unknown',\n    countryName: record.user.countryName ?? 'Unknown',\n    isActive: !record.isExpelled && !record.isFailed,\n    discord: record.user.discord,\n    totalScore: record.totalScore,\n    mentor: record.mentor\n      ? {\n          id: record.mentor.id,\n          name: userService.createName(record.mentor.user),\n          githubId: record.mentor.user.githubId,\n          cityName: record.mentor.user.cityName ?? undefined,\n          countryName: record.mentor.user.countryName ?? undefined,\n          isActive: !record.mentor.isExpelled,\n        }\n      : null,\n  };\n}\n"
  },
  {
    "path": "server/src/repositories/user.repository.ts",
    "content": "import { AbstractRepository, EntityRepository, getRepository } from 'typeorm';\nimport { Student, User } from '../models';\n\n@EntityRepository(User)\nexport class UserRepository extends AbstractRepository<User> {\n  public async findByStudentIds(studentIds: number[]): Promise<{ studentId: number; githubId: string }[]> {\n    if (!studentIds || studentIds.length === 0) {\n      return [];\n    }\n    const data = await getRepository(Student)\n      .createQueryBuilder('s')\n      .innerJoin('s.user', 'u')\n      .addSelect(['s.id', 'u.githubId'])\n      .where('s.id IN (:...ids)', { ids: studentIds })\n      .getMany();\n\n    return data.map(s => ({\n      studentId: s.id,\n      githubId: s.user.githubId,\n    }));\n  }\n}\n"
  },
  {
    "path": "server/src/reset.d.ts",
    "content": "import '@total-typescript/ts-reset';\n"
  },
  {
    "path": "server/src/routes/checks/getBadComment.ts",
    "content": "import { OK } from 'http-status-codes';\nimport { RouterContext } from '../guards';\nimport { ILogger } from '../../logger';\nimport { setResponse } from '../utils';\nimport { getCheckersWithoutComments } from '../../services/check.service';\n\nexport const getBadComment = (_: ILogger) => async (ctx: RouterContext) => {\n  const { taskId } = ctx.params;\n\n  const badCheckers = await getCheckersWithoutComments(Number(taskId));\n\n  setResponse(ctx, OK, badCheckers);\n};\n"
  },
  {
    "path": "server/src/routes/checks/getMaxScoreCheckers.ts",
    "content": "import { OK } from 'http-status-codes';\nimport { RouterContext } from '../guards';\nimport { ILogger } from '../../logger';\nimport { setResponse } from '../utils';\nimport { getCheckersWithMaxScore } from '../../services/check.service';\n\nexport const getMaxScoreCheckers = (_: ILogger) => async (ctx: RouterContext) => {\n  const { taskId } = ctx.params;\n\n  const badCheckers = await getCheckersWithMaxScore(Number(taskId));\n\n  setResponse(ctx, OK, badCheckers);\n};\n"
  },
  {
    "path": "server/src/routes/checks/index.ts",
    "content": "import { getBadComment } from './getBadComment';\nimport { getMaxScoreCheckers } from './getMaxScoreCheckers';\nimport Router from '@koa/router';\nimport { ILogger } from '../../logger';\nimport { courseDementorGuard } from '../guards';\n\nexport function checksRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/checks' });\n\n  router.get('/badcomment/:courseId/:taskId', courseDementorGuard, getBadComment(logger));\n  router.get('/maxscore/:courseId/:taskId', courseDementorGuard, getMaxScoreCheckers(logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/common.ts",
    "content": "import { NOT_FOUND, OK, BAD_REQUEST } from 'http-status-codes';\nimport Router from '@koa/router';\nimport { setResponse } from './utils';\nimport { getManager, getRepository, ObjectType } from 'typeorm';\nimport { ILogger } from '../logger';\n\nexport const createGetRoute =\n  <T extends ObjectType<T>>(entity: T, _?: ILogger, relations?: string[]) =>\n  async (ctx: Router.RouterContext) => {\n    const id = ctx.params.id;\n    const user = await getManager().findOne(entity, {\n      where: { id: Number(id) } as any,\n      relations,\n    });\n    if (user === undefined) {\n      setResponse(ctx, NOT_FOUND);\n      return;\n    }\n    setResponse(ctx, OK, user);\n  };\n\nexport const createGetAllRoute =\n  <T extends ObjectType<T>>(entity: T, params: { take: number; skip: number }, _?: ILogger, relations?: string[]) =>\n  async (ctx: Router.RouterContext) => {\n    const data = await getManager().find(entity, { relations, take: params.take || 20, skip: params.skip || 0 });\n    if (data === undefined) {\n      setResponse(ctx, NOT_FOUND);\n      return;\n    }\n    setResponse(ctx, OK, data);\n  };\n\nexport const createPostRoute =\n  <T extends ObjectType<T>>(entity: T, logger?: ILogger) =>\n  async (ctx: Router.RouterContext) => {\n    const { id, createdDate, ...data } = ctx.request.body;\n    try {\n      const {\n        identifiers: [identifier],\n      } = await getRepository(entity).insert(data);\n\n      setResponse(ctx, OK, { id: identifier.id });\n    } catch (e) {\n      if (logger) {\n        logger.error((e as Error).message);\n      }\n      setResponse(ctx, BAD_REQUEST, { message: (e as Error).message });\n    }\n  };\n\nexport const createPutRoute =\n  <T extends ObjectType<T>>(entity: T, logger?: ILogger) =>\n  async (ctx: Router.RouterContext) => {\n    const { id: _id, createdDate, ...data } = ctx.request.body;\n    const id: number = Number(ctx.params.id);\n    try {\n      const result = await getRepository(entity).update(id, data);\n      setResponse(ctx, OK, result);\n    } catch (e) {\n      if (logger) {\n        logger.error((e as Error).message);\n      }\n      setResponse(ctx, BAD_REQUEST, { message: (e as Error).message });\n    }\n  };\n\nexport const createDeleteRoute =\n  <T extends ObjectType<T>>(entity: T, logger?: ILogger) =>\n  async (ctx: Router.RouterContext) => {\n    const id: number = ctx.params.id;\n    try {\n      const result = await getRepository(entity).delete(id);\n      setResponse(ctx, OK, result);\n    } catch (e) {\n      if (logger) {\n        logger.error((e as Error).message);\n      }\n      setResponse(ctx, BAD_REQUEST, { message: (e as Error).message });\n    }\n  };\n\nexport const createMultiplePostRoute =\n  <T extends ObjectType<T>>(entity: T, logger?: ILogger) =>\n  async (ctx: Router.RouterContext) => {\n    const data = ctx.request.body;\n\n    data.forEach(async (someEntity: any) => {\n      try {\n        const result = await getRepository(entity).insert(someEntity);\n        setResponse(ctx, OK, result);\n      } catch (e) {\n        if (logger) {\n          logger.error((e as Error).message);\n        }\n        setResponse(ctx, BAD_REQUEST, { message: (e as Error).message });\n      }\n    });\n  };\n\nexport const createMultiplePutRoute =\n  <T extends ObjectType<T>>(entity: T, logger?: ILogger) =>\n  async (ctx: Router.RouterContext) => {\n    const data = ctx.request.body;\n\n    data.forEach(async (someEntity: any) => {\n      try {\n        const result = await getRepository(entity).update(someEntity.id, someEntity);\n        setResponse(ctx, OK, result);\n      } catch (e) {\n        if (logger) {\n          logger.error((e as Error).message);\n        }\n        setResponse(ctx, BAD_REQUEST, { message: (e as Error).message });\n      }\n    });\n  };\n"
  },
  {
    "path": "server/src/routes/course/certificates.ts",
    "content": "import Router from '@koa/router';\nimport { getCustomRepository, getRepository } from 'typeorm';\nimport axios from 'axios';\nimport { OK, BAD_REQUEST } from 'http-status-codes';\nimport { ILogger } from '../../logger';\nimport { Student } from '../../models';\nimport { setResponse } from '../utils';\nimport { config } from '../../config';\nimport { StudentRepository } from '../../repositories/student.repository';\n\nexport const postCertificates = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const data: {\n    criteria: { courseTaskIds?: number[]; minScore?: number; minTotalScore: number };\n  } = ctx.request.body;\n\n  if (data == null) {\n    setResponse(ctx, BAD_REQUEST);\n    return;\n  }\n\n  const { courseTaskIds, minScore, minTotalScore } = data.criteria ?? {};\n  const emptyCriteria = !minScore && !minTotalScore && (!courseTaskIds || !courseTaskIds?.length);\n  const studentRepository = getCustomRepository(StudentRepository);\n  const studentIds = await studentRepository.findByCriteria(courseId, {\n    courseTaskIds: courseTaskIds ?? [],\n    minScore: minScore != null ? Number(minScore) : null,\n    minTotalScore: minTotalScore != null ? Number(minTotalScore) : null,\n  });\n\n  if (studentIds.length === 0 && !emptyCriteria) {\n    setResponse(ctx, OK, []);\n    return;\n  }\n\n  let students: Student[];\n  const initialQuery = getRepository(Student)\n    .createQueryBuilder('student')\n    .innerJoin('student.course', 'course')\n    .innerJoin('course.discipline', 'discipline')\n    .innerJoin('student.user', 'user')\n    .addSelect([\n      'user.id',\n      'user.firstName',\n      'user.lastName',\n      'user.githubId',\n      'course.name',\n      'course.disciplineId',\n      'course.primarySkillName',\n      'course.certificateIssuer',\n      'discipline.name',\n      'discipline.id',\n    ]);\n  if (studentIds.length > 0) {\n    students = await initialQuery.where('student.\"id\" IN (:...ids)', { ids: studentIds }).getMany();\n  } else {\n    students = await initialQuery\n      .leftJoinAndSelect('student.certificate', 'certificate')\n      .where(\n        [\n          'certificate.id IS NULL',\n          'student.\"courseId\" = :courseId',\n          'student.\"isExpelled\" = false',\n          'student.\"isFailed\" = false',\n        ].join(' AND '),\n        {\n          courseId,\n        },\n      )\n      .getMany();\n  }\n\n  const result = students.map(student => {\n    const course = student.course!;\n    const user = student.user!;\n    return {\n      courseId,\n      courseName: course.name,\n      coursePrimarySkill: course.discipline?.name ?? course.primarySkillName,\n      certificateIssuer: course.certificateIssuer,\n      studentId: student.id,\n      studentName: `${user.firstName} ${user.lastName}`,\n      timestamp: Date.now(),\n    };\n  });\n  await axios.post(`${config.aws.restApiUrl}/certificate`, result, {\n    headers: { 'x-api-key': config.aws.restApiKey },\n  });\n  setResponse(ctx, OK, result);\n};\n\nexport const postStudentCertificate = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params;\n  const student = await getRepository(Student).findOne({\n    where: {\n      courseId: Number(courseId),\n      user: { githubId },\n    },\n    relations: ['user', 'course', 'course.discipline'],\n  });\n\n  if (student == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'No student' });\n    return;\n  }\n  const result = {\n    courseId,\n    courseName: student.course.name,\n    coursePrimarySkill: student.course.discipline?.name ?? student.course.primarySkillName,\n    certificateIssuer: student.course.certificateIssuer,\n    studentId: student.id,\n    studentName: `${student.user.firstName} ${student.user.lastName}`,\n    timestamp: Date.now(),\n  };\n  await axios.post(`${config.aws.restApiUrl}/certificate`, result, {\n    headers: { 'x-api-key': config.aws.restApiKey },\n  });\n  setResponse(ctx, OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/createCompletion.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { CrossCheckStatus } from '../../../models/courseTask';\nimport { courseService, taskService } from '../../../services';\nimport { ScoreService } from '../../../services/score';\nimport { setResponse } from '../../utils';\n\nconst DEFAULT_PAIRS_COUNT = 4;\n\nexport const createCompletion = (__: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseTaskId, courseId } = ctx.params;\n\n  const courseTask = await taskService.getCourseTask(courseTaskId);\n\n  if (courseTask == null) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST);\n    return;\n  }\n\n  if (!taskService.isSubmissionDeadlinePassed(courseTask) || courseTask.crossCheckStatus === CrossCheckStatus.Initial) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST);\n    return;\n  }\n\n  const scoreService = new ScoreService(Number(courseId));\n\n  const pairsCount = Math.max((courseTask.pairsCount ?? DEFAULT_PAIRS_COUNT) - 1, 1);\n  const studentScores = await courseService.getTaskSolutionCheckers(courseTaskId, pairsCount);\n\n  for (const studentScore of studentScores) {\n    const data = { authorId: -1, comment: 'Cross-Check score', score: studentScore.score };\n    await scoreService.saveScore(studentScore.studentId, courseTaskId, data);\n  }\n\n  await taskService.changeCourseTaskStatus(courseTask, CrossCheckStatus.Completed);\n\n  setResponse(ctx, StatusCodes.OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/createDistribution.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { TaskSolutionChecker } from '../../../models';\nimport { CrossCheckStatus } from '../../../models/courseTask';\nimport { CrossCheckDistributionService } from '../../../services/distribution';\nimport { courseService, taskService } from '../../../services';\nimport { setResponse } from '../../utils';\n\nconst crossCheckDistributionService = new CrossCheckDistributionService();\n\nexport const createDistribution = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseTaskId } = ctx.params;\n\n  const courseTask = await taskService.getCourseTask(courseTaskId);\n\n  if (courseTask == null) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST);\n    return;\n  }\n\n  if (!taskService.isSubmissionDeadlinePassed(courseTask)) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST);\n    return;\n  }\n\n  const solutions = await courseService.getTaskSolutionsWithoutChecker(courseTaskId);\n  const solutionsMap = new Map<number, number>();\n\n  await taskService.changeCourseTaskStatus(courseTask, CrossCheckStatus.Distributed);\n\n  for (const solution of solutions) {\n    solutionsMap.set(solution.studentId, solution.id);\n  }\n\n  const students = Array.from(solutionsMap.keys());\n\n  if (students.length === 0) {\n    setResponse(ctx, StatusCodes.OK, { crossCheckPairs: [] });\n    return;\n  }\n\n  const pairs = crossCheckDistributionService.distribute(students, courseTask.pairsCount ?? undefined);\n  const crossCheckPairs = pairs\n    .filter(pair => solutionsMap.has(pair.studentId))\n    .map(pair => ({\n      ...pair,\n      courseTaskId,\n      taskSolutionId: solutionsMap.get(pair.studentId),\n    }));\n\n  await getRepository(TaskSolutionChecker).save(crossCheckPairs);\n\n  setResponse(ctx, StatusCodes.OK, { crossCheckPairs });\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/createMessage.ts",
    "content": "import Router from '@koa/router';\nimport { getRepository } from 'typeorm';\nimport { StatusCodes } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { IUserSession, Student } from '../../../models';\nimport { CrossCheckMessageAuthorRole } from '../../../models/taskSolutionResult';\nimport { courseService, CrossCheckService, notificationService } from '../../../services';\nimport { getTaskSolutionResultById } from '../../../services/taskResults.service';\nimport { setErrorResponse, setResponse } from '../../utils';\nimport { getCourseTask } from '../../../services/tasks.service';\n\nexport const createMessage = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, taskSolutionResultId, courseTaskId } = ctx.params;\n  const { user } = ctx.state as { user: IUserSession };\n\n  const crossCheckService = new CrossCheckService(courseTaskId);\n  const [student, taskSolutionResult, courseTask] = await Promise.all([\n    courseService.queryStudentByGithubId(courseId, user.githubId),\n    getTaskSolutionResultById(taskSolutionResultId),\n    getCourseTask(courseTaskId, true),\n  ]);\n\n  if (!student) {\n    setErrorResponse(ctx, StatusCodes.BAD_REQUEST, 'not valid student or course');\n    return;\n  }\n  if (!courseTask) {\n    setErrorResponse(ctx, StatusCodes.BAD_REQUEST, 'not valid task');\n    return;\n  }\n\n  if (!taskSolutionResult) {\n    setErrorResponse(ctx, StatusCodes.BAD_REQUEST, 'task solution result is not exist');\n    return;\n  }\n\n  const inputData: {\n    content: string;\n    role: CrossCheckMessageAuthorRole;\n  } = ctx.request.body;\n\n  switch (inputData.role) {\n    case CrossCheckMessageAuthorRole.Reviewer:\n      if (student.id !== taskSolutionResult.checkerId) {\n        setErrorResponse(ctx, StatusCodes.BAD_REQUEST, 'user is not checker');\n        return;\n      }\n      break;\n\n    case CrossCheckMessageAuthorRole.Student:\n      if (student.id !== taskSolutionResult.studentId) {\n        setErrorResponse(ctx, StatusCodes.BAD_REQUEST, 'user is not student');\n        return;\n      }\n      break;\n\n    default:\n      setErrorResponse(ctx, StatusCodes.BAD_REQUEST, 'incorrect message role');\n      return;\n  }\n\n  const data = {\n    content: inputData.content ?? '',\n    role: inputData.role,\n  };\n\n  await crossCheckService.saveMessage(taskSolutionResultId, data, {\n    user: user,\n  });\n\n  const userId = await getUserId(student.id, taskSolutionResult.checkerId, inputData.role);\n  if (!userId) {\n    setErrorResponse(ctx, StatusCodes.BAD_REQUEST, 'user not found');\n    return;\n  }\n\n  await notificationService\n    .sendNotification({\n      userId,\n      notificationId: 'messages',\n      data: {\n        isReviewerMessage: inputData.role === CrossCheckMessageAuthorRole.Reviewer,\n        courseAlias: courseTask.course.alias,\n        courseTaskId,\n        taskName: courseTask.task.name,\n        studentGithubId: student.githubId,\n      },\n    })\n    .catch(() => null);\n\n  setResponse(ctx, StatusCodes.OK);\n};\n\nasync function getUserId(studentId: number, checkerId: number, role: CrossCheckMessageAuthorRole) {\n  if (role === CrossCheckMessageAuthorRole.Reviewer) {\n    return studentId;\n  }\n\n  const checker = await getRepository(Student).findOne({ where: { id: checkerId } });\n\n  return checker?.userId;\n}\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/createResult.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { config } from '../../../config';\nimport { ILogger } from '../../../logger';\nimport { IUserSession } from '../../../models';\nimport { TaskSolutionComment, TaskSolutionReview } from '../../../models/taskSolution';\nimport { CrossCheckStatus } from '../../../models/courseTask';\nimport { CrossCheckCriteriaData } from '../../../models/taskSolutionResult';\n\nimport {\n  courseService,\n  CrossCheckService,\n  notificationService,\n  taskResultsService,\n  taskService,\n} from '../../../services';\nimport { setErrorResponse, setResponse } from '../../utils';\n\nexport const createResult = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId, courseTaskId } = ctx.params;\n  const { user } = ctx.state as { user: IUserSession };\n  const crossCheckService = new CrossCheckService(courseTaskId);\n  const [student, checker, courseTask] = await Promise.all([\n    courseService.queryStudentByGithubId(courseId, githubId),\n    courseService.queryStudentByGithubId(courseId, user.githubId),\n    taskService.getCourseTask(courseTaskId, true),\n  ]);\n\n  if (courseTask?.crossCheckStatus !== CrossCheckStatus.Distributed) {\n    setErrorResponse(ctx, BAD_REQUEST, \"task review can't be submitted\");\n    return;\n  }\n\n  if (student == null || courseTask == null || checker == null) {\n    setErrorResponse(ctx, BAD_REQUEST, 'not valid student or course task');\n    return;\n  }\n  if (!CrossCheckService.isCrossCheckTask(courseTask)) {\n    setErrorResponse(ctx, BAD_REQUEST, 'task solution is supported for this task');\n    return;\n  }\n\n  const taskChecker = await taskResultsService.getTaskSolutionChecker(student.id, checker.id, courseTaskId);\n  if (taskChecker == null) {\n    setErrorResponse(ctx, BAD_REQUEST, 'no assigned cross-check');\n    return;\n  }\n\n  const inputData: {\n    score: number;\n    comment: string;\n    anonymous: boolean;\n    review: TaskSolutionReview[];\n    comments: TaskSolutionComment[];\n    criteria: CrossCheckCriteriaData[];\n  } = ctx.request.body;\n\n  const data = {\n    score: Math.round(Number(inputData.score)),\n    comment: inputData.comment || '',\n    anonymous: inputData.anonymous !== false,\n    review: Array.isArray(inputData.review) ? inputData.review : [],\n  };\n\n  if (data.score > courseTask.maxScore) {\n    setErrorResponse(ctx, BAD_REQUEST, 'score provided is greater than max score for the task');\n    return;\n  }\n\n  if (isNaN(data.score) || data.score < 0) {\n    setErrorResponse(ctx, BAD_REQUEST, 'no score provided');\n    return;\n  }\n\n  const previousScore = await crossCheckService.saveResult(taskChecker.studentId, taskChecker.checkerId, data, {\n    userId: user.id,\n    criteria: inputData.criteria,\n  });\n\n  await crossCheckService.saveSolutionComments(taskChecker.studentId, courseTaskId, {\n    comments: inputData.comments ?? [],\n    authorId: taskChecker.checkerId,\n    authorGithubId: user.githubId,\n    recipientId: taskChecker.studentId,\n  });\n\n  await notificationService.sendNotification({\n    userId: student.userId,\n    notificationId: 'taskGrade',\n    data: {\n      previousScore,\n      courseTask,\n      score: data.score,\n      comment: data.comment,\n      resultLink: `${config.host}/course/student/cross-check-submit?course=${courseTask.course.alias}&taskId=${courseTaskId}`,\n    },\n  });\n\n  setResponse(ctx, OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/createSolution.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { TaskSolution } from '../../../models';\nimport { CrossCheckService } from '../../../services';\nimport { setResponse } from '../../utils';\n\nexport const createSolution = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId, courseTaskId } = ctx.params;\n\n  const crossCheckService = new CrossCheckService(courseTaskId);\n  const { student, courseTask } = await crossCheckService.getStudentAndTask(courseId, githubId);\n\n  if (student == null || courseTask == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid student or course task' });\n    return;\n  }\n  if (!CrossCheckService.isCrossCheckTask(courseTask)) {\n    setResponse(ctx, BAD_REQUEST, { message: 'task solution is not supported for this task' });\n    return;\n  }\n\n  const { review, url, comments }: Partial<TaskSolution> = ctx.request.body ?? {};\n  const taskSolution = {\n    review,\n    url,\n    comments: comments?.map(c => ({ ...c, authorId: student.id })),\n  };\n  if (!CrossCheckService.isValidTaskSolution(taskSolution)) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid request payload' });\n    return;\n  }\n\n  await crossCheckService.saveSolution(student.id, taskSolution);\n\n  setResponse(ctx, OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/deleteSolution.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { CrossCheckService } from '../../../services';\nimport { setResponse } from '../../utils';\n\nexport const deleteSolution = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId, courseTaskId } = ctx.params;\n\n  const crossCheckService = new CrossCheckService(courseTaskId);\n  const { student, courseTask } = await crossCheckService.getStudentAndTask(courseId, githubId);\n\n  if (student == null || courseTask == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid student or course task' });\n    return;\n  }\n  if (!CrossCheckService.isCrossCheckTask(courseTask)) {\n    setResponse(ctx, BAD_REQUEST, { message: 'task solution is not supported for this task' });\n    return;\n  }\n\n  await crossCheckService.deleteSolution(student.id);\n\n  setResponse(ctx, OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/getAssignments.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { courseService, taskResultsService, taskService } from '../../../services';\nimport { setResponse } from '../../utils';\n\nexport const getAssignments = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId, courseTaskId } = ctx.params;\n  const [student, courseTask] = await Promise.all([\n    courseService.queryStudentByGithubId(courseId, githubId),\n    taskService.getCourseTask(courseTaskId),\n  ]);\n\n  if (student == null || courseTask == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid student or course task' });\n    return;\n  }\n  if (courseTask.checker !== 'crossCheck') {\n    setResponse(ctx, BAD_REQUEST, { message: 'not supported task' });\n    return;\n  }\n  const records = await taskResultsService.getTaskSolutionAssignments(student.id, courseTaskId);\n  const result = records.map(r => ({\n    student: courseService.convertToStudentBasic(r.student),\n    url: r.taskSolution.url,\n  }));\n  setResponse(ctx, OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/getResult.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { IUserSession } from '../../../models';\nimport { courseService, CrossCheckService, taskResultsService, taskService } from '../../../services';\nimport { setErrorResponse, setResponse } from '../../utils';\n\nexport const getResult = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId, courseTaskId } = ctx.params;\n  const { user } = ctx.state as { user: IUserSession };\n\n  const crossCheckService = new CrossCheckService(courseTaskId);\n\n  const [student, checker, courseTask] = await Promise.all([\n    courseService.queryStudentByGithubId(courseId, githubId),\n    courseService.queryStudentByGithubId(courseId, user.githubId),\n    taskService.getCourseTask(courseTaskId),\n  ]);\n\n  if (student == null || courseTask == null || checker == null) {\n    setErrorResponse(ctx, BAD_REQUEST, 'not valid student or course task');\n    return;\n  }\n  if (courseTask.checker !== 'crossCheck') {\n    setErrorResponse(ctx, BAD_REQUEST, 'task solution is supported for this task');\n    return;\n  }\n  const taskChecker = await taskResultsService.getTaskSolutionChecker(student.id, checker.id, courseTaskId);\n  if (taskChecker == null) {\n    setErrorResponse(ctx, BAD_REQUEST, 'no assigned cross-check');\n    return;\n  }\n  const existingResult = await crossCheckService.getResult(student.id, checker.id, checker.githubId);\n  setResponse(ctx, OK, existingResult);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/getSolution.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, NOT_FOUND, OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { courseService, taskResultsService, taskService } from '../../../services';\nimport { setResponse } from '../../utils';\n\nexport const getSolution = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId, courseTaskId } = ctx.params;\n\n  const [student, courseTask] = await Promise.all([\n    courseService.queryStudentByGithubId(courseId, githubId),\n    taskService.getCourseTask(courseTaskId),\n  ]);\n\n  if (student == null || courseTask == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid student or course task' });\n    return;\n  }\n\n  const result = await taskResultsService.getTaskSolution(student.id, courseTask.id);\n\n  if (result == null) {\n    setResponse(ctx, NOT_FOUND, { message: 'solution is not found ' });\n    return;\n  }\n\n  const { updatedDate, id, url, review, comments } = result;\n\n  setResponse(ctx, OK, {\n    updatedDate,\n    id,\n    url,\n    review,\n    studentId: student.id,\n    comments: comments.filter(c => c.authorId == student.id && c.recipientId == null),\n  });\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/getTaskDetails.ts",
    "content": "import Router from '@koa/router';\nimport { OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { CrossCheckService } from '../../../services';\nimport { setResponse } from '../../utils';\n\nexport const getTaskDetails = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseTaskId } = ctx.params;\n  const crossCheckService = new CrossCheckService(courseTaskId);\n  const { criteria, studentEndDate } = await crossCheckService.getTaskDetails();\n\n  const response = {\n    criteria,\n    studentEndDate,\n  };\n  setResponse(ctx, OK, response);\n};\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/index.ts",
    "content": "export * from './createCompletion';\nexport * from './createDistribution';\nexport * from './createResult';\nexport * from './createSolution';\nexport * from './createMessage';\nexport * from './updateMessage';\nexport * from './deleteSolution';\nexport * from './getAssignments';\nexport * from './getResult';\nexport * from './getSolution';\nexport * from './getTaskDetails';\n"
  },
  {
    "path": "server/src/routes/course/crossCheck/updateMessage.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { IUserSession } from '../../../models';\nimport { CrossCheckMessageAuthorRole } from '../../../models/taskSolutionResult';\nimport { courseService, CrossCheckService } from '../../../services';\nimport { getTaskSolutionResultById } from '../../../services/taskResults.service';\nimport { setErrorResponse, setResponse } from '../../utils';\n\nexport const updateMessage = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, taskSolutionResultId, courseTaskId } = ctx.params;\n  const { user } = ctx.state as { user: IUserSession };\n\n  const crossCheckService = new CrossCheckService(courseTaskId);\n  const student = await courseService.queryStudentByGithubId(courseId, user.githubId);\n\n  if (!student) {\n    setErrorResponse(ctx, BAD_REQUEST, 'not valid student or course');\n    return;\n  }\n\n  const taskSolutionResult = await getTaskSolutionResultById(taskSolutionResultId);\n\n  if (!taskSolutionResult) {\n    setErrorResponse(ctx, BAD_REQUEST, 'task solution result is not exist');\n    return;\n  }\n\n  const inputData: {\n    role: CrossCheckMessageAuthorRole;\n  } = ctx.request.body;\n\n  switch (inputData.role) {\n    case CrossCheckMessageAuthorRole.Reviewer:\n      if (student.id !== taskSolutionResult.checkerId) {\n        setErrorResponse(ctx, BAD_REQUEST, 'user is not checker');\n        return;\n      }\n      break;\n\n    case CrossCheckMessageAuthorRole.Student:\n      if (student.id !== taskSolutionResult.studentId) {\n        setErrorResponse(ctx, BAD_REQUEST, 'user is not student');\n        return;\n      }\n      break;\n\n    default:\n      setErrorResponse(ctx, BAD_REQUEST, 'incorrect message role');\n      return;\n  }\n\n  const data = {\n    role: inputData.role,\n  };\n\n  await crossCheckService.updateMessage(taskSolutionResultId, data);\n\n  setResponse(ctx, OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/events.ts",
    "content": "// tslint:disable-next-line\nimport Router from '@koa/router';\nimport { OK } from 'http-status-codes';\nimport { ILogger } from '../../logger';\nimport { courseService } from '../../services';\nimport { setResponse } from '../utils';\n\nexport const getCourseEvent = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const eventId: number = ctx.params.id;\n  const data = await courseService.getEvent(eventId);\n\n  setResponse(ctx, OK, data);\n};\n\nexport const getCourseEvents = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const data = await courseService.getEvents(courseId);\n\n  setResponse(ctx, OK, data);\n};\n"
  },
  {
    "path": "server/src/routes/course/index.ts",
    "content": "import Router from '@koa/router';\nimport { ILogger } from '../../logger';\nimport {\n  adminGuard,\n  anyCourseMentorGuard,\n  basicAuthAws,\n  courseGuard,\n  courseInterviewGuard,\n  courseManagerGuard,\n  courseMentorGuard,\n  courseMentorOrDementorGuard,\n  courseSupervisorGuard,\n  courseSupervisorOrDementorGuard,\n  courseSupervisorOrMentorGuard,\n  crossCheckGuard,\n  guard,\n  taskOwnerGuard,\n} from '../guards';\nimport { postCertificates, postStudentCertificate } from './certificates';\nimport { getCourseEvent, getCourseEvents } from './events';\nimport {\n  deleteMentor as postMentorStatusExpelled,\n  getMentorInterview,\n  getMentorStudents,\n  postMentor,\n  restoreExpelledMentor,\n} from './mentor';\nimport * as score from './score';\nimport * as stageInterview from './stageInterview';\nimport {\n  getStudents,\n  getStudentsCsv,\n  getStudentsWithDetails,\n  postStudents,\n  searchStudent,\n  updateStatuses,\n} from './students';\nimport { postTaskArtefact } from './taskArtefact';\nimport { getCourseTasksVerifications, getStudentTaskVerifications } from './taskVerifications';\n\nimport * as interviews from './interviews';\n\nimport {\n  validateCrossCheckExpirationDate,\n  validateExpelledStudent,\n  validateGithubId,\n  validateGithubIdAndAccess,\n  validateGithubIdAndAccessForUserOrPowerUser,\n} from '../validators';\nimport * as crossCheck from './crossCheck';\nimport {\n  createRepositories,\n  createRepository,\n  inviteAllMentorsToTeam,\n  inviteMentorToTeam,\n  updateRepositories,\n} from './repository';\nimport { getScheduleAsCsv, setScheduleFromCsv } from './schedule';\nimport {\n  createInterviewResult,\n  getCrossMentors,\n  getStudent,\n  postFeedback,\n  selfUpdateStudentStatus,\n  updateMentoringAvailability,\n  updateStudent,\n  updateStudentStatus,\n} from './student';\nimport * as tasks from './tasks';\n\nexport function courseRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/course/:courseId' });\n\n  router.post('/certificates', courseManagerGuard, postCertificates(logger));\n  router.post('/repositories', courseManagerGuard, createRepositories(logger));\n  router.put('/repositories', courseManagerGuard, updateRepositories(logger));\n\n  addScoreApi(router, logger);\n  addStageInterviewApi(router, logger);\n  addInterviewsApi(router, logger);\n  addEventApi(router, logger);\n  addTaskApi(router, logger);\n  addMentorApi(router, logger);\n  addStudentApi(router, logger);\n  addStudentCrossCheckApi(router, logger);\n  addScheduleApi(router, logger);\n  return router;\n}\n\nfunction addScoreApi(router: Router<any, any>, logger: ILogger) {\n  router.post('/scores/calculation', adminGuard, score.recalculateScore(logger));\n  router.post('/scores/:courseTaskId', taskOwnerGuard, score.createMultipleScores(logger));\n}\n\nfunction addInterviewsApi(router: Router<any, any>, logger: ILogger) {\n  router.post(\n    '/interview/:courseTaskId/interviewer/:githubId/student/:studentGithubId/',\n    courseInterviewGuard,\n    interviews.createInterview(logger),\n  );\n  router.post('/interviews/:courseTaskId', courseManagerGuard, interviews.createInterviews(logger));\n  router.delete('/interviews/:courseTaskId/:id', courseManagerGuard, interviews.cancelInterview(logger));\n}\n\nfunction addEventApi(router: Router<any, any>, logger: ILogger) {\n  router.get('/event/:id', courseGuard, getCourseEvent(logger));\n\n  router.get('/events', courseGuard, getCourseEvents(logger));\n}\n\nfunction addTaskApi(router: Router<any, any>, logger: ILogger) {\n  router.get('/tasks/details', courseGuard, tasks.getCourseTasksDetails(logger));\n\n  router.get('/tasks/verifications', basicAuthAws, getCourseTasksVerifications(logger));\n  router.post('/task/:courseTaskId/distribution', courseManagerGuard, tasks.createCourseTaskDistribution(logger));\n  router.post('/task/:courseTaskId/artefact', courseGuard, postTaskArtefact(logger));\n  router.post('/task/:courseTaskId/cross-check/distribution', crossCheckGuard, crossCheck.createDistribution(logger));\n  router.get(`/task/:courseTaskId/cross-check/details`, courseGuard, crossCheck.getTaskDetails(logger));\n  router.post('/task/:courseTaskId/cross-check/completion', crossCheckGuard, crossCheck.createCompletion(logger));\n}\n\nfunction addStageInterviewApi(router: Router<any, any>, logger: ILogger) {\n  router.post(\n    '/interview/stage/interviewer/:githubId/student/:studentGithubId/',\n    courseMentorGuard,\n    stageInterview.createInterview(logger),\n  );\n  router.get(\n    '/interview/stage/interviewer/:githubId/students',\n    courseMentorGuard,\n    stageInterview.getInterviewerStudents(logger),\n  );\n\n  /**\n   * @deprecated. should be removed after feedbacks are migrated to new template\n   */\n  router.get('/interview/stage/:interviewId/feedback', courseMentorGuard, stageInterview.getFeedback(logger));\n  router.post('/interview/stage/:interviewId/feedback', courseMentorGuard, stageInterview.createFeedback(logger));\n\n  router.put('/interview/stage/:interviewId', courseMentorGuard, stageInterview.updateInterview(logger));\n  router.delete('/interview/stage/:interviewId', courseMentorGuard, stageInterview.cancelInterview(logger));\n\n  router.post('/interviews/stage', courseManagerGuard, stageInterview.createInterviews(logger));\n  router.get('/interviews/stage', courseMentorGuard, stageInterview.getInterviews(logger));\n}\n\nfunction addMentorApi(router: Router<any, any>, logger: ILogger) {\n  const validators = [validateGithubIdAndAccess];\n\n  const mentorLogger = logger.child({ module: 'course/mentor' });\n  router.post('/mentor/:githubId', guard, ...validators, postMentor(mentorLogger));\n  router.post('/repositories/mentor/:githubId', courseManagerGuard, ...validators, inviteMentorToTeam(mentorLogger));\n  router.post('/repositories/mentors', courseManagerGuard, inviteAllMentorsToTeam(mentorLogger));\n  router.get('/mentor/:githubId/students', guard, ...validators, getMentorStudents(mentorLogger));\n  router.get('/mentor/:githubId/interview/:courseTaskId', guard, ...validators, getMentorInterview(mentorLogger));\n  router.get('/mentor/:githubId/interviews', guard, ...validators, interviews.getMentorInterviews(mentorLogger));\n  router.post(\n    '/mentor/:githubId/status/expelled',\n    courseManagerGuard,\n    validateGithubId,\n    postMentorStatusExpelled(mentorLogger),\n  );\n  router.post(\n    '/mentor/:githubId/status/restore',\n    courseManagerGuard,\n    validateGithubId,\n    restoreExpelledMentor(mentorLogger),\n  );\n}\n\nfunction addStudentApi(router: Router<any, any>, logger: ILogger) {\n  const validators = [validateGithubIdAndAccess];\n  const mentorValidators = [courseMentorGuard, validateGithubId];\n  const mentorOrDementorValidators = [courseMentorOrDementorGuard, validateGithubId];\n\n  router.get('/student/:githubId', courseSupervisorGuard, getStudent(logger));\n  router.put('/student/:githubId', courseSupervisorOrMentorGuard, updateStudent(logger));\n\n  router.get(\n    '/student/:githubId/interview/stage',\n    courseGuard,\n    ...validators,\n    stageInterview.getInterviewStudent(logger),\n  );\n\n  router.get(\n    '/student/:githubId/interview/:courseTaskId',\n    courseGuard,\n    ...validators,\n    interviews.getInterviewStudent(logger),\n  );\n\n  router.post('/student/:githubId/availability', courseManagerGuard, updateMentoringAvailability(logger));\n  router.get('/student/:githubId/tasks/cross-mentors', courseGuard, ...validators, getCrossMentors(logger));\n  router.get('/student/:githubId/tasks/verifications', courseGuard, ...validators, getStudentTaskVerifications(logger));\n  router.get('/student/:githubId/interviews', courseGuard, ...validators, interviews.getStudentInterviews(logger));\n  router.post('/student/:githubId/task/:courseTaskId/result', courseGuard, score.createSingleScore(logger));\n  router.post('/student/:githubId/interview/:courseTaskId/result', ...mentorValidators, createInterviewResult(logger));\n\n  router.post(\n    '/student/:githubId/repository',\n    guard,\n    validateGithubIdAndAccessForUserOrPowerUser,\n    createRepository(logger),\n  );\n  router.post('/student/:githubId/status', ...mentorOrDementorValidators, updateStudentStatus(logger));\n  router.post('/student/:githubId/status-self', courseGuard, selfUpdateStudentStatus(logger));\n  router.get('/student/:githubId/score', courseGuard, score.getScoreByStudent(logger));\n  router.post('/student/:githubId/certificate', courseManagerGuard, validateGithubId, postStudentCertificate(logger));\n  router.post('/student/feedback', anyCourseMentorGuard, postFeedback(logger));\n\n  router.get('/students', courseSupervisorGuard, getStudents(logger));\n  router.get('/students/csv', courseSupervisorGuard, getStudentsCsv(logger));\n  router.post('/students/status', courseManagerGuard, updateStatuses(logger));\n  router.post('/students', adminGuard, postStudents(logger));\n  router.get('/students/details', courseSupervisorOrDementorGuard, getStudentsWithDetails(logger));\n  router.get('/students/score/csv', courseSupervisorGuard, score.getScoreCsv(logger));\n\n  router.get('/students/search/:searchText', guard, searchStudent(logger));\n}\n\nfunction addStudentCrossCheckApi(router: Router<any, any>, logger: ILogger) {\n  const validators = [validateGithubIdAndAccess];\n  const activeStudentValidators = [validateGithubIdAndAccess, validateExpelledStudent];\n  const baseUrl = `/student/:githubId/task/:courseTaskId`;\n\n  router.post(\n    `${baseUrl}/cross-check/solution`,\n    courseGuard,\n    ...activeStudentValidators,\n    validateCrossCheckExpirationDate,\n    crossCheck.createSolution(logger),\n  );\n  router.delete(\n    `${baseUrl}/cross-check/solution`,\n    courseGuard,\n    ...validators,\n    validateCrossCheckExpirationDate,\n    crossCheck.deleteSolution(logger),\n  );\n  router.get(`${baseUrl}/cross-check/solution`, courseGuard, validateGithubId, crossCheck.getSolution(logger));\n  router.post(`${baseUrl}/cross-check/result`, courseGuard, validateGithubId, crossCheck.createResult(logger));\n  router.get(`${baseUrl}/cross-check/result`, courseGuard, validateGithubId, crossCheck.getResult(logger));\n  router.get(`${baseUrl}/cross-check/assignments`, courseGuard, ...validators, crossCheck.getAssignments(logger));\n  router.post(\n    `/taskSolutionResult/:taskSolutionResultId/task/:courseTaskId/cross-check/messages`,\n    courseGuard,\n    crossCheck.createMessage(logger),\n  );\n  router.put(\n    `/taskSolutionResult/:taskSolutionResultId/task/:courseTaskId/cross-check/messages`,\n    courseGuard,\n    crossCheck.updateMessage(logger),\n  );\n}\n\nfunction addScheduleApi(router: Router<any, any>, logger: ILogger) {\n  router.get('/schedule/csv/:timeZone', courseSupervisorGuard, getScheduleAsCsv(logger));\n  router.post('/schedule/csv/:timeZone', courseSupervisorGuard, setScheduleFromCsv(logger));\n}\n"
  },
  {
    "path": "server/src/routes/course/interviews.ts",
    "content": "import { StatusCodes } from 'http-status-codes';\nimport Router from '@koa/router';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../logger';\nimport { courseService, InterviewService, notificationService, taskService } from '../../services';\nimport { setResponse } from '../utils';\nimport { InterviewRepository } from '../../repositories/interview.repository';\nimport { StageInterviewRepository } from '../../repositories/stageInterview.repository';\nimport { userGuards } from '../guards';\n\ntype Params = { courseId: number; githubId: string; courseTaskId: number };\n\nexport const getStudentInterviews = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params;\n  const interviewRepository = getCustomRepository(InterviewRepository);\n  const stageInterviewRepository = getCustomRepository(StageInterviewRepository);\n  const [interviews, stageInterviews] = await Promise.all([\n    interviewRepository.findByStudent(courseId, githubId),\n    stageInterviewRepository.findByStudent(courseId, githubId),\n  ]);\n  const result = stageInterviews.concat(interviews);\n\n  setResponse(ctx, StatusCodes.OK, result);\n};\n\nexport const getMentorInterviews = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params as Params;\n  const interviewRepository = getCustomRepository(InterviewRepository);\n  const stageInterviewRepository = getCustomRepository(StageInterviewRepository);\n  const [interviews, stageInterviews] = await Promise.all([\n    interviewRepository.findByInterviewer(courseId, githubId),\n    stageInterviewRepository.findByInterviewer(courseId, githubId),\n  ]);\n  const result = stageInterviews.concat(interviews);\n  setResponse(ctx, StatusCodes.OK, result);\n};\n\nexport const getInterviewStudent = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId, courseTaskId } = ctx.params;\n  try {\n    const student = await courseService.queryStudentByGithubId(courseId, githubId);\n    if (student == null) {\n      setResponse(ctx, StatusCodes.BAD_REQUEST, null);\n      return;\n    }\n    const repository = getCustomRepository(InterviewRepository);\n    const result = await repository.findRegisteredStudent(courseId, Number(courseTaskId), student.id);\n    setResponse(ctx, StatusCodes.OK, result);\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n\nexport const createInterview = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const user = ctx.state.user;\n  const guard = userGuards(user);\n  const { courseId, courseTaskId, studentGithubId, githubId: interviewerGithubId } = ctx.params;\n  const interviewService = new InterviewService(courseId, logger);\n\n  if (guard.isMentor(courseId) && !guard.isPowerUser(courseId)) {\n    const isStarted = await interviewService.isInterviewStarted(courseTaskId);\n    if (!isStarted) {\n      setResponse(ctx, StatusCodes.FORBIDDEN);\n      return;\n    }\n  }\n\n  const result = await interviewService.createInterview(courseTaskId, interviewerGithubId, studentGithubId);\n\n  await sendInteviewerAssignedNotification(logger, courseId, { interviewerGithubId, studentGithubId });\n\n  setResponse(ctx, StatusCodes.OK, { id: result?.id });\n};\n\nexport async function sendInteviewerAssignedNotification(\n  logger: ILogger,\n  courseId: number,\n  {\n    interviewerGithubId,\n    interviewerId,\n    studentGithubId,\n    studentId,\n  }: {\n    interviewerGithubId?: string;\n    studentGithubId?: string | undefined;\n    studentId?: number;\n    interviewerId?: number;\n  },\n) {\n  if ((!interviewerGithubId && !interviewerId) || (!studentId && !studentGithubId)) {\n    logger.info(`sendInteviewerAssignedNotification: missing id info`);\n    return;\n  }\n  try {\n    const mentorRequest =\n      (interviewerGithubId !== undefined && courseService.queryMentorByGithubId(courseId, interviewerGithubId)) ||\n      (interviewerId !== undefined && courseService.queryMentorById(courseId, interviewerId));\n\n    const studentRequest =\n      (studentGithubId !== undefined && courseService.queryStudentByGithubId(courseId, studentGithubId)) ||\n      (studentId !== undefined && courseService.queryStudentById(courseId, studentId));\n\n    if (!mentorRequest || !studentRequest) return;\n    const [interviewer, student] = await Promise.all([mentorRequest, studentRequest]);\n    if (!student || !interviewer) return;\n\n    await notificationService.sendNotification({\n      userId: student.userId,\n      notificationId: 'interviewerAssigned',\n      data: {\n        interviewer,\n      },\n    });\n  } catch (e) {\n    logger.error(`sendInteviewerAssignedNotification: ${(e as Error).message}`);\n  }\n}\n\nexport const createInterviews = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = Number(ctx.params.courseId);\n  const courseTaskId: number = Number(ctx.params.courseTaskId);\n\n  try {\n    const { clean = false, registrationEnabled = true } = ctx.request.body as {\n      clean: boolean;\n      registrationEnabled: boolean;\n    };\n\n    const courseTask = await taskService.getCourseTask(courseTaskId);\n\n    if (courseTask == null) {\n      setResponse(ctx, StatusCodes.BAD_REQUEST, { message: 'not valid course task' });\n      return;\n    }\n\n    if (courseTask.isCreatingInterviewPairs) {\n      setResponse(ctx, StatusCodes.CONFLICT, { message: 'course task is already being processed' });\n      return;\n    }\n\n    await taskService.changeCourseTaskProcessing(courseTaskId, true);\n\n    const interviewService = new InterviewService(courseId, logger);\n    const result = await interviewService.createInterviewsAutomatically(courseTaskId, {\n      clean,\n      registrationEnabled,\n    });\n\n    if (result == null) {\n      setResponse(ctx, StatusCodes.BAD_REQUEST);\n      return;\n    }\n\n    await Promise.all(\n      result.map(\n        async pair =>\n          await sendInteviewerAssignedNotification(logger, courseId, {\n            interviewerId: pair.mentorId,\n            studentId: pair.studentId,\n          }),\n      ),\n    );\n    setResponse(ctx, StatusCodes.OK, result);\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  } finally {\n    await taskService.changeCourseTaskProcessing(courseTaskId, false);\n  }\n};\n\nexport const cancelInterview = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = Number(ctx.params.courseId);\n  const pairId: number = Number(ctx.params.id);\n\n  try {\n    const interviewService = new InterviewService(courseId, logger);\n    await interviewService.cancelInterviewPair(pairId);\n    setResponse(ctx, StatusCodes.OK, {});\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/mentor.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, NOT_FOUND, OK, StatusCodes } from 'http-status-codes';\nimport { getCustomRepository, getRepository } from 'typeorm';\nimport { PreferredStudentsLocation } from '../../models/mentorRegistry';\nimport { ILogger } from '../../logger';\nimport { Mentor, MentorRegistry, Student } from '../../models';\nimport { StudentRepository } from '../../repositories/student.repository';\nimport { courseService } from '../../services';\nimport { getUserByGithubId } from '../../services/user.service';\nimport { userGuards } from '../guards';\nimport { setResponse } from '../utils';\n\ntype Params = { courseId: number; githubId: string; courseTaskId: number };\n\nexport const getMentorStudents = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params as Params;\n  const repository = getCustomRepository(StudentRepository);\n  const students = await repository.findByMentor(courseId, githubId);\n  setResponse(ctx, OK, students);\n};\n\nexport const deleteMentor = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const githubId: string = ctx.params.githubId;\n  await courseService.expelMentor(courseId, githubId);\n  setResponse(ctx, OK);\n};\n\nexport const restoreExpelledMentor = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const githubId: string = ctx.params.githubId;\n  await courseService.restoreMentor(courseId, githubId);\n  setResponse(ctx, OK);\n};\n\nexport const getMentorInterview = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId, courseTaskId } = ctx.params as Params;\n  const mentor = await courseService.getMentorByGithubId(courseId, githubId);\n  if (mentor == null) {\n    setResponse(ctx, NOT_FOUND);\n    return;\n  }\n  const students = await courseService.getInterviewStudentsByMentorId(courseTaskId, mentor.id);\n  setResponse(ctx, OK, students);\n};\n\nexport const postMentor = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = +ctx.params.courseId;\n  const githubId: string = ctx.params.githubId;\n\n  const input: { maxStudentsLimit: number; preferedStudentsLocation: PreferredStudentsLocation; students: number[] } =\n    ctx.request.body;\n  const user = await getUserByGithubId(githubId);\n\n  if (user == null) {\n    setResponse(ctx, BAD_REQUEST);\n    return;\n  }\n\n  const { maxStudentsLimit, preferedStudentsLocation } = input;\n  const data = {\n    ...(maxStudentsLimit ? { maxStudentsLimit } : {}),\n    ...(preferedStudentsLocation ? { studentsPreference: preferedStudentsLocation } : {}),\n  };\n  const mentorRepository = getRepository(Mentor);\n  const exist = await mentorRepository.findOne({ where: { courseId, userId: user.id } });\n  let mentorId = exist?.id;\n  if (mentorId == null) {\n    const mentorRegistrationInfo = await getRepository(MentorRegistry).findOne({ where: { userId: user.id } });\n\n    const guard = userGuards(ctx.state.user);\n    const isMentorApproved = (mentorRegistrationInfo?.preselectedCourses ?? []).some(\n      approvedCourseId => courseId === +approvedCourseId,\n    );\n\n    if (!isMentorApproved && !(guard.isPowerUser(courseId) || guard.isSupervisor(courseId))) {\n      setResponse(ctx, StatusCodes.FORBIDDEN);\n      return;\n    }\n    const {\n      identifiers: [identifier],\n    } = await mentorRepository.insert({\n      courseId,\n      userId: user.id,\n      ...data,\n    });\n    mentorId = identifier['id'];\n  } else {\n    await mentorRepository.update(mentorId, data);\n  }\n\n  const studentRepository = getRepository(Student);\n  if (input.students.length > 0) {\n    if (exist) {\n      await studentRepository.update({ mentorId }, { mentorId: null });\n    }\n    await studentRepository.update(input.students, { mentorId });\n  }\n\n  setResponse(ctx, OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/repository.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { ILogger } from '../../logger';\nimport { RepositoryService } from '../../services';\nimport { setResponse } from '../utils';\nimport { GithubService } from '../../services/github.service';\n\nexport const createRepositories = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId } = ctx.params as { courseId: number };\n  const github = await GithubService.initGithub();\n  const repositoryService = new RepositoryService(courseId, github, logger);\n  repositoryService.createMany();\n  setResponse(ctx, StatusCodes.OK);\n};\n\nexport const updateRepositories = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId } = ctx.params as { courseId: number };\n  const github = await GithubService.initGithub();\n  const repositoryService = new RepositoryService(courseId, github, logger);\n  repositoryService.updateRepositories();\n  setResponse(ctx, StatusCodes.OK);\n};\n\nexport const createRepository = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params as { courseId: number; githubId: string };\n  const github = await GithubService.initGithub();\n  const repositoryService = new RepositoryService(courseId, github, logger);\n  const result = await repositoryService.createSingle(githubId);\n  setResponse(ctx, StatusCodes.OK, { repository: result?.repository });\n};\n\nexport const inviteMentorToTeam = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params as { courseId: number; githubId: string };\n  const github = await GithubService.initGithub();\n  const repositoryService = new RepositoryService(courseId, github, logger);\n  await repositoryService.inviteMentor(githubId);\n  setResponse(ctx, StatusCodes.OK);\n};\n\nexport const inviteAllMentorsToTeam = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId } = ctx.params as { courseId: number };\n  const github = await GithubService.initGithub();\n  const repositoryService = new RepositoryService(courseId, github, logger);\n  await repositoryService.inviteAllMentors();\n  setResponse(ctx, StatusCodes.OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/schedule.ts",
    "content": "import { getRepository, getConnection } from 'typeorm';\nimport { OK } from 'http-status-codes';\nimport { parseAsync } from 'json2csv';\nimport Router from '@koa/router';\nimport moment from 'moment-timezone';\nimport { Task, CourseTask, Event, CourseEvent } from '../../models';\nimport { ILogger } from '../../logger';\nimport { getCourseTasksWithOwner, getEvents } from '../../services/course.service';\nimport { getUserByGithubId } from '../../services/user.service';\nimport { setCsvResponse, setResponse, dateFormatter } from '../utils';\nimport { Checker } from '../../models/courseTask';\n\nconst DEFAULT_TIMEZONE = 'Europe/Minsk';\n\ntype EntityFromCSV = {\n  entityType: 'task' | 'event';\n  templateId: number;\n  id: number;\n  startDate: string;\n  endDate: string;\n  type: string;\n  special: string;\n  name: string;\n  descriptionUrl: string;\n  githubId: string | null;\n  place: string | null;\n  checker: Checker | null;\n  pairsCount: number | null;\n};\n\nexport const getScheduleAsCsv = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = Number(ctx.params.courseId);\n  const timeZone = ctx.params.timeZone ? ctx.params.timeZone.replace('_', '/') : DEFAULT_TIMEZONE;\n  const courseTasks = await getCourseTasksWithOwner(courseId);\n  const courseEvents = await getEvents(courseId);\n\n  const tasksToCsv = courseTasks.map(item => ({\n    entityType: 'task',\n    templateId: item.taskId,\n    id: item.id,\n    startDate: dateFormatter(item.studentStartDate as string, timeZone, 'YYYY-MM-DD HH:mm'),\n    endDate: dateFormatter(item.studentEndDate as string, timeZone, 'YYYY-MM-DD HH:mm'),\n    type: item.type || item.task.type,\n    name: item.task.name,\n    descriptionUrl: item.task.descriptionUrl,\n    githubId: item.taskOwner ? item.taskOwner.githubId : null,\n    place: null,\n    checker: item.checker,\n    pairsCount: item.pairsCount,\n  }));\n  const eventsToCsv = courseEvents.map(item => ({\n    entityType: 'event',\n    templateId: item.eventId,\n    id: item.id,\n    startDate: item.dateTime ? dateFormatter(item.dateTime.toString(), timeZone, 'YYYY-MM-DD HH:mm') : '',\n    type: item.event.type,\n    special: item.special,\n    name: item.event.name,\n    descriptionUrl: item.event.descriptionUrl,\n    githubId: item.organizer ? item.organizer.githubId : null,\n    place: item.place,\n    checker: null,\n    pairsCount: null,\n  }));\n\n  const csv = await parseAsync(\n    [...tasksToCsv, ...eventsToCsv].sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()),\n  );\n\n  setCsvResponse(ctx, OK, csv, `schedule_${courseId}`);\n};\n\nexport const setScheduleFromCsv = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = Number(ctx.params.courseId);\n  const timeZone = ctx.params.timeZone ? ctx.params.timeZone.replace('_', '/') : DEFAULT_TIMEZONE;\n  const data = ctx.request.body as EntityFromCSV[];\n\n  const tasks = data.filter((entity: EntityFromCSV) => entity.entityType === 'task');\n  const events = data.filter((entity: EntityFromCSV) => entity.entityType === 'event');\n  const queryRunner = getConnection().createQueryRunner();\n  let response = 'Import CSV file successfully finished.';\n\n  await queryRunner.startTransaction();\n  try {\n    await saveTasks(tasks, courseId, timeZone);\n    await saveEvents(events, courseId, timeZone);\n    await queryRunner.commitTransaction();\n  } catch (err) {\n    const error = err as Error;\n    logger.error(error.message);\n    response = error.message;\n    await queryRunner.rollbackTransaction();\n    return;\n  } finally {\n    await queryRunner.release();\n    setResponse(ctx, OK, response);\n  }\n};\n\nconst saveTasks = async (tasks: EntityFromCSV[], courseId: number, timeZone: string) => {\n  for await (const task of tasks) {\n    const taskData = {\n      name: task.name,\n      type: task.type,\n      descriptionUrl: task.descriptionUrl || '',\n    } as Partial<Task>;\n\n    const user = task.githubId ? await getUserByGithubId(task.githubId) : null;\n\n    const courseTaskData = {\n      courseId,\n      taskId: task.templateId,\n      studentStartDate: moment(task.startDate).tz(timeZone).toISOString() || null,\n      studentEndDate: moment(task.endDate).tz(timeZone).toISOString() || null,\n      special: task.special,\n      taskOwner: user,\n      checker: task.checker,\n      pairsCount: task.pairsCount || null,\n    } as Partial<CourseTask>;\n\n    // update task & courseTask\n    if (task.templateId && task.id) {\n      await getRepository(Task).update({ id: task.templateId }, taskData);\n      await getRepository(CourseTask).update({ id: task.id }, courseTaskData);\n    }\n\n    // create new courseTask\n    if (task.templateId && !task.id) {\n      await getRepository(Task).update({ id: task.templateId }, taskData);\n      const { id } = await getRepository(CourseTask).save(courseTaskData);\n\n      if (!id) {\n        throw new Error('Creating new course task failed.');\n      }\n    }\n\n    //create task & courseTask\n    if (!task.templateId) {\n      const { id } = await getRepository(Task).save(taskData);\n\n      if (!id) {\n        throw new Error('Creating new task failed.');\n      }\n\n      const { id: courseId } = await getRepository(CourseTask).save({ ...courseTaskData, taskId: id });\n\n      if (!courseId) {\n        throw new Error('Creating new course task failed.');\n      }\n    }\n  }\n};\n\nconst saveEvents = async (events: EntityFromCSV[], courseId: number, timeZone: string) => {\n  for await (const event of events) {\n    const eventData: Partial<Event> = {\n      name: event.name,\n      type: event.type,\n      descriptionUrl: event.descriptionUrl || '',\n    };\n\n    const user = event.githubId ? await getUserByGithubId(event.githubId) : null;\n\n    const courseEventData = {\n      courseId,\n      eventId: event.templateId,\n      dateTime: moment(event.startDate).tz(timeZone).toISOString() || null,\n      special: event.special,\n      organizer: user,\n      place: event.place || null,\n    } as Partial<CourseEvent>;\n\n    // update event & courseEvent\n    if (event.templateId && event.id) {\n      await getRepository(Event).update({ id: event.templateId }, eventData);\n      await getRepository(CourseEvent).update({ id: event.id }, courseEventData);\n    }\n\n    // create new courseEvent\n    if (event.templateId && !event.id) {\n      await getRepository(Event).update({ id: event.templateId }, eventData);\n      const { id } = await getRepository(CourseEvent).save(courseEventData);\n\n      if (!id) {\n        throw new Error('Creating new course event failed.');\n      }\n    }\n\n    //create event & courseEvent\n    if (!event.templateId) {\n      const { id } = await getRepository(Event).save(eventData);\n\n      if (!id) {\n        throw new Error('Creating new event failed.');\n      }\n\n      const { id: courseId } = await getRepository(CourseEvent).save({ ...courseEventData, eventId: id });\n\n      if (!courseId) {\n        throw new Error('Creating new course event failed.');\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/score/createMultipleScores.ts",
    "content": "import Router from '@koa/router';\nimport { OK } from 'http-status-codes';\nimport { getRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { IUserSession, Student } from '../../../models';\nimport { OperationResult } from '../../../services';\nimport { ScoreService } from '../../../services/score';\nimport { setResponse } from '../../utils';\n\ntype ScoresInput = {\n  studentGithubId: string;\n  courseTaskId: number;\n  score: number;\n  comment: string;\n  githubPrUrl?: string;\n  mentorGithubId?: string;\n};\n\nexport const createMultipleScores = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const courseTaskId: number = ctx.params.courseTaskId;\n\n  const inputData: ScoresInput[] = ctx.request.body;\n  const result: OperationResult[] = [];\n\n  const scoreService = new ScoreService(courseId);\n\n  for (const item of inputData) {\n    try {\n      logger.info(item.studentGithubId);\n\n      const data = {\n        studentGithubId: item.studentGithubId,\n        courseTaskId,\n        score: Math.round(Number(item.score)),\n        comment: item.comment || '',\n        githubPrUrl: item.githubPrUrl || '',\n      };\n\n      const { studentGithubId } = data;\n\n      const student = await getRepository(Student)\n        .createQueryBuilder('student')\n        .innerJoinAndSelect('student.user', 'user')\n        .where('\"user\".\"githubId\" = :studentGithubId AND \"student\".\"courseId\" = :courseId', {\n          studentGithubId,\n          courseId,\n        })\n        .getOne();\n\n      if (student == null) {\n        result.push({ status: 'skipped', value: `no student: ${studentGithubId}` });\n        continue;\n      }\n\n      const user = ctx.state.user as IUserSession | null;\n      const authorId = user?.id ?? 0;\n\n      const [isNew] = await scoreService.saveScore(Number(student.id), Number(courseTaskId), {\n        authorId,\n        comment: data.comment,\n        score: data.score,\n        githubPrUrl: data.githubPrUrl,\n      });\n\n      if (isNew) {\n        result.push({ status: 'created', value: undefined });\n      } else {\n        result.push({ status: 'updated', value: undefined });\n      }\n    } catch (e) {\n      result.push({ status: 'failed', value: (e as Error).message });\n    }\n  }\n\n  setResponse(ctx, OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/course/score/createSingleScore.ts",
    "content": "import Router from '@koa/router';\nimport { AxiosError } from 'axios';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { config } from '../../../config';\nimport { ILogger } from '../../../logger';\nimport { isAdmin, isManager, isTaskOwner, IUserSession } from '../../../models';\nimport { courseService, notificationService, taskService } from '../../../services';\nimport { ScoreService } from '../../../services/score';\nimport { setResponse } from '../../utils';\n\ntype ScoreInput = {\n  score: number | string;\n  comment?: string;\n  githubPrUrl?: string;\n};\n\nexport const createSingleScore = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, courseTaskId, githubId } = ctx.params;\n  const session = ctx.state.user as IUserSession;\n\n  const inputData: ScoreInput = ctx.request.body;\n\n  const scoreService = new ScoreService(courseId);\n\n  const student = await courseService.queryStudentByGithubId(courseId, githubId);\n  if (student == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid student' });\n    return;\n  }\n\n  if (Number.isNaN(Number(inputData.score))) {\n    setResponse(ctx, BAD_REQUEST, 'no score');\n    return;\n  }\n  const data = {\n    score: Math.round(Number(inputData.score)),\n    comment: inputData.comment || '',\n    githubPrUrl: inputData.githubPrUrl,\n  };\n  logger.info(data);\n\n  const authorId = ctx.state.user.id;\n  const courseTask = await taskService.getCourseTask(courseTaskId, true);\n  if (courseTask == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid course task' });\n    return;\n  }\n\n  const mentor = await courseService.getMentorByUserId(courseId, authorId);\n\n  const isNotTaskOwner = !isTaskOwner(session, courseId);\n  if (mentor == null && !isAdmin(session) && !isManager(session, courseId) && isNotTaskOwner) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid submitter' });\n    return;\n  }\n\n  const [result, previousScore] = await scoreService.saveScore(student.id, courseTask.id, { ...data, authorId });\n  setResponse(ctx, OK, result);\n\n  try {\n    await notificationService.sendNotification({\n      userId: student.userId,\n      notificationId: 'taskGrade',\n      data: {\n        previousScore,\n        courseTask,\n        score: data.score,\n        comment: data.comment,\n        resultLink: `${config.host}/course/student/dashboard?course=${courseTask.course.alias}&statType=completed`,\n      },\n    });\n  } catch (e) {\n    logger.error(`Failed to publish notification ${(e as AxiosError).message}`);\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/score/getScoreByStudent.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { courseService } from '../../../services';\nimport { getStudentScore } from '../../../services/course.service';\nimport { setResponse } from '../../utils';\n\nexport const getScoreByStudent = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params;\n\n  const student = await courseService.queryStudentByGithubId(courseId, githubId);\n  if (student == null) {\n    setResponse(ctx, BAD_REQUEST);\n    return;\n  }\n  const students = await getStudentScore(student.id);\n  setResponse(ctx, OK, students);\n};\n"
  },
  {
    "path": "server/src/routes/course/score/getScoreCsv.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { parseAsync, transforms } from 'json2csv';\nimport { ILogger } from '../../../logger';\nimport { IUserSession, CourseRole } from '../../../models';\nimport { ScoreService } from '../../../services/score';\nimport { setCsvResponse } from '../../utils';\n\nexport const getScoreCsv = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = ctx.params.courseId;\n  const user = ctx.state?.user as IUserSession | undefined;\n  const { cityName, ['mentor.githubId']: mentor } = ctx.query;\n  const isCourseManager = user?.courses[courseId]?.roles?.includes(CourseRole.Manager);\n\n  const filters = {\n    activeOnly: false,\n    cityName,\n    'mentor.githubId': mentor,\n  };\n\n  const service = new ScoreService(courseId, {\n    includeContacts: (user?.isAdmin || user?.isHirer) ?? false,\n    includeCertificate: (user?.isAdmin || user?.isHirer || isCourseManager) ?? false,\n  });\n  const result = await service.getStudentsScoreForExport(filters);\n  const csv = await parseAsync(result, { transforms: [transforms.flatten()] });\n\n  setCsvResponse(ctx, StatusCodes.OK, csv, 'score');\n};\n"
  },
  {
    "path": "server/src/routes/course/score/index.ts",
    "content": "export * from './createMultipleScores';\nexport * from './createSingleScore';\nexport * from './getScoreCsv';\nexport * from './getScoreByStudent';\nexport * from './recalculateScore';\n"
  },
  {
    "path": "server/src/routes/course/score/recalculateScore.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { ILogger } from '../../../logger';\nimport { getCourse } from '../../../services/course.service';\nimport { ScoreService } from '../../../services/score';\nimport { setResponse } from '../../utils';\n\nexport const recalculateScore = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = Number(ctx.params.courseId);\n\n  const course = await getCourse(courseId);\n\n  await ScoreService.recalculateTotalScore(logger, course ? [course] : undefined);\n\n  setResponse(ctx, StatusCodes.OK);\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/cancelInterview.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { StageInterview } from '../../../models';\nimport { setResponse } from '../../utils';\n\nexport const cancelInterview = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const interviewId = Number(ctx.params.interviewId);\n  try {\n    const interview = await getRepository(StageInterview).update(interviewId, { isCanceled: true });\n    setResponse(ctx, StatusCodes.OK, interview);\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/createFeedback.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository, getRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { IUserSession, Student } from '../../../models';\nimport { StageInterviewRepository } from '../../../repositories/stageInterview.repository';\nimport { StageInterviewFeedbackRepository } from '../../../repositories/stageInterviewFeedback.repository';\nimport { courseService } from '../../../services';\nimport { setResponse } from '../../utils';\n\ntype BodyParams = {\n  githubId: string;\n  json: string;\n  isCompleted: boolean;\n  decision: string | null;\n  isGoodCandidate: boolean | null;\n};\n\n/**\n * @deprecated. should be removed after feedbacks are migrated to new template\n */\nexport const createFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const data: BodyParams = ctx.request.body;\n  const { courseId } = ctx.params;\n\n  const user = ctx.state!.user as IUserSession;\n  const githubId = data.githubId;\n\n  try {\n    const repository = getCustomRepository(StageInterviewRepository);\n    const interviews = await repository.findByStudent(courseId, githubId);\n    const interview = interviews.find(it => it.interviewer.githubId === user.githubId);\n\n    if (interview == null) {\n      throw new Error(`Stage Interview for interviewer='${user.githubId}' and student='${githubId}' is not found'`);\n    }\n\n    const feedbackRepository = getCustomRepository(StageInterviewFeedbackRepository);\n    await feedbackRepository.create(interview.id, data);\n\n    const [student, mentor] = await Promise.all([\n      courseService.queryStudentByGithubId(courseId, githubId),\n      courseService.queryMentorByGithubId(courseId, user.githubId),\n    ]);\n\n    if (data.decision === 'yes' && student && mentor) {\n      await getRepository(Student).update(student?.id, { mentorId: mentor.id });\n    }\n\n    setResponse(ctx, StatusCodes.OK, { stageInterviewId: interview.id, ...data });\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/createInterview.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { StageInterviewRepository } from '../../../repositories/stageInterview.repository';\nimport { setResponse } from '../../utils';\nimport { sendInteviewerAssignedNotification } from '../interviews';\n\ntype RequestParams = {\n  courseId: number;\n  studentGithubId: string;\n  githubId: string;\n};\n\nexport const createInterview = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, studentGithubId, githubId: mentorGithubId } = ctx.params as RequestParams;\n  const repository = getCustomRepository(StageInterviewRepository);\n  const result = await repository.create(courseId, studentGithubId, mentorGithubId);\n\n  await sendInteviewerAssignedNotification(logger, courseId, { interviewerGithubId: mentorGithubId, studentGithubId });\n  setResponse(ctx, StatusCodes.OK, { id: result?.id });\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/createInterviews.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { StageInterviewRepository } from '../../../repositories/stageInterview.repository';\nimport { setResponse } from '../../utils';\nimport { sendInteviewerAssignedNotification } from '../interviews';\n\ntype BodyParams = {\n  noRegistration: boolean;\n};\n\nexport const createInterviews = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = Number(ctx.params.courseId);\n  try {\n    const { noRegistration = false } = ctx.request.body as BodyParams;\n    const repository = getCustomRepository(StageInterviewRepository);\n    const result = await repository.createAutomatically(courseId, noRegistration);\n\n    await Promise.all(\n      result.map(\n        async pair =>\n          await sendInteviewerAssignedNotification(logger, courseId, {\n            interviewerId: pair.mentorId,\n            studentId: pair.studentId,\n          }),\n      ),\n    );\n    setResponse(ctx, StatusCodes.OK, result);\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/getFeedback.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { IUserSession } from '../../../models';\nimport { StageInterviewFeedbackRepository } from '../../../repositories/stageInterviewFeedback.repository';\nimport { setResponse } from '../../utils';\n\n/**\n * @deprecated. should be removed after feedbacks are migrated to new template\n */\nexport const getFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { interviewId } = ctx.params;\n  const mentorGithubId = (ctx.state!.user as IUserSession).githubId;\n  try {\n    const repository = getCustomRepository(StageInterviewFeedbackRepository);\n    const feedback = await repository.find(Number(interviewId), mentorGithubId);\n    const result = JSON.parse(feedback?.json ?? '{}');\n    setResponse(ctx, StatusCodes.OK, result);\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/getInterviewStudent.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { StageInterviewRepository } from '../../../repositories/stageInterview.repository';\nimport { courseService } from '../../../services';\nimport { setResponse } from '../../utils';\n\ntype RequestParams = {\n  courseId: number;\n  githubId: string;\n};\n\nexport const getInterviewStudent = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params as RequestParams;\n  try {\n    const student = await courseService.queryStudentByGithubId(courseId, githubId);\n    if (student == null) {\n      setResponse(ctx, StatusCodes.BAD_REQUEST, null);\n      return;\n    }\n    const repository = getCustomRepository(StageInterviewRepository);\n    const result = await repository.findStudent(courseId, student.id);\n    setResponse(ctx, StatusCodes.OK, result);\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/getInterviewerStudents.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { StageInterviewRepository } from '../../../repositories/stageInterview.repository';\nimport { setResponse } from '../../utils';\n\nexport const getInterviewerStudents = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params;\n  const repository = getCustomRepository(StageInterviewRepository);\n  const result = await repository.findByInterviewer(courseId, githubId);\n  setResponse(ctx, StatusCodes.OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/getInterviews.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { StageInterviewRepository } from '../../../repositories/stageInterview.repository';\nimport { ILogger } from '../../../logger';\nimport { setResponse } from '../../utils';\n\nexport const getInterviews = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = Number(ctx.params.courseId);\n  const repository = getCustomRepository(StageInterviewRepository);\n  const result = await repository.findMany(courseId);\n  setResponse(ctx, StatusCodes.OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/index.ts",
    "content": "export { createFeedback } from './createFeedback';\nexport { createInterview } from './createInterview';\nexport { createInterviews } from './createInterviews';\nexport { cancelInterview } from './cancelInterview';\nexport { getFeedback } from './getFeedback';\nexport { getInterviewStudent } from './getInterviewStudent';\nexport { getInterviewerStudents } from './getInterviewerStudents';\nexport { getInterviews } from './getInterviews';\nexport { updateInterview } from './updateInterview';\n"
  },
  {
    "path": "server/src/routes/course/stageInterview/updateInterview.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { StageInterviewRepository } from '../../../repositories/stageInterview.repository';\nimport { setResponse } from '../../utils';\n\nexport const updateInterview = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  try {\n    const { interviewId } = ctx.params;\n    const { githubId } = ctx.request.body;\n\n    const repository = getCustomRepository(StageInterviewRepository);\n    await repository.updateInterviewer(Number(interviewId), githubId);\n    setResponse(ctx, StatusCodes.OK, {});\n  } catch (e) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, { message: (e as Error).message });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/student.ts",
    "content": "import { BAD_REQUEST, OK, FORBIDDEN, StatusCodes } from 'http-status-codes';\nimport Router from '@koa/router';\nimport { getRepository, getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../logger';\nimport { Feedback, TaskInterviewResult } from '../../models';\nimport { courseService, taskService, studentService } from '../../services';\nimport { setResponse } from '../utils';\nimport { StudentRepository } from '../../repositories/student.repository';\nimport { userGuards } from '../guards';\nimport { sendNotification } from '../../services/notification.service';\nimport { MentorBasic } from '../../../../common/models';\n\ntype FeedbackInput = { toUserId: number; comment: string };\n\nexport const updateStudentStatus = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId } = ctx.params;\n  const data: { comment?: string; status: 'expelled' | 'active' | 'self-study' } = ctx.request.body;\n  const { allow, message = 'no access' } = await studentService.canChangeStatus(ctx.state.user, courseId, githubId);\n\n  if (!allow) {\n    setResponse(ctx, BAD_REQUEST, { message });\n    return;\n  }\n\n  const studentRepository = getCustomRepository(StudentRepository);\n  switch (data.status) {\n    case 'active':\n      await studentRepository.restore(courseId, githubId);\n      setResponse(ctx, OK);\n      break;\n    case 'expelled':\n      await studentRepository.expel(courseId, githubId, data.comment);\n      setResponse(ctx, OK);\n      break;\n    case 'self-study':\n      await studentRepository.setSelfStudy(courseId, githubId, data.comment);\n      setResponse(ctx, OK);\n      break;\n    default:\n      setResponse(ctx, BAD_REQUEST, { message: 'not supported status' });\n      break;\n  }\n};\n\nexport const selfUpdateStudentStatus = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId } = ctx.params;\n  const data: { status: 'self-study'; comment?: string } = ctx.request.body;\n\n  if (data.status !== 'self-study') {\n    throw new Error('Not supported status');\n  }\n\n  if (ctx.state.user.githubId === githubId) {\n    const studentRepository = getCustomRepository(StudentRepository);\n    await studentRepository.setSelfStudy(courseId, githubId);\n    setResponse(ctx, OK);\n  } else {\n    setResponse(ctx, BAD_REQUEST, { message: 'access denied' });\n  }\n};\n\nexport const postFeedback = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const data: FeedbackInput = ctx.request.body;\n  const id = ctx.state.user.id;\n\n  const feedback: Partial<Feedback> = {\n    comment: data.comment,\n    courseId,\n    fromUser: id,\n    toUserId: data.toUserId,\n  };\n  const result = await getRepository(Feedback).save(feedback);\n\n  setResponse(ctx, OK, result);\n  return;\n};\n\nexport const updateStudent = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params;\n  const student = await courseService.queryStudentByGithubId(courseId, githubId);\n  const data: { mentorGithuId: string | null } = ctx.request.body;\n  if (student == null || data.mentorGithuId === undefined) {\n    setResponse(ctx, BAD_REQUEST, null);\n    return;\n  }\n  const user = ctx.state!.user;\n  const guard = userGuards(user);\n  const [isPowerUser, isSupervisor, isCourseMentor] = [\n    guard.isPowerUser(courseId),\n    guard.isSupervisor(courseId),\n    guard.isMentor(courseId),\n  ];\n  const isPowerUserOrSupervisor = isPowerUser || isSupervisor;\n  if (!isPowerUserOrSupervisor && isCourseMentor) {\n    const mentorStudents = await getCustomRepository(StudentRepository).findByMentor(courseId, user.githubId);\n    const isUpdatedStudentMenteeOfRequestor = mentorStudents.some(\n      ({ githubId: studentGithubId }) => studentGithubId === githubId,\n    );\n    const isSelfAssignStudent = user.githubId === data.mentorGithuId;\n    if (!isUpdatedStudentMenteeOfRequestor && !isSelfAssignStudent) {\n      setResponse(ctx, FORBIDDEN, null);\n      return;\n    }\n  }\n  const studentRepository = getCustomRepository(StudentRepository);\n  let mentor: MentorBasic | null = null;\n  if (data.mentorGithuId) {\n    mentor = await courseService.getMentorByGithubId(courseId, data.mentorGithuId);\n    if (!mentor) {\n      setResponse(ctx, StatusCodes.BAD_REQUEST);\n      return;\n    }\n  }\n  await studentRepository.setMentor(courseId, githubId, mentor?.id);\n\n  if (mentor) {\n    await sendNotification({\n      notificationId: 'mentor:assigned',\n      userId: student.id,\n      data: {\n        mentor,\n      },\n    });\n  }\n\n  const updatedStudent = await studentRepository.findAndIncludeMentor(courseId, githubId);\n\n  setResponse(ctx, OK, updatedStudent);\n};\n\nexport const getStudent = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params;\n  const studentRepository = getCustomRepository(StudentRepository);\n  const student = await studentRepository.findAndIncludeDetails(courseId, githubId);\n  setResponse(ctx, OK, student);\n};\n\nexport const createInterviewResult = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId, courseTaskId } = ctx.params as {\n    githubId: string;\n    courseId: number;\n    courseTaskId: number;\n  };\n  const userId = ctx.state.user.id;\n\n  const inputData: InterviewResultInput = ctx.request.body;\n\n  if (inputData.score == null) {\n    setResponse(ctx, BAD_REQUEST, 'no score');\n    return;\n  }\n\n  const [student, mentor] = await Promise.all([\n    courseService.queryStudentByGithubId(courseId, githubId),\n    courseService.getCourseMentor(courseId, userId),\n  ]);\n\n  if (student == null || mentor == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid student or mentor' });\n    return;\n  }\n\n  const courseTask = await taskService.getCourseTaskOnly(courseTaskId);\n  if (courseTask == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid course task' });\n    return;\n  }\n\n  const repository = getRepository(TaskInterviewResult);\n  const existingResult = await repository\n    .createQueryBuilder('taskInterviewResult')\n    .where('\"taskInterviewResult\".\"studentId\" = :studentId', { studentId: student.id })\n    .andWhere('\"taskInterviewResult\".\"courseTaskId\" = :courseTaskId', { courseTaskId: courseTask.id })\n    .andWhere('\"taskInterviewResult\".\"mentorId\" = :mentorId', { mentorId: mentor.id })\n    .getOne();\n\n  if (existingResult != null) {\n    const result = await repository.update(existingResult.id, {\n      formAnswers: inputData.formAnswers,\n      score: Math.round(Number(inputData.score)),\n      comment: inputData.comment || '',\n    });\n    setResponse(ctx, OK, result);\n    return;\n  }\n\n  const entry: Partial<TaskInterviewResult> = {\n    mentorId: mentor.id,\n    studentId: student.id,\n    formAnswers: inputData.formAnswers,\n    score: Math.round(Number(inputData.score)),\n    comment: inputData.comment || '',\n    courseTaskId: courseTask.id,\n  };\n  const result = await repository.insert(entry);\n  setResponse(ctx, OK, result);\n};\n\nexport const getCrossMentors = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId, courseId } = ctx.params as {\n    githubId: string;\n    courseId: number;\n  };\n\n  const taskCheckers = await courseService.getCrossMentorsByStudent(courseId, githubId);\n\n  setResponse(ctx, OK, taskCheckers);\n};\n\nexport const updateMentoringAvailability = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params;\n  const student = await courseService.queryStudentByGithubId(courseId, githubId);\n  const { mentoring = false } = ctx.request.body as { mentoring: boolean };\n  if (student == null || mentoring === undefined) {\n    setResponse(ctx, BAD_REQUEST, null);\n    return;\n  }\n  const studentRepository = getCustomRepository(StudentRepository);\n  await studentRepository.updateMentoringAvailability(student.id, mentoring);\n  setResponse(ctx, OK, {});\n};\n\ntype InterviewResultInput = {\n  score: number | string;\n  comment: string;\n  formAnswers: {\n    questionId: string;\n    questionText: string;\n    answer: string;\n  }[];\n};\n"
  },
  {
    "path": "server/src/routes/course/students.ts",
    "content": "import { NOT_FOUND, OK, BAD_REQUEST } from 'http-status-codes';\nimport { parseAsync } from 'json2csv';\nimport Router from '@koa/router';\nimport { getRepository, getCustomRepository } from 'typeorm';\nimport { MentorBasic } from '../../../../common/models';\nimport { ILogger } from '../../logger';\nimport { Student } from '../../models';\nimport { courseService, OperationResult, userService } from '../../services';\nimport { setCsvResponse, setResponse } from '../utils';\nimport { StudentRepository } from '../../repositories/student.repository';\n\nexport const getStudents = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = Number(ctx.params.courseId);\n  const status: string = ctx.query.status;\n  const students = await courseService.getStudents(courseId, status === 'active');\n  setResponse(ctx, OK, students);\n};\n\nexport const getStudentsCsv = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = Number(ctx.params.courseId);\n  const status: string = ctx.query.status;\n  const students = await courseService.getStudents(courseId, status === 'active');\n  const csv = await parseAsync(\n    students.map(student => ({\n      id: student.id,\n      githubId: student.githubId,\n      name: student.name,\n      isActive: student.isActive,\n      mentorName: (student.mentor as MentorBasic)?.name,\n      mentorGithubId: (student.mentor as MentorBasic)?.githubId,\n      totalScore: student.totalScore,\n      city: student.cityName,\n      country: student.countryName,\n      repository: student.repository,\n    })),\n  );\n  setCsvResponse(ctx, OK, csv, 'students');\n};\n\nexport const getStudentsWithDetails = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId = Number(ctx.params.courseId);\n  const status: string = ctx.query.status;\n  const students = await courseService.getStudentsWithDetails(courseId, status === 'active');\n  setResponse(ctx, OK, students);\n};\n\nexport const searchStudent = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, searchText } = ctx.params;\n  const { onlyStudentsWithoutMentorShown } = ctx.query;\n\n  const repository = getCustomRepository(StudentRepository);\n  const result = await repository.search(Number(courseId), searchText, onlyStudentsWithoutMentorShown === 'true');\n\n  setResponse(ctx, OK, result);\n};\n\ntype StudentInput = {\n  githubId: string;\n  isExpelled: boolean;\n  expellingReason: string;\n  readyFullTime: boolean;\n};\n\nexport const updateStatuses = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const data: {\n    criteria: { courseTaskIds?: number[]; minScore?: number };\n    options: { keepWithMentor?: boolean };\n    expellingReason: string;\n  } = ctx.request.body;\n\n  if (data == null || data.expellingReason == null) {\n    setResponse(ctx, BAD_REQUEST);\n    return;\n  }\n\n  const studentRepository = getCustomRepository(StudentRepository);\n  const students = await studentRepository.findForExpel(\n    courseId,\n    {\n      courseTaskIds: data.criteria.courseTaskIds ?? [],\n      minScore: data.criteria.minScore != null ? Number(data.criteria.minScore) : null,\n    },\n    data.options,\n  );\n  await studentRepository.save(\n    students.map(({ id }) => ({ id, isExpelled: true, endDate: new Date(), expellingReason: data.expellingReason })),\n  );\n\n  setResponse(ctx, OK);\n};\n\nexport const postStudents = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const data: StudentInput[] = ctx.request.body;\n\n  const studentRepository = getRepository(Student);\n\n  if (data == null) {\n    setResponse(ctx, NOT_FOUND);\n    return;\n  }\n\n  const result: OperationResult[] = [];\n  for (const item of data) {\n    const user = await userService.getUserByGithubId(item.githubId);\n    if (user == null || user.id == null) {\n      result.push({ status: 'skipped', value: `no user: ${item.githubId}` });\n      continue;\n    }\n\n    const existingStudent = await courseService.queryStudentByGithubId(courseId, item.githubId);\n    if (existingStudent) {\n      result.push({ status: 'skipped', value: `exists already: ${item.githubId}` });\n      continue;\n    }\n\n    const { githubId, ...restData } = item;\n    const student: Partial<Student> = { ...restData, userId: user.id, courseId };\n    const savedStudent = await studentRepository.save(student);\n    result.push({ status: 'created', value: savedStudent.id });\n  }\n\n  setResponse(ctx, OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/course/taskArtefact.ts",
    "content": "import Router from '@koa/router';\nimport { getRepository } from 'typeorm';\nimport { OK, BAD_REQUEST } from 'http-status-codes';\n\nimport { setResponse } from '../utils';\nimport { Student, Task, User, TaskArtefact } from '../../models';\nimport { ILogger } from '../../logger';\nimport { taskService, taskResultsService } from '../../services';\n\ntype Input = {\n  studentId: number | string;\n  comment?: string;\n  videoUrl?: string;\n  presentationUrl?: string;\n};\n\nexport const postTaskArtefact = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseTaskId: number = ctx.params.courseTaskId;\n\n  const authorId = ctx.state.user.id;\n  const courseTask = await taskService.getCourseTask(courseTaskId);\n  if (courseTask == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid course task' });\n    return;\n  }\n\n  const inputData: Input = ctx.request.body;\n  const data = {\n    courseTaskId: courseTask.id,\n    studentId: Number(inputData.studentId),\n    comment: inputData.comment || '',\n    videoUrl: inputData.videoUrl,\n    presentationUrl: inputData.presentationUrl,\n  };\n\n  const student = await getRepository(Student).findOne({ where: { id: data.studentId }, relations: ['user'] });\n  if (student == null) {\n    setResponse(ctx, BAD_REQUEST, { message: 'not valid student' });\n    return;\n  }\n\n  const { studentId } = data;\n  const task = courseTask.task as Task;\n\n  if (!task.allowStudentArtefacts) {\n    setResponse(ctx, BAD_REQUEST, { message: 'does not allow to student submit' });\n    return;\n  }\n  if (authorId === (student.user as User).id) {\n    const existingResult = await taskResultsService.getStudentTaskArtefact(studentId, courseTaskId);\n    if (existingResult == null) {\n      const taskArtefact = taskResultsService.createStudentArtefactTaskResult(data);\n      const addResult = await getRepository(TaskArtefact).insert(taskArtefact);\n      setResponse(ctx, OK, addResult);\n      return;\n    }\n    if (data.videoUrl) {\n      existingResult.videoUrl = data.videoUrl;\n    }\n    if (data.presentationUrl) {\n      existingResult.presentationUrl = data.presentationUrl;\n    }\n    const updateResult = await getRepository(TaskArtefact).update(existingResult.id, {\n      videoUrl: data.videoUrl,\n      presentationUrl: data.presentationUrl,\n      comment: data.comment || existingResult.comment,\n    });\n    setResponse(ctx, OK, updateResult);\n    return;\n  }\n};\n"
  },
  {
    "path": "server/src/routes/course/taskVerifications.ts",
    "content": "import { OK, BAD_REQUEST } from 'http-status-codes';\nimport Router from '@koa/router';\nimport { getRepository } from 'typeorm';\nimport { ILogger } from '../../logger';\nimport { TaskVerification } from '../../models';\nimport { setResponse } from '../utils';\nimport { getStudentByGithubId } from '../../services/course.service';\n\ntype Params = { courseId: number; githubId: string };\n\nexport const getStudentTaskVerifications = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId, githubId } = ctx.params as Params;\n\n  const student = await getStudentByGithubId(courseId, githubId);\n  if (student == null) {\n    setResponse(ctx, BAD_REQUEST, {});\n    return;\n  }\n\n  const verifications = await getRepository(TaskVerification)\n    .createQueryBuilder('v')\n    .innerJoin('v.courseTask', 'courseTask')\n    .innerJoin('courseTask.task', 'task')\n    .addSelect(['task.name', 'courseTask.id', 'courseTask.type'])\n    .where('v.studentId = :id', { id: student.id })\n    .andWhere('courseTask.disabled = :disabled', { disabled: false })\n    .orderBy('v.updatedDate', 'DESC')\n    .getMany();\n\n  setResponse(ctx, OK, verifications);\n};\n\nexport const getCourseTasksVerifications = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { courseId } = ctx.params as Params;\n\n  const verifications = await getRepository(TaskVerification)\n    .createQueryBuilder('v')\n    .select(['v.id', 'v.status', 'v.courseTaskId'])\n    .innerJoin('v.courseTask', 'courseTask')\n    .innerJoin('courseTask.task', 'task')\n    .innerJoin('v.student', 'student')\n    .innerJoin('student.user', 'user')\n    .addSelect([\n      'student.id',\n      'user.githubId',\n      'task.name',\n      'task.githubRepoName',\n      'task.sourceGithubRepoUrl',\n      'task.attributes',\n      'courseTask.id',\n    ])\n    .where('courseTask.courseId = :courseId', { courseId })\n    .andWhere('courseTask.disabled = :disabled', { disabled: false })\n    .andWhere(\"v.status = 'pending' \")\n    .orderBy('v.createdDate', 'ASC')\n    .getMany();\n\n  const result = verifications.map(verification => ({\n    courseId: Number(courseId),\n    id: verification.id,\n    githubId: verification.student.user.githubId,\n    courseTaskId: verification.courseTaskId,\n    taskName: verification.courseTask.task.name,\n    sourceGithubRepoUrl: verification.courseTask.task.sourceGithubRepoUrl,\n    githubRepoName: verification.courseTask.task.githubRepoName,\n    attributes: verification.courseTask.task.attributes,\n  }));\n\n  setResponse(ctx, OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/course/tasks/createCourseTaskDistribution.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository, getRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { CourseTask, TaskChecker } from '../../../models';\nimport { MentorRepository } from '../../../repositories/mentor.repository';\nimport { CrossMentorDistributionService } from '../../../services/distribution';\nimport { setResponse } from '../../utils';\n\nconst crossMentorDistributionService = new CrossMentorDistributionService();\n\nexport const createCourseTaskDistribution = (logger: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseTaskId = Number(ctx.params.courseTaskId);\n  const courseId = Number(ctx.params.courseId);\n  const cleanDistribution = ctx.request.body.clean;\n\n  const courseTask = await getRepository(CourseTask).findOne({ where: { id: courseTaskId }, select: ['id'] });\n\n  if (courseTask == null) {\n    setResponse(ctx, StatusCodes.NOT_FOUND);\n    return;\n  }\n\n  const mentorRepository = getCustomRepository(MentorRepository);\n  const mentors = await mentorRepository.findActive(courseId, true);\n\n  if (mentors.length === 0) {\n    setResponse(ctx, StatusCodes.OK, {});\n    return;\n  }\n\n  const checkerRepository = getRepository(TaskChecker);\n\n  if (cleanDistribution) {\n    await checkerRepository.delete({ courseTaskId });\n  }\n\n  const existingPairs = await checkerRepository.findBy({ courseTaskId });\n\n  const { mentors: crossMentors } = crossMentorDistributionService.distribute(mentors, existingPairs);\n\n  const taskCheckPairs = crossMentors\n    .map(stm => stm.students?.map(s => ({ courseTaskId, mentorId: stm.id, studentId: s.id })) ?? [])\n    .reduce((acc, student) => acc.concat(student), []);\n\n  await checkerRepository.insert(taskCheckPairs);\n\n  logger.info(`Created [${taskCheckPairs.length}] cross-mentor pairs`);\n  setResponse(ctx, StatusCodes.OK, taskCheckPairs);\n};\n"
  },
  {
    "path": "server/src/routes/course/tasks/getCourseTasksDetails.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { ILogger } from '../../../logger';\nimport { CourseTaskRepository } from '../../../repositories/courseTask.repository';\nimport { setResponse } from '../../utils';\n\nexport const getCourseTasksDetails = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const courseId: number = ctx.params.courseId;\n  const repository = getCustomRepository(CourseTaskRepository);\n  const data = await repository.findWithDetails(courseId);\n\n  setResponse(ctx, StatusCodes.OK, data);\n};\n"
  },
  {
    "path": "server/src/routes/course/tasks/index.ts",
    "content": "export { createCourseTaskDistribution } from './createCourseTaskDistribution';\nexport { getCourseTasksDetails } from './getCourseTasksDetails';\n"
  },
  {
    "path": "server/src/routes/feedback.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { getCustomRepository } from 'typeorm';\nimport { FeedbackRepository, GetGratitudeQuery } from '../repositories/feedback.repository';\nimport { ILogger } from '../logger';\nimport { guard } from './guards';\nimport { setResponse } from './utils';\n\nconst { OK } = StatusCodes;\n\nexport function feedbackRoute(_: ILogger) {\n  const router = new Router<any, any>({ prefix: '/feedback' });\n  router.get('/gratitude', guard, getGratitudeFeedback());\n  return router;\n}\n\nconst getGratitudeFeedback = () => {\n  return async (ctx: Router.RouterContext) => {\n    const data: GetGratitudeQuery = ctx.query;\n    const feedbackRepository = getCustomRepository(FeedbackRepository);\n    const result = await feedbackRepository.getGratitude(data);\n    setResponse(ctx, OK, result);\n    return;\n  };\n};\n"
  },
  {
    "path": "server/src/routes/file/index.ts",
    "content": "import Router from '@koa/router';\nimport { ILogger } from '../../logger';\nimport { guard } from '../guards';\nimport { uploadFile } from './upload';\n\nexport function filesRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/file' });\n\n  router.post('/upload', guard, uploadFile(logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/file/upload.ts",
    "content": "import { OK } from 'http-status-codes';\nimport Router from '@koa/router';\nimport { IUserSession } from '../../models';\nimport { ILogger } from '../../logger';\nimport { setResponse } from '../utils';\nimport { uploadFileByGithubId } from '../../services/aws.service';\n\nexport const uploadFile = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId } = ctx.state!.user as IUserSession;\n  const { key = '' } = ctx.query as { key: string | undefined };\n  const body = ctx.request.body;\n  const response = await uploadFileByGithubId(githubId, key, body);\n  setResponse(ctx, OK, response);\n};\n"
  },
  {
    "path": "server/src/routes/guards.ts",
    "content": "import Router from '@koa/router';\nimport { config } from '../config';\nimport {\n  IUserSession,\n  isAdmin,\n  isHirer,\n  isAnyManager,\n  isAnySupervisor,\n  isManager,\n  isMentor,\n  isAnyMentor,\n  isStudent,\n  isTaskOwner,\n  isSupervisor,\n  isDementor,\n} from '../models';\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst auth = require('koa-basic-auth'); //tslint:disable-line\n\nexport type RouterContext = Router.RouterContext<\n  unknown,\n  { state: { user?: IUserSession }; isAuthenticated?: () => boolean }\n>;\n\nconst basicAuthAdmin = auth({ name: config.admin.username, pass: config.admin.password });\n\nexport const basicAuthAws = auth({\n  name: config.users.cloud.username,\n  pass: config.users.cloud.password,\n});\n\nexport const crossCheckGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n\n    if (guards.isLoggedIn(ctx) && guards.isPowerUser(courseId)) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAws(ctx, next);\n};\n\nexport const userGuards = (user: IUserSession) => {\n  const guards = {\n    isAdmin: () => isAdmin(user),\n    isHirer: () => isHirer(user),\n    hasRole: (courseId: number) => !!user.courses[courseId],\n    isAnyManager: () => isAnyManager(user),\n    isAnySupervisor: () => isAnySupervisor(user),\n    isManager: (courseId: number) => isManager(user, courseId),\n    isDementor: (courseId: number) => isDementor(user, courseId),\n    isMentor: (courseId: number) => isMentor(user, courseId),\n    isAnyMentor: () => isAnyMentor(user),\n    isStudent: (courseId: number) => isStudent(user, courseId),\n    isTaskOwner: (courseId: number) => isTaskOwner(user, courseId),\n    isLoggedIn: (_: RouterContext) => user != null || config.isDevMode,\n    isSupervisor: (courseId: number) => isSupervisor(user, courseId),\n  };\n  return {\n    ...guards,\n    isPowerUser: (courseId: number) => guards.isAdmin() || guards.isManager(courseId),\n  };\n};\n\nexport const guard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  if (user) {\n    const guards = userGuards(user);\n    if (guards.isLoggedIn(ctx)) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  ctx.params.courseId = Number(ctx.params.courseId);\n  const user = ctx.state.user;\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (guards.isLoggedIn(ctx) && (guards.hasRole(courseId) || guards.isPowerUser(courseId))) {\n      await next();\n      return;\n    }\n  }\n\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseMentorGuard: Router.Middleware<unknown, RouterContext> = async (\n  ctx: RouterContext,\n  next: () => Promise<void>,\n) => {\n  ctx.params.courseId = Number(ctx.params.courseId);\n  const user = ctx.state.user;\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (\n      guards.isLoggedIn(ctx) &&\n      (guards.isMentor(courseId) || guards.isSupervisor(courseId) || guards.isPowerUser(courseId))\n    ) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseMentorOrDementorGuard: Router.Middleware<unknown, RouterContext> = async (\n  ctx: RouterContext,\n  next: () => Promise<void>,\n) => {\n  ctx.params.courseId = Number(ctx.params.courseId);\n  const user = ctx.state.user;\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (\n      (guards.isLoggedIn(ctx) &&\n        (guards.isMentor(courseId) || guards.isSupervisor(courseId) || guards.isPowerUser(courseId))) ||\n      guards.isDementor(courseId)\n    ) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const anyCourseMentorGuard: Router.Middleware<unknown, RouterContext> = async (\n  ctx: RouterContext,\n  next: () => Promise<void>,\n) => {\n  ctx.params.courseId = Number(ctx.params.courseId);\n  const user = ctx.state.user;\n  if (user) {\n    const guards = userGuards(user);\n    if (guards.isLoggedIn(ctx) && guards.isAnyMentor()) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const adminGuard: Router.Middleware<unknown, RouterContext> = async (\n  ctx: RouterContext,\n  next: () => Promise<void>,\n) => {\n  const user = ctx.state.user;\n  if (user) {\n    const guards = userGuards(user);\n    if (guards.isLoggedIn(ctx) && guards.isAdmin()) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const taskOwnerGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (\n      guards.isLoggedIn(ctx) &&\n      (guards.isTaskOwner(courseId) || guards.isMentor(courseId) || guards.isPowerUser(courseId))\n    ) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseManagerGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (guards.isLoggedIn(ctx) && guards.isPowerUser(courseId)) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseInterviewGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (guards.isLoggedIn(ctx) && (guards.isPowerUser(courseId) || guards.isMentor(courseId))) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const anyCourseManagerGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    if (guards.isLoggedIn(ctx) && (guards.isAnyManager() || guards.isAdmin())) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const anyCoursePowerUserGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    if (guards.isLoggedIn(ctx) && (guards.isAnyManager() || guards.isAnySupervisor() || guards.isAdmin())) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseSupervisorGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (guards.isLoggedIn(ctx) && (guards.isPowerUser(courseId) || guards.isSupervisor(courseId))) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseDementorGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (guards.isLoggedIn(ctx) && (guards.isPowerUser(courseId) || guards.isDementor(courseId))) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseSupervisorOrDementorGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (\n      guards.isLoggedIn(ctx) &&\n      (guards.isPowerUser(courseId) || guards.isSupervisor(courseId) || guards.isDementor(courseId))\n    ) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n\nexport const courseSupervisorOrMentorGuard = async (ctx: RouterContext, next: () => Promise<void>) => {\n  const user = ctx.state.user;\n  ctx.params.courseId = Number(ctx.params.courseId);\n  if (user) {\n    const guards = userGuards(user);\n    const { courseId } = ctx.params;\n    if (\n      guards.isLoggedIn(ctx) &&\n      (guards.isPowerUser(courseId) || guards.isSupervisor(courseId) || guards.isMentor(courseId))\n    ) {\n      await next();\n      return;\n    }\n  }\n  await basicAuthAdmin(ctx, next);\n};\n"
  },
  {
    "path": "server/src/routes/index.ts",
    "content": "import Router from '@koa/router';\n\nimport { courseRoute } from './course';\nimport { feedbackRoute } from './feedback';\nimport { filesRoute } from './file';\nimport { errorHandlerMiddleware } from './logging';\nimport { publicMeRouter } from './me';\nimport { profileRoute } from './profile';\nimport { registryRouter } from './registry';\nimport { repositoryRoute } from './repository';\nimport { taskRoute } from './task';\nimport { tasksRoute } from './tasks';\nimport { taskVerification } from './taskVerification';\nimport { usersRoute } from './users';\n\nimport { ILogger } from '../logger';\nimport { checksRoute } from './checks';\nimport { courseMiddleware, userRolesMiddleware } from './middlewares';\n\nexport * from './logging';\n\ntype RoutesMiddleware = (logger: ILogger) => { publicRouter: Router };\n\nfunction applyRouter(topRouter: Router, router: Router) {\n  topRouter.use(router.routes());\n  topRouter.use(router.allowedMethods());\n}\n\nexport const routesMiddleware: RoutesMiddleware = (logger: ILogger) => {\n  const router = new Router<any, any>();\n\n  router.use(errorHandlerMiddleware(logger));\n  router.use(userRolesMiddleware, courseMiddleware);\n\n  // public routes\n\n  applyRouter(router, publicMeRouter(logger));\n  applyRouter(router, registryRouter(logger));\n  applyRouter(router, courseRoute(logger));\n  applyRouter(router, usersRoute(logger));\n  applyRouter(router, taskRoute(logger));\n  applyRouter(router, tasksRoute(logger));\n  applyRouter(router, taskVerification(logger));\n  applyRouter(router, profileRoute(logger));\n  applyRouter(router, feedbackRoute(logger));\n  applyRouter(router, checksRoute(logger));\n  applyRouter(router, filesRoute(logger));\n  applyRouter(router, repositoryRoute(logger));\n\n  return { publicRouter: router };\n};\n"
  },
  {
    "path": "server/src/routes/logging.ts",
    "content": "import Router from '@koa/router';\nimport { Next } from 'koa';\nimport { ILogger, sendError } from '../logger';\n\nexport const routeLoggerMiddleware: Router.Middleware = async (ctx: Router.RouterContext<any, any>, next: Next) => {\n  const oldLogger = ctx.logger;\n  const userId = ctx.state && ctx.state.user ? ctx.state.user.id : undefined;\n  ctx.logger = ctx.logger.child({ module: 'route', userId });\n  await next();\n  ctx.logger = oldLogger;\n};\n\nexport const errorHandlerMiddleware = (logger: ILogger) => async (ctx: Router.RouterContext, next: Next) => {\n  try {\n    await next();\n  } catch (err) {\n    const error = err as Error & { status?: number };\n    if (error?.message === 'Unauthorized') {\n      logger.info('Unauthorized request');\n    } else {\n      logger.error(error);\n      await sendError(error);\n    }\n    ctx.status = error.status || 500;\n    ctx.body = JSON.stringify({\n      message: error.message,\n    });\n  }\n};\n"
  },
  {
    "path": "server/src/routes/me.ts",
    "content": "import Router from '@koa/router';\nimport { NOT_FOUND, OK } from 'http-status-codes';\nimport { getRepository } from 'typeorm';\nimport { User } from '../models';\nimport { ILogger } from '../logger';\nimport { setResponse } from './utils';\n\nexport function publicMeRouter(_: ILogger) {\n  const router = new Router<any, any>({ prefix: '/v2/me' });\n\n  /**\n   * @swagger\n   *\n   * /v2/me:\n   *   get:\n   *     description: Returns users profile\n   *     produces:\n   *       - application/json\n   *     responses:\n   *       200:\n   *         description: User object\n   */\n  router.get('/', async (ctx: Router.RouterContext) => {\n    const id = ctx.state!.user.id;\n    if (!id) {\n      setResponse(ctx, NOT_FOUND);\n      return;\n    }\n    const user = await getRepository(User).findOneBy({ id });\n    if (user === undefined) {\n      setResponse(ctx, NOT_FOUND);\n      return;\n    }\n    setResponse(ctx, OK, user);\n  });\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/middlewares.ts",
    "content": "import Router from '@koa/router';\nimport { StatusCodes } from 'http-status-codes';\nimport { createQueryBuilder } from 'typeorm';\nimport { Next } from 'koa';\nimport { setResponse } from './utils';\nimport { IUserSession, CourseUser, CourseRole, User, JwtToken } from '../models';\n\nexport const courseMiddleware = async (ctx: Router.RouterContext, next: Next) => {\n  if (!ctx.params.courseId) {\n    await next();\n    return;\n  }\n  const courseId = Number(ctx.params.courseId);\n  if (isNaN(courseId)) {\n    setResponse(ctx, StatusCodes.BAD_REQUEST, 'Incorrect [Course Id]');\n    return;\n  }\n  ctx.params.courseId = courseId;\n  const courseTaskId = Number(ctx.params.courseTaskId);\n  if (!isNaN(courseTaskId)) {\n    ctx.params.courseTaskId = courseTaskId;\n  }\n  await next();\n};\n\n/**\n * This middleware exists TEMPORARY for compatibility with /nestjs part as we need to add roles to the user session\n */\nexport const userRolesMiddleware = async (ctx: Router.RouterContext, next: Next) => {\n  const user = ctx.state?.user as JwtToken;\n\n  if (user == null) {\n    await next();\n    return;\n  }\n\n  const authDetails = await getAuthDetails(user.id);\n\n  const enrichedUser: IUserSession = {\n    ...authDetails,\n    ...user,\n    courses: {},\n  };\n\n  authDetails.students.forEach(student => {\n    const current = enrichedUser.courses[student.courseId] ?? { mentorId: null, studentId: null, roles: [] };\n    enrichedUser.courses[student.courseId] = {\n      ...current,\n      studentId: student.id,\n      roles: current.roles.includes(CourseRole.Student) ? current.roles : current.roles.concat([CourseRole.Student]),\n    };\n  });\n  authDetails.mentors.forEach(mentor => {\n    const current = enrichedUser.courses[mentor.courseId] ?? { mentorId: null, studentId: null, roles: [] };\n    enrichedUser.courses[mentor.courseId] = {\n      ...current,\n      mentorId: mentor.id,\n      roles: current.roles.includes(CourseRole.Mentor) ? current.roles : current.roles.concat([CourseRole.Mentor]),\n    };\n  });\n  authDetails.courseUsers.forEach(courseUser => {\n    const current = enrichedUser.courses[courseUser.courseId] ?? { mentorId: null, studentId: null, roles: [] };\n    if (courseUser.isManager && !current.roles.includes(CourseRole.Manager)) {\n      current.roles.push(CourseRole.Manager);\n    }\n    if (courseUser.isSupervisor && !current.roles.includes(CourseRole.Supervisor)) {\n      current.roles.push(CourseRole.Supervisor);\n    }\n    if (courseUser.isDementor && !current.roles.includes(CourseRole.Dementor)) {\n      current.roles.push(CourseRole.Dementor);\n    }\n    enrichedUser.courses[courseUser.courseId] = current;\n  });\n\n  ctx.state.user = enrichedUser;\n  await next();\n};\n\ntype AuthDetails = {\n  id: number;\n  githubId: string;\n  students: { courseId: number; id: number; isExpelled?: boolean }[];\n  mentors: { courseId: number; id: number }[];\n  courseUsers: CourseUser[];\n};\n\n// TODO: copy/paste from nestjs. Temporary\nasync function getAuthDetails(id: number): Promise<AuthDetails> {\n  const query = createQueryBuilder(User, 'user')\n    .select('user.id', 'id')\n    .addSelect('user.githubId', 'githubId')\n    .addSelect(\n      qb =>\n        qb\n          .select(`jsonb_agg(json_build_object('id', mentor.id, 'courseId', mentor.\"courseId\"))`)\n          .from('mentor', 'mentor')\n          .where('mentor.userId = :id', { id }),\n      'mentors',\n    )\n    .addSelect(\n      qb =>\n        qb\n          .select(\n            `jsonb_agg(json_build_object('id', student.id, 'courseId', student.\"courseId\", 'isExpelled', student.\"isExpelled\"))`,\n          )\n          .from('student', 'student')\n          .where('student.userId = :id', { id }),\n      'students',\n    )\n    .addSelect(\n      qb =>\n        qb.select('jsonb_agg(\"courseUser\")').from(CourseUser, 'courseUser').where('courseUser.userId = :id', { id }),\n      'courseUsers',\n    )\n    .where({ id });\n\n  const result = await query.getRawOne();\n  return {\n    id: result.id,\n    githubId: result.githubId,\n    students: result.students ?? [],\n    mentors: result.mentors ?? [],\n    courseUsers: result.courseUsers ?? [],\n  };\n}\n"
  },
  {
    "path": "server/src/routes/profile/__test__/permissions.test.ts",
    "content": "import { CourseRole, IUserSession } from '../../../models';\nimport { getPermissions, defineRole, getProfilePermissionsSettings } from '../permissions';\n\nconst mockSession = {\n  id: 1,\n  githubId: 'githubId',\n  isHirer: false,\n  isAdmin: false,\n  courses: {\n    '1': {\n      mentorId: 1,\n      studentId: 2,\n      roles: [],\n    },\n    '2': {\n      mentorId: 1,\n      studentId: 2,\n      roles: [],\n    },\n    '11': {\n      mentorId: 1,\n      studentId: 2,\n      roles: [],\n    },\n  },\n} as IUserSession;\n\ndescribe('getPermissions', () => {\n  it('Should be an instance of Function', () => {\n    expect(getPermissions).toBeInstanceOf(Function);\n  });\n  describe('Should return permissions object with all keys equal \"false\"', () => {\n    it('if \"isProfileOwner\" is \"false\" and no \"role\" and \"permissions\" have passed', () => {\n      expect(\n        getPermissions({\n          isProfileOwner: false,\n          isAdmin: false,\n        }),\n      ).toEqual({\n        isProfileVisible: false,\n        isAboutVisible: false,\n        isEducationVisible: false,\n        isEnglishVisible: false,\n        isEmailVisible: false,\n        isTelegramVisible: false,\n        isWhatsAppVisible: false,\n        isSkypeVisible: false,\n        isPhoneVisible: false,\n        isContactsNotesVisible: false,\n        isLinkedInVisible: false,\n        isPublicFeedbackVisible: false,\n        isMentorStatsVisible: false,\n        isStudentStatsVisible: false,\n        isStageInterviewFeedbackVisible: false,\n        isCoreJsFeedbackVisible: false,\n        isConsentsVisible: false,\n        isExpellingReasonVisible: false,\n      });\n    });\n  });\n  describe('Should return permissions object depends on \"role\" and \"permissions\" have passed', () => {\n    describe('if \"isProfileOwner\" is \"false\"', () => {\n      it('\"role\" is \"all\" and some \"permissions\" set with \"all\" = \"true\"', () => {\n        expect(\n          getPermissions({\n            isProfileOwner: false,\n            isAdmin: false,\n            role: 'all',\n            permissions: {\n              isProfileVisible: { all: true },\n              isAboutVisible: { all: true, mentor: true, student: true },\n              isEducationVisible: { all: true, mentor: true, student: true },\n              isEnglishVisible: { all: false, student: false },\n              isEmailVisible: { all: true, student: true },\n              isTelegramVisible: { all: false, student: false },\n              isSkypeVisible: { all: true, student: true },\n              isPhoneVisible: { all: false, student: false },\n              isContactsNotesVisible: { all: true, student: true },\n              isLinkedInVisible: { all: false, mentor: false, student: false },\n              isPublicFeedbackVisible: { all: true, mentor: true, student: true },\n              isMentorStatsVisible: { all: true, mentor: true, student: true },\n              isStudentStatsVisible: { all: true, student: true },\n            },\n          }),\n        ).toEqual({\n          isProfileVisible: true,\n          isAboutVisible: true,\n          isEducationVisible: true,\n          isEnglishVisible: false,\n          isEmailVisible: true,\n          isTelegramVisible: false,\n          isWhatsAppVisible: false,\n          isSkypeVisible: true,\n          isPhoneVisible: false,\n          isContactsNotesVisible: true,\n          isLinkedInVisible: false,\n          isPublicFeedbackVisible: true,\n          isMentorStatsVisible: true,\n          isStudentStatsVisible: true,\n          isStageInterviewFeedbackVisible: false,\n          isCoreJsFeedbackVisible: false,\n          isConsentsVisible: false,\n          isExpellingReasonVisible: false,\n        });\n      });\n      it('\"role\" is \"mentor\" and some \"permissions\" set with \"mentor\" = \"true\"', () => {\n        expect(\n          getPermissions({\n            isProfileOwner: false,\n            isAdmin: false,\n            role: 'mentor',\n            permissions: {\n              isProfileVisible: { all: true },\n              isAboutVisible: { all: false, mentor: true, student: false },\n              isEducationVisible: { all: false, mentor: false, student: true },\n              isEnglishVisible: { all: false, student: false },\n              isEmailVisible: { all: false, student: true },\n              isTelegramVisible: { all: false, student: false },\n              isSkypeVisible: { all: false, student: true },\n              isPhoneVisible: { all: false, student: false },\n              isContactsNotesVisible: { all: true, student: true },\n              isLinkedInVisible: { all: false, mentor: false, student: false },\n              isPublicFeedbackVisible: { all: false, mentor: true, student: true },\n              isMentorStatsVisible: { all: false, mentor: true, student: true },\n              isStudentStatsVisible: { all: false, student: true },\n            },\n          }),\n        ).toEqual({\n          isProfileVisible: true,\n          isAboutVisible: true,\n          isEducationVisible: false,\n          isEnglishVisible: true,\n          isEmailVisible: true,\n          isTelegramVisible: true,\n          isWhatsAppVisible: true,\n          isSkypeVisible: true,\n          isPhoneVisible: true,\n          isContactsNotesVisible: true,\n          isLinkedInVisible: false,\n          isPublicFeedbackVisible: true,\n          isMentorStatsVisible: true,\n          isStudentStatsVisible: true,\n          isStageInterviewFeedbackVisible: true,\n          isCoreJsFeedbackVisible: true,\n          isConsentsVisible: false,\n          isExpellingReasonVisible: true,\n        });\n      });\n      it('\"role\" is \"student\" and some \"permissions\" set with \"student\" = \"true\"', () => {\n        expect(\n          getPermissions({\n            isProfileOwner: false,\n            isAdmin: false,\n            role: 'student',\n            permissions: {\n              isProfileVisible: { all: true },\n              isAboutVisible: { all: false, mentor: true, student: true },\n              isEducationVisible: { all: false, mentor: false, student: false },\n              isEnglishVisible: { all: false, student: false },\n              isEmailVisible: { all: false, student: false },\n              isTelegramVisible: { all: false, student: true },\n              isSkypeVisible: { all: false, student: true },\n              isPhoneVisible: { all: false, student: false },\n              isContactsNotesVisible: { all: true, student: true },\n              isLinkedInVisible: { all: false, mentor: false, student: false },\n              isPublicFeedbackVisible: { all: false, mentor: true, student: true },\n              isMentorStatsVisible: { all: false, mentor: true, student: true },\n              isStudentStatsVisible: { all: false, student: true },\n            },\n          }),\n        ).toEqual({\n          isProfileVisible: true,\n          isAboutVisible: true,\n          isEducationVisible: false,\n          isEnglishVisible: false,\n          isEmailVisible: false,\n          isTelegramVisible: true,\n          isWhatsAppVisible: true,\n          isSkypeVisible: true,\n          isPhoneVisible: false,\n          isContactsNotesVisible: true,\n          isLinkedInVisible: false,\n          isPublicFeedbackVisible: true,\n          isMentorStatsVisible: true,\n          isStudentStatsVisible: true,\n          isStageInterviewFeedbackVisible: false,\n          isCoreJsFeedbackVisible: false,\n          isConsentsVisible: false,\n          isExpellingReasonVisible: false,\n        });\n      });\n      it('\"role\" is \"coursemanager\" and some \"permissions\" set with \"coursemanager\" = \"true\"', () => {\n        expect(\n          getPermissions({\n            isProfileOwner: false,\n            isAdmin: false,\n            role: 'coursemanager',\n            permissions: {\n              isProfileVisible: { all: true },\n              isAboutVisible: { all: false, mentor: true, student: true },\n              isEducationVisible: { all: false, mentor: false, student: false },\n              isEnglishVisible: { all: false, student: false },\n              isEmailVisible: { all: false, student: false },\n              isTelegramVisible: { all: false, student: true },\n              isSkypeVisible: { all: false, student: true },\n              isPhoneVisible: { all: false, student: false },\n              isContactsNotesVisible: { all: true, student: true },\n              isLinkedInVisible: { all: false, mentor: false, student: false },\n              isPublicFeedbackVisible: { all: false, mentor: true, student: true },\n              isMentorStatsVisible: { all: false, mentor: true, student: true },\n              isStudentStatsVisible: { all: false, student: true },\n            },\n          }),\n        ).toEqual({\n          isProfileVisible: true,\n          isAboutVisible: true,\n          isEducationVisible: true,\n          isEnglishVisible: true,\n          isEmailVisible: true,\n          isTelegramVisible: true,\n          isWhatsAppVisible: true,\n          isSkypeVisible: true,\n          isPhoneVisible: true,\n          isContactsNotesVisible: true,\n          isLinkedInVisible: true,\n          isPublicFeedbackVisible: true,\n          isMentorStatsVisible: true,\n          isStudentStatsVisible: true,\n          isStageInterviewFeedbackVisible: true,\n          isCoreJsFeedbackVisible: true,\n          isConsentsVisible: true,\n          isExpellingReasonVisible: true,\n        });\n      });\n    });\n    describe('if \"isProfileOwner\" is \"true\"', () => {\n      it('\"role\" is \"all\" and all \"permissions\" set with \"all\" = \"false\"', () => {\n        expect(\n          getPermissions({\n            isProfileOwner: true,\n            isAdmin: false,\n            role: 'all',\n            permissions: {\n              isProfileVisible: { all: false },\n              isAboutVisible: { all: false, mentor: false, student: false },\n              isEducationVisible: { all: false, mentor: false, student: false },\n              isEnglishVisible: { all: false, student: false },\n              isEmailVisible: { all: false, student: false },\n              isTelegramVisible: { all: false, student: false },\n              isSkypeVisible: { all: false, student: false },\n              isPhoneVisible: { all: false, student: false },\n              isContactsNotesVisible: { all: false, student: false },\n              isLinkedInVisible: { all: false, mentor: false, student: false },\n              isPublicFeedbackVisible: { all: false, mentor: false, student: false },\n              isMentorStatsVisible: { all: false, mentor: false, student: false },\n              isStudentStatsVisible: { all: false, student: false },\n            },\n          }),\n        ).toEqual({\n          isProfileVisible: true,\n          isAboutVisible: true,\n          isEducationVisible: true,\n          isEnglishVisible: true,\n          isEmailVisible: true,\n          isTelegramVisible: true,\n          isWhatsAppVisible: true,\n          isSkypeVisible: true,\n          isPhoneVisible: true,\n          isContactsNotesVisible: true,\n          isLinkedInVisible: true,\n          isPublicFeedbackVisible: true,\n          isMentorStatsVisible: true,\n          isStudentStatsVisible: true,\n          isStageInterviewFeedbackVisible: false,\n          isCoreJsFeedbackVisible: false,\n          isConsentsVisible: true,\n          isExpellingReasonVisible: false,\n        });\n      });\n    });\n  });\n});\n\ndescribe('defineRole', () => {\n  it('Should be an instance of Function', () => {\n    expect(defineRole).toBeInstanceOf(Function);\n  });\n\n  describe('Should return user role', () => {\n    it('\"student\", if user is a student', () => {\n      expect(\n        defineRole({\n          relationsRoles: {\n            student: 'dima',\n            mentors: ['andrey', 'dasha'],\n            interviewers: ['sasha', 'max'],\n            stageInterviewers: ['alex'],\n            checkers: ['masha', 'ivan'],\n          },\n          registryCourses: null,\n          studentCourses: null,\n          session: mockSession,\n          userGithubId: 'dima',\n        }),\n      ).toBe('student');\n    });\n    it('\"mentor\", if user is an assigned mentor', () => {\n      expect(\n        defineRole({\n          relationsRoles: {\n            student: 'dima',\n            mentors: ['andrey', 'dasha'],\n            interviewers: ['sasha', 'max'],\n            stageInterviewers: ['alex'],\n            checkers: ['masha', 'ivan'],\n          },\n          registryCourses: null,\n          studentCourses: null,\n          session: mockSession,\n          userGithubId: 'andrey',\n        }),\n      ).toBe('mentor');\n    });\n    it('\"mentor\", if user is an interviewer', () => {\n      expect(\n        defineRole({\n          relationsRoles: {\n            student: 'dima',\n            mentors: ['andrey', 'dasha'],\n            interviewers: ['sasha', 'max'],\n            stageInterviewers: ['alex'],\n            checkers: ['masha', 'ivan'],\n          },\n          registryCourses: null,\n          studentCourses: null,\n          session: mockSession,\n          userGithubId: 'max',\n        }),\n      ).toBe('mentor');\n    });\n    it('\"mentor\", if user is a stage-interviewer', () => {\n      expect(\n        defineRole({\n          relationsRoles: {\n            student: 'dima',\n            mentors: ['andrey', 'dasha'],\n            interviewers: ['sasha', 'max'],\n            stageInterviewers: ['alex'],\n            checkers: ['masha', 'ivan'],\n          },\n          registryCourses: null,\n          studentCourses: null,\n          session: mockSession,\n          userGithubId: 'alex',\n        }),\n      ).toBe('mentor');\n    });\n    it('\"mentor\", if user is assigned for checking a task', () => {\n      expect(\n        defineRole({\n          relationsRoles: {\n            student: 'dima',\n            mentors: ['andrey', 'dasha'],\n            interviewers: ['sasha', 'max'],\n            stageInterviewers: ['alex'],\n            checkers: ['masha', 'ivan'],\n          },\n          registryCourses: null,\n          studentCourses: null,\n          session: mockSession,\n          userGithubId: 'masha',\n        }),\n      ).toBe('mentor');\n    });\n    it('\"coursementor\", if user is a mentor at the same course where requested user is a student', () => {\n      expect(\n        defineRole({\n          relationsRoles: null,\n          registryCourses: null,\n          studentCourses: [{ courseId: 1 }, { courseId: 11 }],\n          session: mockSession,\n          userGithubId: 'denis',\n        }),\n      ).toBe('coursementor');\n    });\n    it('\"coursemanager\", if user is mentor waiting confirmation and current user is coursemanager', () => {\n      expect(\n        defineRole({\n          relationsRoles: null,\n          registryCourses: [{ courseId: 1 }],\n          studentCourses: null,\n          session: {\n            courses: { 1: { roles: [CourseRole.Manager] } },\n          } as unknown as IUserSession,\n          userGithubId: 'denis',\n        }),\n      ).toBe('coursemanager');\n    });\n    it('\"all\", if user is not a mentor at the same course where requested user is a student', () => {\n      expect(\n        defineRole({\n          relationsRoles: null,\n          registryCourses: null,\n          studentCourses: [{ courseId: 1 }],\n          session: { ...mockSession, courses: { 1: { roles: [] } } },\n          userGithubId: 'denis',\n        }),\n      ).toBe('all');\n    });\n    it('\"all\", if user if student has not registered to any course', () => {\n      expect(\n        defineRole({\n          relationsRoles: null,\n          registryCourses: null,\n          studentCourses: null,\n          session: mockSession,\n          userGithubId: 'denis',\n        }),\n      ).toBe('all');\n    });\n  });\n});\n\ndescribe('getProfilePermissionsSettings', () => {\n  it('Should be an instance of Function', () => {\n    expect(defineRole).toBeInstanceOf(Function);\n  });\n\n  it('Should not mutate param \"permissions\"', () => {\n    const permissions = {\n      isProfileVisible: { all: true },\n    };\n    const permissionsSettings = getProfilePermissionsSettings(permissions);\n\n    expect(permissions).toEqual({ isProfileVisible: { all: true } });\n    expect(permissionsSettings).not.toEqual({ isProfileVisible: { all: true } });\n  });\n\n  it('Should return permissions settings with defaults if all have not been passed', () => {\n    const permissions = {\n      isProfileVisible: { all: false },\n      isAboutVisible: { all: true, mentor: true, student: true },\n      isEducationVisible: { all: true, mentor: true, student: true },\n    };\n    const permissionsSettings = getProfilePermissionsSettings(permissions);\n\n    expect(permissionsSettings).toEqual({\n      isProfileVisible: { all: false },\n      isAboutVisible: { all: true, mentor: true, student: true },\n      isEducationVisible: { all: true, mentor: true, student: true },\n      isEnglishVisible: { all: false, student: false },\n      isEmailVisible: { all: false, student: true },\n      isTelegramVisible: { all: false, student: true },\n      isSkypeVisible: { all: false, student: true },\n      isPhoneVisible: { all: false, student: true },\n      isContactsNotesVisible: { all: false, student: true },\n      isLinkedInVisible: { all: false, mentor: false, student: false },\n      isPublicFeedbackVisible: { all: false, mentor: false, student: false },\n      isMentorStatsVisible: { all: false, mentor: false, student: false },\n      isStudentStatsVisible: { all: false, student: false },\n    });\n  });\n});\n"
  },
  {
    "path": "server/src/routes/profile/index.ts",
    "content": "import Router from '@koa/router';\nimport { ILogger } from '../../logger';\nimport { guard } from '../guards';\nimport { getProfileInfo } from './info';\nimport { getMyProfile, updateMyProfile } from './me';\n\nexport function profileRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/profile' });\n\n  /**\n   * @swagger\n   *\n   * /profile:\n   *   get:\n   *      description: get user profile\n   *      security:\n   *        - cookieAuth: []\n   *      produces:\n   *        - application/json\n   *      responses:\n   *        200:\n   *          description: profile\n   */\n  router.get('/info', guard, getProfileInfo(logger));\n\n  /**\n   * @swagger\n   *\n   * /profile/me:\n   *   get:\n   *      description: get current user profile\n   *      security:\n   *        - cookieAuth: []\n   *      produces:\n   *        - application/json\n   *      responses:\n   *        200:\n   *          description: profile\n   */\n  router.get('/me', guard, getMyProfile(logger));\n\n  /**\n   * @swagger\n   *\n   * /profile/me:\n   *   get:\n   *      description: update current user profile\n   *      security:\n   *        - cookieAuth: []\n   *      produces:\n   *        - application/json\n   *      responses:\n   *        200:\n   *          description: profile\n   */\n  router.post('/me', guard, updateMyProfile(logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/profile/info.ts",
    "content": "import { NOT_FOUND, OK, FORBIDDEN } from 'http-status-codes';\nimport Router from '@koa/router';\nimport { ILogger } from '../../logger';\nimport { setResponse } from '../utils';\nimport { IUserSession } from '../../models';\nimport { ConfigurableProfilePermissions } from '../../../../common/models/profile';\nimport { getMentorStats } from './mentor-stats';\nimport { getPublicFeedback } from './public-feedback';\nimport { getStageInterviewFeedback } from './stage-interview-feedback';\nimport { getStudentStats } from './student-stats';\nimport { getUserInfo } from './user-info';\nimport {\n  getProfilePermissionsSettings,\n  getConfigurableProfilePermissions,\n  getRelationsRoles,\n  getStudentCourses,\n  getPermissions,\n  getMentorCourses,\n  defineRole,\n  RelationRole,\n  Permissions,\n} from './permissions';\n\nexport const getProfileInfo = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const session = ctx.state!.user as IUserSession;\n  const { githubId: userGithubId, isAdmin } = ctx.state!.user as IUserSession;\n  const { githubId: requestedGithubId = userGithubId } = ctx.query as { githubId: string | undefined };\n\n  if (!requestedGithubId) {\n    return setResponse(ctx, NOT_FOUND);\n  }\n\n  const isProfileOwner = requestedGithubId === userGithubId;\n\n  const profilePermissions = await getConfigurableProfilePermissions(requestedGithubId);\n\n  let role: RelationRole;\n  let permissions: Permissions;\n  let permissionsSettings: ConfigurableProfilePermissions | undefined;\n  if (isProfileOwner) {\n    permissions = getPermissions({ isProfileOwner, isAdmin });\n    permissionsSettings = getProfilePermissionsSettings(profilePermissions);\n  } else {\n    const relationsRoles = await getRelationsRoles(userGithubId, requestedGithubId);\n    const [studentCourses, registryCourses] = !relationsRoles\n      ? await Promise.all([getStudentCourses(requestedGithubId), getMentorCourses(requestedGithubId)])\n      : [null, null];\n    role = defineRole({ relationsRoles, studentCourses, registryCourses, session, userGithubId });\n    permissions = getPermissions({ isAdmin, isProfileOwner, role, permissions: profilePermissions });\n  }\n\n  const {\n    isProfileVisible,\n    isPublicFeedbackVisible,\n    isMentorStatsVisible,\n    isStudentStatsVisible,\n    isStageInterviewFeedbackVisible,\n  } = permissions;\n\n  if (!isProfileVisible && !isProfileOwner) {\n    return setResponse(ctx, FORBIDDEN);\n  }\n\n  const { generalInfo, contacts, discord } = await getUserInfo(requestedGithubId, permissions);\n  const publicFeedback = isPublicFeedbackVisible ? await getPublicFeedback(requestedGithubId) : undefined;\n  const mentorStats = isMentorStatsVisible ? await getMentorStats(requestedGithubId) : undefined;\n  const studentStats = isStudentStatsVisible ? await getStudentStats(requestedGithubId, permissions) : undefined;\n  const stageInterviewFeedback = isStageInterviewFeedbackVisible\n    ? await getStageInterviewFeedback(requestedGithubId)\n    : undefined;\n\n  const profileInfo = {\n    permissionsSettings,\n    generalInfo,\n    contacts,\n    discord,\n    mentorStats,\n    publicFeedback,\n    stageInterviewFeedback,\n    studentStats,\n  };\n\n  setResponse(ctx, OK, profileInfo);\n};\n"
  },
  {
    "path": "server/src/routes/profile/me.ts",
    "content": "import { BAD_REQUEST, OK, NOT_FOUND } from 'http-status-codes';\nimport Router from '@koa/router';\nimport { getRepository } from 'typeorm';\nimport { ILogger } from '../../logger';\nimport { User } from '../../models';\nimport { IUserSession } from '../../models/session';\nimport { setResponse } from '../utils';\n\nexport const getMyProfile = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId } = ctx.state!.user as IUserSession;\n\n  const profile = await getRepository(User).findOne({ where: { githubId } });\n  if (profile === undefined) {\n    setResponse(ctx, NOT_FOUND);\n    return;\n  }\n\n  setResponse(ctx, OK, profile);\n};\n\nexport const updateMyProfile = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const { githubId } = ctx.state!.user as IUserSession;\n  const inputData: Partial<User> = ctx.request.body;\n  if (!inputData) {\n    setResponse(ctx, BAD_REQUEST);\n    return;\n  }\n  const userRepository = getRepository(User);\n  const user = await userRepository.findOne({ where: { githubId } });\n  if (!user) {\n    setResponse(ctx, BAD_REQUEST);\n    return;\n  }\n  // remove immutable fields from the payload\n  const { id, githubId: gId, createdDate, updatedDate, ...data } = inputData;\n  const result = await userRepository.save({ ...user, ...data });\n  setResponse(ctx, OK, result);\n};\n"
  },
  {
    "path": "server/src/routes/profile/mentor-stats.ts",
    "content": "import { getRepository } from 'typeorm';\nimport { MentorStats } from '../../../../common/models/profile';\nimport { getFullName } from '../../rules';\nimport { User, Mentor, Student, Course } from '../../models';\nimport { RepositoryService } from '../../services';\n\nexport const getMentorStats = async (githubId: string): Promise<MentorStats[]> => {\n  const rawData = await getRepository(Mentor)\n    .createQueryBuilder('mentor')\n    .select('\"course\".\"name\" AS \"courseName\"')\n    .addSelect('\"course\".\"alias\" AS \"courseAlias\"')\n    .addSelect('\"course\".\"id\" AS \"courseId\"')\n    .addSelect('\"course\".\"locationName\" AS \"courseLocationName\"')\n    .addSelect('ARRAY_AGG (\"userStudent\".\"githubId\") AS \"studentGithubIds\"')\n    .addSelect('ARRAY_AGG (\"userStudent\".\"firstName\") AS \"studentFirstNames\"')\n    .addSelect('ARRAY_AGG (\"userStudent\".\"lastName\") AS \"studentLastNames\"')\n    .addSelect('ARRAY_AGG (\"student\".\"isExpelled\") AS \"studentIsExpelledStatuses\"')\n    .addSelect('ARRAY_AGG (\"student\".\"totalScore\") AS \"studentTotalScores\"')\n    .leftJoin(User, 'user', '\"user\".\"id\" = \"mentor\".\"userId\"')\n    .leftJoin(Course, 'course', '\"course\".\"id\" = \"mentor\".\"courseId\"')\n    .leftJoin(Student, 'student', '\"student\".\"mentorId\" = \"mentor\".\"id\"')\n    .leftJoin(User, 'userStudent', '\"userStudent\".\"id\" = \"student\".\"userId\"')\n    .where('\"user\".\"githubId\" = :githubId', { githubId })\n    .groupBy('\"course\".\"id\"')\n    .orderBy('\"course\".\"endDate\"', 'DESC')\n    .getRawMany();\n  return rawData.map(\n    ({\n      courseName,\n      courseAlias,\n      courseLocationName,\n      studentGithubIds,\n      studentFirstNames,\n      studentLastNames,\n      studentIsExpelledStatuses,\n      studentTotalScores,\n    }: any) => {\n      const students = studentGithubIds[0]\n        ? studentGithubIds.map((githubId: string, idx: number) => ({\n            githubId,\n            name: getFullName(studentFirstNames[idx], studentLastNames[idx], githubId),\n            isExpelled: studentIsExpelledStatuses[idx],\n            totalScore: studentTotalScores[idx],\n            repoUrl: `https://github.com/rolling-scopes-school/${RepositoryService.getRepoName(githubId, {\n              alias: courseAlias,\n            })}`,\n          }))\n        : undefined;\n      return { courseLocationName, courseName, students };\n    },\n  );\n};\n"
  },
  {
    "path": "server/src/routes/profile/permissions.ts",
    "content": "import get from 'lodash/get';\nimport mapValues from 'lodash/mapValues';\nimport mergeWith from 'lodash/mergeWith';\nimport cloneDeep from 'lodash/cloneDeep';\nimport uniqBy from 'lodash/uniqBy';\nimport { In, getRepository } from 'typeorm';\nimport {\n  User,\n  Student,\n  Mentor,\n  ProfilePermissions,\n  TaskChecker,\n  TaskInterviewResult,\n  StageInterview,\n  isManager,\n  IUserSession,\n  isSupervisor,\n  MentorRegistry,\n  Discipline,\n  Course,\n} from '../../models';\nimport { defaultProfilePermissionsSettings } from '../../models/profilePermissions';\nimport { ConfigurableProfilePermissions } from '../../../../common/models/profile';\n\ninterface Relations {\n  student: string;\n  mentors: string[];\n  interviewers: string[];\n  stageInterviewers: string[];\n  checkers: string[];\n}\n\nexport type RelationRole = 'student' | 'mentor' | 'coursementor' | 'coursesupervisor' | 'coursemanager' | 'all';\n\ninterface PermissionsSetup {\n  isProfileOwner: boolean;\n  isAdmin: boolean;\n  role?: RelationRole;\n  permissions?: ConfigurableProfilePermissions;\n}\n\nexport interface Permissions {\n  isProfileVisible: boolean;\n  isAboutVisible: boolean;\n  isEducationVisible: boolean;\n  isEnglishVisible: boolean;\n  isEmailVisible: boolean;\n  isTelegramVisible: boolean;\n  isSkypeVisible: boolean;\n  isWhatsAppVisible: boolean;\n  isPhoneVisible: boolean;\n  isContactsNotesVisible: boolean;\n  isLinkedInVisible: boolean;\n  isPublicFeedbackVisible: boolean;\n  isMentorStatsVisible: boolean;\n  isStudentStatsVisible: boolean;\n  isStageInterviewFeedbackVisible: boolean;\n  isCoreJsFeedbackVisible: boolean;\n  isConsentsVisible: boolean;\n  isExpellingReasonVisible: boolean;\n}\n\nexport const getStudentCourses = async (githubId: string): Promise<{ courseId: number }[] | null> => {\n  const result = await getRepository(User)\n    .createQueryBuilder('user')\n    .select('\"student\".\"courseId\" AS \"courseId\"')\n    .leftJoin(Student, 'student', '\"student\".\"userId\" = \"user\".\"id\"')\n    .where('\"user\".\"githubId\" = :githubId', { githubId })\n    .getRawMany();\n  return result ?? null;\n};\n\nexport const getMentorCourses = async (githubId: string): Promise<{ courseId: number }[] | null> => {\n  const [registerdCourseIds, registryCourseIds] = await Promise.all([\n    getRegisteredMentorsCourseIds(githubId),\n    getMentorsFromRegistryCourseIds(githubId),\n  ]);\n\n  const mentorsCourses = registerdCourseIds.concat(registryCourseIds);\n\n  return mentorsCourses.length ? mentorsCourses : null;\n};\n\nconst getRegisteredMentorsCourseIds = async (githubId: string) => {\n  const result: { courseId: number }[] = await getRepository(Mentor)\n    .createQueryBuilder('mentor')\n    .select(['mentor.courseId'])\n    .leftJoin('mentor.user', 'user')\n    .where('user.githubId = :githubId', { githubId })\n    .getMany();\n\n  return result.length ? result : [];\n};\n\nconst getMentorsFromRegistryCourseIds = async (githubId: string) => {\n  const result = await getRepository(MentorRegistry)\n    .createQueryBuilder('mentorRegistry')\n    .select(['mentorRegistry.preferedCourses', 'mentorRegistry.technicalMentoring'])\n    .leftJoin('mentorRegistry.user', 'user')\n    .where('user.githubId = :githubId', { githubId })\n    .andWhere('\"mentorRegistry\".canceled = false')\n    .getOne();\n\n  const disciplines = await getRepository(Discipline).find({ where: { name: In(result?.technicalMentoring ?? []) } });\n  const disciplinesIds = disciplines.map(({ id }) => id);\n  const coursesByDisciplines = await getRepository(Course).find({ where: { disciplineId: In(disciplinesIds) } });\n\n  const preferredCourseIds = result?.preferedCourses?.map(courseId => ({ courseId: Number(courseId) })) ?? [];\n  const courseIdsByDisciplines = coursesByDisciplines.map(({ id }) => ({ courseId: id }));\n\n  const courseIds = uniqBy(preferredCourseIds.concat(courseIdsByDisciplines), ({ courseId }) => courseId);\n\n  return courseIds;\n};\n\nexport const getConfigurableProfilePermissions = async (githubId: string): Promise<ConfigurableProfilePermissions> =>\n  (await getRepository(ProfilePermissions)\n    .createQueryBuilder('pp')\n    .select('\"pp\".\"isProfileVisible\" AS \"isProfileVisible\"')\n    .addSelect('\"pp\".\"isAboutVisible\" AS \"isAboutVisible\"')\n    .addSelect('\"pp\".\"isEducationVisible\" AS \"isEducationVisible\"')\n    .addSelect('\"pp\".\"isEnglishVisible\" AS \"isEnglishVisible\"')\n    .addSelect('\"pp\".\"isEmailVisible\" AS \"isEmailVisible\"')\n    .addSelect('\"pp\".\"isTelegramVisible\" AS \"isTelegramVisible\"')\n    .addSelect('\"pp\".\"isSkypeVisible\" AS \"isSkypeVisible\"')\n    .addSelect('\"pp\".\"isPhoneVisible\" AS \"isPhoneVisible\"')\n    .addSelect('\"pp\".\"isContactsNotesVisible\" AS \"isContactsNotesVisible\"')\n    .addSelect('\"pp\".\"isLinkedInVisible\" AS \"isLinkedInVisible\"')\n    .addSelect('\"pp\".\"isPublicFeedbackVisible\" AS \"isPublicFeedbackVisible\"')\n    .addSelect('\"pp\".\"isMentorStatsVisible\" AS \"isMentorStatsVisible\"')\n    .addSelect('\"pp\".\"isStudentStatsVisible\" AS \"isStudentStatsVisible\"')\n    .leftJoin(User, 'user', '\"user\".\"id\" = \"pp\".\"userId\"')\n    .where('\"user\".\"githubId\" = :githubId', { githubId })\n    .getRawOne()) || {};\n\nexport const getRelationsRoles = async (userGithubId: string, requestedGithubId: string): Promise<Relations | null> =>\n  (await getRepository(Student)\n    .createQueryBuilder('student')\n    .select('\"userStudent\".\"githubId\" AS \"student\"')\n    .addSelect('ARRAY_AGG(\"userMentor\".\"githubId\") as \"mentors\"')\n    .addSelect('ARRAY_AGG(\"userInterviewer\".\"githubId\") as \"interviewers\"')\n    .addSelect('ARRAY_AGG(\"userStageInterviewer\".\"githubId\") as \"stageInterviewers\"')\n    .addSelect('ARRAY_AGG(\"userChecker\".\"githubId\") as \"checkers\"')\n    .leftJoin(User, 'userStudent', '\"student\".\"userId\" = \"userStudent\".\"id\"')\n    .leftJoin(Mentor, 'mentor', '\"mentor\".\"id\" = \"student\".\"mentorId\"')\n    .leftJoin(User, 'userMentor', '\"mentor\".\"userId\" = \"userMentor\".\"id\"')\n    .leftJoin(TaskChecker, 'taskChecker', '\"student\".\"id\" = \"taskChecker\".\"studentId\"')\n    .leftJoin(Mentor, 'mentorChecker', '\"mentorChecker\".\"id\" = \"taskChecker\".\"mentorId\"')\n    .leftJoin(User, 'userChecker', '\"mentorChecker\".\"userId\" = \"userChecker\".\"id\"')\n    .leftJoin(TaskInterviewResult, 'taskInterviewResult', '\"student\".\"id\" = \"taskInterviewResult\".\"studentId\"')\n    .leftJoin(Mentor, 'mentorInterviewer', '\"mentorInterviewer\".\"id\" = \"taskInterviewResult\".\"mentorId\"')\n    .leftJoin(User, 'userInterviewer', '\"mentorInterviewer\".\"userId\" = \"userInterviewer\".\"id\"')\n    .leftJoin(StageInterview, 'stageInterview', '\"student\".\"id\" = \"stageInterview\".\"studentId\"')\n    .leftJoin(Mentor, 'mentorStageInterviewer', '\"mentorStageInterviewer\".\"id\" = \"stageInterview\".\"mentorId\"')\n    .leftJoin(User, 'userStageInterviewer', '\"mentorStageInterviewer\".\"userId\" = \"userStageInterviewer\".\"id\"')\n    .where(\n      `\"userStudent\".\"githubId\" = :userGithubId AND\n      (\"userMentor\".\"githubId\" = :requestedGithubId OR\n      \"userStageInterviewer\".\"githubId\" = :requestedGithubId OR\n      \"userInterviewer\".\"githubId\" = :requestedGithubId OR\n      \"userChecker\".\"githubId\" = :requestedGithubId )`,\n      { userGithubId, requestedGithubId },\n    )\n    .orWhere(\n      `\"userStudent\".\"githubId\" = :requestedGithubId AND\n      (\"userMentor\".\"githubId\" = :userGithubId OR\n      \"userStageInterviewer\".\"githubId\" = :userGithubId OR\n      \"userInterviewer\".\"githubId\" = :userGithubId OR\n      \"userChecker\".\"githubId\" = :userGithubId)`,\n    )\n    .groupBy('\"userStudent\".\"githubId\"')\n    .getRawOne()) || null;\n\nexport const defineRole = ({\n  relationsRoles,\n  studentCourses,\n  registryCourses,\n  session,\n  userGithubId,\n}: {\n  relationsRoles: Relations | null;\n  registryCourses: { courseId: number }[] | null;\n  studentCourses: { courseId: number }[] | null;\n  session: IUserSession;\n  userGithubId: string;\n}): RelationRole => {\n  if (registryCourses?.some(({ courseId }) => isManager(session, courseId))) {\n    return 'coursemanager';\n  } else if (registryCourses?.some(({ courseId }) => isSupervisor(session, courseId))) {\n    return 'coursesupervisor';\n  } else if (studentCourses?.some(({ courseId }) => isManager(session, courseId))) {\n    return 'coursemanager';\n  } else if (studentCourses?.some(({ courseId }) => isSupervisor(session, courseId))) {\n    return 'coursemanager';\n  } else if (relationsRoles) {\n    const { student, mentors, interviewers, stageInterviewers, checkers } = relationsRoles;\n\n    if (student === userGithubId) {\n      return 'student';\n    } else if (new Set([...mentors, ...interviewers, ...stageInterviewers, ...checkers]).has(userGithubId)) {\n      return 'mentor';\n    }\n  } else if (studentCourses?.some(({ courseId }) => !!session?.courses?.[courseId]?.mentorId)) {\n    return 'coursementor';\n  }\n\n  return 'all';\n};\n\nexport const getPermissions = ({ isAdmin, isProfileOwner, role, permissions }: PermissionsSetup): Permissions => {\n  const defaultPermissions: Permissions = {\n    isProfileVisible: false,\n    isAboutVisible: false,\n    isEducationVisible: false,\n    isEnglishVisible: false,\n    isEmailVisible: false,\n    isTelegramVisible: false,\n    isSkypeVisible: false,\n    isWhatsAppVisible: false,\n    isPhoneVisible: false,\n    isContactsNotesVisible: false,\n    isLinkedInVisible: false,\n    isPublicFeedbackVisible: false,\n    isMentorStatsVisible: false,\n    isStudentStatsVisible: false,\n    isStageInterviewFeedbackVisible: false,\n    isCoreJsFeedbackVisible: false,\n    isConsentsVisible: false,\n    isExpellingReasonVisible: false,\n  };\n\n  const accessToContacts = (permission: string, role?: RelationRole) => {\n    return (\n      [\n        'isEmailVisible',\n        'isTelegramVisible',\n        'isSkypeVisible',\n        'isPhoneVisible',\n        'isWhatsAppVisible',\n        'isContactsNodesVisible',\n        'isEnglishVisible',\n      ].includes(permission) &&\n      role &&\n      ['mentor', 'coursemanager', 'coursesupervisor'].includes(role)\n    );\n  };\n\n  const defaultAccessToContacts = (permission: string, role?: RelationRole) => {\n    return (\n      [\n        'isEmailVisible',\n        'isWhatsAppVisible',\n        'isTelegramVisible',\n        'isSkypeVisible',\n        'isPhoneVisible',\n        'isContactsNodesVisible',\n      ].includes(permission) &&\n      role &&\n      ['student'].includes(role)\n    );\n  };\n\n  const accessToFeedbacks = (permission: string, role?: RelationRole) => {\n    return (\n      [\n        'isStageInterviewFeedbackVisible',\n        'isStudentStatsVisible',\n        'isCoreJsFeedbackVisible',\n        'isProfileVisible',\n        'isExpellingReasonVisible',\n      ].includes(permission) &&\n      role &&\n      ['mentor', 'coursementor', 'coursemanager'].includes(role)\n    );\n  };\n\n  const accessToProfile = (permission: string, role?: RelationRole) =>\n    ['isProfileVisible'].includes(permission) && role && ['student'].includes(role);\n\n  return mapValues(defaultPermissions, (_, permission) => {\n    if (isAdmin || role === 'coursemanager') {\n      return true;\n    }\n    if (role === 'coursesupervisor' && permission === 'isProfileVisible') {\n      return true;\n    }\n    if (accessToFeedbacks(permission, role)) {\n      return true;\n    }\n    if (accessToContacts(permission, role)) {\n      return true;\n    }\n    if (accessToProfile(permission, role)) {\n      return true;\n    }\n    // do not show own feedbacks\n    if (\n      isProfileOwner &&\n      !['isStageInterviewFeedbackVisible', 'isCoreJsFeedbackVisible', 'isExpellingReasonVisible'].includes(permission)\n    ) {\n      return true;\n    }\n    if (get(permissions, `${permission}.all`) || get(permissions, `${permission}.${role}`)) {\n      return true;\n    }\n    // show mentor contacts to students by default\n    if (get(permissions, `${permission}.student`) === undefined && defaultAccessToContacts(permission, role)) {\n      return true;\n    }\n    return false;\n  });\n};\n\nexport const getProfilePermissionsSettings = (permissions: ConfigurableProfilePermissions) => {\n  const newPermissions = cloneDeep(permissions);\n\n  mergeWith(newPermissions, defaultProfilePermissionsSettings, (setting, defaultSetting) =>\n    mapValues(defaultSetting, (value, key) => get(setting, key, value)),\n  );\n\n  return newPermissions;\n};\n"
  },
  {
    "path": "server/src/routes/profile/public-feedback.ts",
    "content": "import { getRepository } from 'typeorm';\nimport { PublicFeedback } from '../../../../common/models/profile';\nimport { getFullName } from '../../rules';\nimport { User, Feedback } from '../../models';\n\nexport const getPublicFeedback = async (githubId: string): Promise<PublicFeedback[]> =>\n  (\n    await getRepository(Feedback)\n      .createQueryBuilder('feedback')\n      .select('\"feedback\".\"updatedDate\" AS \"feedbackDate\"')\n      .addSelect('\"feedback\".\"badgeId\" AS \"badgeId\"')\n      .addSelect('\"feedback\".\"comment\" AS \"comment\"')\n      .addSelect('\"fromUser\".\"firstName\" AS \"fromUserFirstName\", \"fromUser\".\"lastName\" AS \"fromUserLastName\"')\n      .addSelect('\"fromUser\".\"githubId\" AS \"fromUserGithubId\"')\n      .leftJoin(User, 'user', '\"user\".\"id\" = \"feedback\".\"toUserId\"')\n      .leftJoin(User, 'fromUser', '\"fromUser\".\"id\" = \"feedback\".\"fromUserId\"')\n      .where('\"user\".\"githubId\" = :githubId', { githubId })\n      .orderBy('\"feedback\".\"updatedDate\"', 'DESC')\n      .getRawMany()\n  ).map(({ feedbackDate, badgeId, comment, fromUserFirstName, fromUserLastName, fromUserGithubId }: any) => ({\n    feedbackDate,\n    badgeId,\n    comment,\n    fromUser: {\n      name: getFullName(fromUserFirstName, fromUserLastName, fromUserGithubId),\n      githubId: fromUserGithubId,\n    },\n  }));\n"
  },
  {
    "path": "server/src/routes/profile/stage-interview-feedback.ts",
    "content": "import { getRepository } from 'typeorm';\nimport { StageInterviewDetailedFeedback } from '../../../../common/models/profile';\nimport { getFullName } from '../../rules';\nimport { User, Mentor, Student, Course, StageInterview, StageInterviewFeedback, CourseTask } from '../../models';\nimport { stageInterviewService } from '../../services';\nimport { StageInterviewFeedbackJson } from '../../../../common/models';\n\ntype FeedbackData = {\n  decision: string;\n  isGoodCandidate: boolean;\n  courseName: string;\n  courseFullName: string;\n  interviewResultJson: any;\n  interviewFeedbackDate: string;\n  interviewerFirstName: string;\n  interviewerLastName: string;\n  interviewerGithubId: string;\n  feedbackVersion: null | number;\n  interviewScore: null | number;\n  maxScore: number;\n};\n\nexport const getStageInterviewFeedback = async (githubId: string): Promise<StageInterviewDetailedFeedback[]> => {\n  const data = await getRepository(StageInterview)\n    .createQueryBuilder('stageInterview')\n    .select('\"stageInterview\".\"decision\" AS \"decision\"')\n    .addSelect('\"stageInterview\".\"isGoodCandidate\" AS \"isGoodCandidate\"')\n    .addSelect('\"stageInterview\".\"score\" AS \"interviewScore\"')\n    .addSelect('\"course\".\"name\" AS \"courseName\"')\n    .addSelect('\"course\".\"fullName\" AS \"courseFullName\"')\n    .addSelect('\"stageInterviewFeedback\".\"json\" AS \"interviewResultJson\"')\n    .addSelect('\"stageInterviewFeedback\".\"updatedDate\" AS \"interviewFeedbackDate\"')\n    .addSelect('\"stageInterviewFeedback\".\"version\" AS \"feedbackVersion\"')\n    .addSelect('\"userMentor\".\"firstName\" AS \"interviewerFirstName\"')\n    .addSelect('\"userMentor\".\"lastName\" AS \"interviewerLastName\"')\n    .addSelect('\"userMentor\".\"githubId\" AS \"interviewerGithubId\"')\n    .addSelect('\"courseTask\".\"maxScore\" AS \"maxScore\"')\n    .leftJoin(Student, 'student', '\"student\".\"id\" = \"stageInterview\".\"studentId\"')\n    .leftJoin(User, 'user', '\"user\".\"id\" = \"student\".\"userId\"')\n    .leftJoin(Course, 'course', '\"course\".\"id\" = \"stageInterview\".\"courseId\"')\n    .leftJoin(\n      StageInterviewFeedback,\n      'stageInterviewFeedback',\n      '\"stageInterview\".\"id\" = \"stageInterviewFeedback\".\"stageInterviewId\"',\n    )\n    .leftJoin(CourseTask, 'courseTask', '\"courseTask\".\"id\" = \"stageInterview\".\"courseTaskId\"')\n    .leftJoin(Mentor, 'mentor', '\"mentor\".\"id\" = \"stageInterview\".\"mentorId\"')\n    .leftJoin(User, 'userMentor', '\"userMentor\".\"id\" = \"mentor\".\"userId\"')\n    .where('\"user\".\"githubId\" = :githubId', { githubId })\n    .andWhere('\"stageInterview\".\"isCompleted\" = true')\n    .orderBy('\"course\".\"updatedDate\"', 'ASC')\n    .getRawMany();\n\n  return data\n    .map((data: FeedbackData) => {\n      const {\n        feedbackVersion,\n        decision,\n        interviewFeedbackDate,\n        interviewerFirstName,\n        courseFullName,\n        courseName,\n        interviewerLastName,\n        interviewerGithubId,\n        isGoodCandidate,\n        interviewScore,\n        interviewResultJson,\n        maxScore,\n      } = data;\n      const feedbackTemplate = JSON.parse(interviewResultJson) as any;\n\n      const { score, feedback } = !feedbackVersion\n        ? parseLegacyFeedback(feedbackTemplate)\n        : {\n            feedback: feedbackTemplate,\n            score: interviewScore ?? 0,\n          };\n\n      return {\n        version: feedbackVersion ?? 0,\n        date: interviewFeedbackDate,\n        decision,\n        isGoodCandidate,\n        courseName,\n        courseFullName,\n        feedback,\n        score,\n        interviewer: {\n          name: getFullName(interviewerFirstName, interviewerLastName, interviewerGithubId),\n          githubId: interviewerGithubId,\n        },\n        maxScore,\n      };\n    })\n    .filter(Boolean);\n};\n\n// this is legacy form\nfunction parseLegacyFeedback(interviewResult: StageInterviewFeedbackJson) {\n  const { english, programmingTask, resume } = interviewResult;\n  const { rating, htmlCss, common, dataStructures } = stageInterviewService.getInterviewRatings(interviewResult);\n\n  return {\n    score: rating,\n    feedback: {\n      english: english.levelMentorOpinion ? english.levelMentorOpinion : english.levelStudentOpinion,\n      programmingTask,\n      comment: resume.comment,\n      skills: {\n        htmlCss,\n        common,\n        dataStructures,\n      },\n    },\n  };\n}\n"
  },
  {
    "path": "server/src/routes/profile/student-stats.ts",
    "content": "import { getRepository } from 'typeorm';\nimport { StudentStats } from '../../../../common/models/profile';\nimport { getFullName } from '../../rules';\nimport {\n  User,\n  Mentor,\n  Student,\n  Course,\n  Task,\n  CourseTask,\n  TaskResult,\n  TaskInterviewResult,\n  Certificate,\n  StageInterview,\n  StageInterviewFeedback,\n} from '../../models';\nimport { Permissions } from './permissions';\nimport omit from 'lodash/omit';\n\n// use this as a mark for identifying self-expelled students.\nconst SELF_EXPELLED_MARK = 'Self expelled from the course';\n\nconst getStudentStatsWithPosition = async (githubId: string, permissions: Permissions): Promise<StudentStats[]> => {\n  const { isCoreJsFeedbackVisible, isExpellingReasonVisible } = permissions;\n\n  const query = await getRepository(Student)\n    .createQueryBuilder('student')\n    .select('\"course\".\"id\" AS \"courseId\"')\n    .addSelect('\"course\".\"name\" AS \"courseName\"')\n    .addSelect('\"course\".\"locationName\" AS \"locationName\"')\n    .addSelect('\"course\".\"fullName\" AS \"courseFullName\"')\n    .addSelect('\"course\".\"completed\" AS \"isCourseCompleted\"')\n    .addSelect('\"student\".\"isExpelled\" AS \"isExpelled\"')\n    .addSelect('\"student\".\"totalScore\" AS \"totalScore\"')\n    .addSelect('\"student\".\"rank\" AS \"rank\"')\n    .addSelect('\"userMentor\".\"firstName\" AS \"mentorFirstName\"')\n    .addSelect('\"userMentor\".\"lastName\" AS \"mentorLastName\"')\n    .addSelect('\"userMentor\".\"githubId\" AS \"mentorGithubId\"')\n    .addSelect('\"certificate\".\"publicId\" AS \"certificateId\"')\n    .addSelect('ARRAY_AGG (\"courseTask\".\"maxScore\") AS \"taskMaxScores\"')\n    .addSelect('ARRAY_AGG (\"courseTask\".\"scoreWeight\") AS \"taskScoreWeights\"')\n    .addSelect('ARRAY_AGG (\"courseTask\".\"studentEndDate\") AS \"taskEndDates\"')\n    .addSelect('ARRAY_AGG (\"task\".\"name\") AS \"taskNames\"')\n    .addSelect('ARRAY_AGG (\"task\".\"descriptionUrl\") AS \"taskDescriptionUris\"')\n    .addSelect('ARRAY_AGG (\"taskResult\".\"githubPrUrl\") AS \"taskGithubPrUris\"').addSelect(`ARRAY_AGG (COALESCE(\n      \"taskResult\".\"score\",\n      \"taskInterview\".\"score\",\n      (\"stageInterviewFeedback\".\"json\"::json -> 'steps' -> 'decision' -> 'values' ->> 'finalScore')::int\n    )) AS \"taskScores\"`);\n\n  query.addSelect('\"student\".\"expellingReason\" AS \"expellingReason\"');\n\n  if (isCoreJsFeedbackVisible) {\n    query\n      .addSelect('ARRAY_AGG (COALESCE(\"taskResult\".\"comment\", \"taskInterview\".\"comment\")) AS \"taskComments\"')\n      .addSelect('ARRAY_AGG (\"taskInterview\".\"formAnswers\") AS \"taskInterviewFormAnswers\"')\n      .addSelect('ARRAY_AGG (\"taskInterview\".\"createdDate\") AS \"taskInterviewDate\"')\n      .addSelect('ARRAY_AGG (\"interviewer\".\"githubId\") AS \"interviewerGithubId\"')\n      .addSelect('ARRAY_AGG (\"interviewer\".\"firstName\") AS \"interviewerFirstName\"')\n      .addSelect('ARRAY_AGG (\"interviewer\".\"lastName\") AS \"interviewerLastName\"');\n  } else {\n    query.addSelect('ARRAY_AGG (\"taskResult\".\"comment\") AS \"taskComments\"');\n  }\n\n  query\n    .leftJoin(User, 'user', '\"user\".\"id\" = \"student\".\"userId\"')\n    .leftJoin(Certificate, 'certificate', '\"certificate\".\"studentId\" = \"student\".\"id\"')\n    .leftJoin(Course, 'course', '\"course\".\"id\" = \"student\".\"courseId\"')\n    .leftJoin(Mentor, 'mentor', '\"mentor\".\"id\" = \"student\".\"mentorId\"')\n    .leftJoin(User, 'userMentor', '\"userMentor\".\"id\" = \"mentor\".\"userId\"')\n    .leftJoin(CourseTask, 'courseTask', '\"courseTask\".\"courseId\" = \"student\".\"courseId\"')\n    .leftJoin(Task, 'task', '\"courseTask\".\"taskId\" = \"task\".\"id\"')\n    .leftJoin(\n      TaskResult,\n      'taskResult',\n      '\"taskResult\".\"studentId\" = \"student\".\"id\" AND \"taskResult\".\"courseTaskId\" = \"courseTask\".\"id\"',\n    )\n    .leftJoin(\n      TaskInterviewResult,\n      'taskInterview',\n      '\"taskInterview\".\"studentId\" = \"student\".\"id\" AND \"taskInterview\".\"courseTaskId\" = \"courseTask\".\"id\"',\n    )\n    .leftJoin(\n      StageInterview,\n      'stageInterview',\n      '\"stageInterview\".\"studentId\" = \"student\".\"id\" AND \"stageInterview\".\"courseTaskId\" = \"courseTask\".\"id\"',\n    )\n    .leftJoin(\n      StageInterviewFeedback,\n      'stageInterviewFeedback',\n      '\"stageInterviewFeedback\".\"stageInterviewId\" = \"stageInterview\".\"id\"',\n    );\n\n  if (isCoreJsFeedbackVisible) {\n    query\n      .leftJoin(Mentor, 'mentorInterviewer', '\"mentorInterviewer\".\"id\" = \"taskInterview\".\"mentorId\"')\n      .leftJoin(User, 'interviewer', '\"interviewer\".\"id\" = \"mentorInterviewer\".\"userId\"');\n  }\n\n  query\n    .where('\"user\".\"githubId\" = :githubId', { githubId })\n    .andWhere('courseTask.disabled = :disabled', { disabled: false })\n    .groupBy('\"course\".\"id\", \"student\".\"id\", \"userMentor\".\"id\", \"certificate\".\"publicId\"')\n    .orderBy('\"course\".\"endDate\"', 'DESC');\n\n  const rawStats = await query.getRawMany();\n\n  return rawStats.map(\n    ({\n      courseId,\n      courseName,\n      locationName,\n      courseFullName,\n      isExpelled,\n      expellingReason,\n      isCourseCompleted,\n      totalScore,\n      mentorFirstName,\n      mentorLastName,\n      mentorGithubId,\n      taskMaxScores,\n      taskScoreWeights,\n      taskEndDates,\n      taskNames,\n      taskDescriptionUris,\n      taskGithubPrUris,\n      taskScores,\n      taskComments,\n      taskInterviewFormAnswers,\n      taskInterviewDate,\n      interviewerGithubId,\n      interviewerFirstName,\n      interviewerLastName,\n      certificateId,\n      rank,\n    }: any) => {\n      const tasksWithDates = taskMaxScores.map((maxScore: number, idx: number) => ({\n        maxScore,\n        endDate: new Date(taskEndDates[idx]).getTime(),\n        scoreWeight: taskScoreWeights[idx],\n        name: taskNames[idx],\n        descriptionUri: taskDescriptionUris[idx],\n        githubPrUri: taskGithubPrUris[idx],\n        score: taskScores[idx],\n        comment: taskComments[idx],\n        interviewFormAnswers: (taskInterviewFormAnswers && taskInterviewFormAnswers[idx]) || undefined,\n        interviewDate: taskInterviewDate && taskInterviewDate[idx] ? String(taskInterviewDate[idx]) : undefined,\n        interviewer:\n          interviewerGithubId && interviewerGithubId[idx]\n            ? {\n                name: getFullName(interviewerFirstName[idx], interviewerLastName[idx], interviewerGithubId[idx]),\n                githubId: interviewerGithubId[idx],\n              }\n            : undefined,\n      }));\n      const orderedTasks = tasksWithDates\n        .sort((a: any, b: any) => a.endDate - b.endDate)\n        .map((task: any) => omit(task, 'endDate'));\n      return {\n        courseId,\n        courseName,\n        locationName,\n        courseFullName,\n        isExpelled,\n        expellingReason: isExpellingReasonVisible ? expellingReason : undefined,\n        isSelfExpelled: (expellingReason as string)?.startsWith(SELF_EXPELLED_MARK),\n        isCourseCompleted,\n        totalScore,\n        tasks: orderedTasks,\n        certificateId,\n        rank,\n        mentor: {\n          githubId: mentorGithubId,\n          name: getFullName(mentorFirstName, mentorLastName, mentorGithubId),\n        },\n      };\n    },\n  );\n};\n\nexport const getStudentStats = async (githubId: string, permissions: Permissions) => {\n  const studentStats = await getStudentStatsWithPosition(githubId, permissions);\n  return studentStats;\n};\n"
  },
  {
    "path": "server/src/routes/profile/user-info.ts",
    "content": "import { getRepository } from 'typeorm';\nimport { UserInfo } from '../../../../common/models/profile';\nimport { getFullName } from '../../rules';\nimport { User } from '../../models';\nimport { Permissions } from './permissions';\n\nexport const getUserInfo = async (githubId: string, permissions: Permissions): Promise<UserInfo> => {\n  const {\n    isAboutVisible,\n    isEducationVisible,\n    isEnglishVisible,\n    isPhoneVisible,\n    isEmailVisible,\n    isTelegramVisible,\n    isSkypeVisible,\n    isContactsNotesVisible,\n    isLinkedInVisible,\n    isWhatsAppVisible,\n  } = permissions;\n\n  const query = await getRepository(User)\n    .createQueryBuilder('user')\n    .select('\"user\".\"firstName\" AS \"firstName\", \"user\".\"lastName\" AS \"lastName\"')\n    .addSelect('\"user\".\"githubId\" AS \"githubId\"')\n    .addSelect('\"user\".\"countryName\" AS \"countryName\"')\n    .addSelect('\"user\".\"cityName\" AS \"cityName\"')\n    .addSelect('\"user\".\"discord\" AS \"discord\"')\n    .addSelect('\"user\".\"languages\" AS \"languages\"');\n\n  if (isEducationVisible) {\n    query.addSelect('\"user\".\"educationHistory\" AS \"educationHistory\"');\n  }\n\n  if (isEnglishVisible) {\n    query.addSelect('\"user\".\"englishLevel\" AS \"englishLevel\"');\n  }\n\n  if (isPhoneVisible) {\n    query.addSelect('\"user\".\"contactsPhone\" AS \"contactsPhone\"');\n  }\n\n  if (isEmailVisible) {\n    query.addSelect('\"user\".\"contactsEmail\" AS \"contactsEmail\"').addSelect('\"user\".\"contactsEpamEmail\" AS \"epamEmail\"');\n  }\n\n  if (isTelegramVisible) {\n    query.addSelect('\"user\".\"contactsTelegram\" AS \"contactsTelegram\"');\n  }\n\n  if (isSkypeVisible) {\n    query.addSelect('\"user\".\"contactsSkype\" AS \"contactsSkype\"');\n  }\n\n  if (isWhatsAppVisible) {\n    query.addSelect('\"user\".\"contactsWhatsApp\" AS \"contactsWhatsApp\"');\n  }\n\n  if (isContactsNotesVisible) {\n    query.addSelect('\"user\".\"contactsNotes\" AS \"contactsNotes\"');\n  }\n\n  if (isLinkedInVisible) {\n    query.addSelect('\"user\".\"contactsLinkedIn\" AS \"contactsLinkedIn\"');\n  }\n\n  if (isAboutVisible) {\n    query.addSelect('\"user\".\"aboutMyself\" AS \"aboutMyself\"');\n  }\n\n  const rawUser = await query.where('\"user\".\"githubId\" = :githubId', { githubId }).getRawOne();\n\n  if (rawUser == null) {\n    throw new Error(`User with githubId ${githubId} not found`);\n  }\n\n  const isContactsVisible =\n    isPhoneVisible || isEmailVisible || isTelegramVisible || isSkypeVisible || isContactsNotesVisible;\n\n  const {\n    firstName,\n    lastName,\n    countryName,\n    cityName,\n    discord,\n    educationHistory = null,\n    englishLevel = null,\n    contactsPhone = null,\n    contactsEmail = null,\n    contactsTelegram = null,\n    contactsSkype = null,\n    contactsWhatsApp = null,\n    contactsNotes = null,\n    contactsLinkedIn = null,\n    aboutMyself = null,\n    epamEmail = null,\n    languages = [],\n  } = rawUser;\n\n  return {\n    generalInfo: {\n      githubId,\n      location: {\n        countryName,\n        cityName,\n      },\n      aboutMyself: isAboutVisible ? aboutMyself : undefined,\n      educationHistory: isEducationVisible ? educationHistory : undefined,\n      englishLevel: isEnglishVisible ? englishLevel : undefined,\n      name: getFullName(firstName, lastName, githubId),\n      languages,\n    },\n    discord,\n    contacts: isContactsVisible\n      ? {\n          phone: contactsPhone,\n          email: contactsEmail,\n          epamEmail,\n          skype: contactsSkype,\n          telegram: contactsTelegram,\n          notes: contactsNotes,\n          linkedIn: contactsLinkedIn,\n          whatsApp: contactsWhatsApp,\n        }\n      : undefined,\n  };\n};\n"
  },
  {
    "path": "server/src/routes/registry/index.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, NOT_FOUND, OK } from 'http-status-codes';\nimport { getCustomRepository, getRepository } from 'typeorm';\nimport { parseAsync } from 'json2csv';\nimport { ILogger } from '../../logger';\nimport { Course, Mentor, MentorRegistry, Registry, Student, User } from '../../models';\nimport { IUserSession } from '../../models';\nimport { createGetRoute } from '../common';\nimport { adminGuard, anyCoursePowerUserGuard } from '../guards';\nimport { setResponse, setCsvResponse } from '../utils';\nimport { MentorRegistryRepository } from '../../repositories/mentorRegistry.repository';\nimport { sendNotification } from '../../services/notification.service';\n\nexport function registryRouter(logger?: ILogger) {\n  const router = new Router<any, any>({ prefix: '/registry' });\n  const repository = getCustomRepository(MentorRegistryRepository);\n\n  router.get('/', adminGuard, async (ctx: Router.RouterContext) => {\n    const { type, courseId } = ctx.query;\n    const registries = await getRepository(Registry).find({\n      skip: 0,\n      take: 1000,\n      order: { id: 'ASC' },\n      relations: ['user', 'course'],\n      where: [{ type: type || 'mentor', course: { id: courseId } }],\n    });\n\n    if (registries === undefined) {\n      setResponse(ctx, NOT_FOUND);\n      return;\n    }\n\n    setResponse(ctx, OK, registries);\n  });\n\n  router.post('/mentor', async (ctx: Router.RouterContext) => {\n    if (!ctx.state.user) {\n      setResponse(ctx, BAD_REQUEST);\n      return;\n    }\n    const { githubId, id } = ctx.state.user as IUserSession;\n    await repository.register(githubId, ctx.request.body);\n    await sendNotification({\n      notificationId: 'mentorRegistrationApproval:submit',\n      userId: id,\n    });\n    setResponse(ctx, OK);\n  });\n\n  router.get('/mentor', async (ctx: Router.RouterContext) => {\n    if (!ctx.state.user) {\n      setResponse(ctx, BAD_REQUEST);\n      return;\n    }\n\n    const { id: userId } = ctx.state.user as IUserSession;\n\n    const mentorRegistry = await getRepository(MentorRegistry).findOne({ where: { userId } });\n    if (mentorRegistry == null) {\n      setResponse(ctx, NOT_FOUND);\n      return;\n    }\n\n    const result = {\n      maxStudentsLimit: mentorRegistry.maxStudentsLimit,\n      preferedStudentsLocation: mentorRegistry.preferedStudentsLocation,\n      preselectedCourses: mentorRegistry.preselectedCourses.map(c => Number(c)),\n      preferredCourses: mentorRegistry.preferedCourses.map(c => Number(c)),\n    };\n    setResponse(ctx, OK, result);\n  });\n\n  router.get('/mentors/csv', anyCoursePowerUserGuard, async (ctx: Router.RouterContext) => {\n    const data = await repository.findAll();\n    const courses = await getRepository(Course).find({ select: ['id', 'name'] });\n\n    const csv = await parseAsync(\n      data.map(d => ({\n        ...d,\n        preferedCourses: d.preferedCourses.map(id => courses.find(c => Number(id) === c.id)?.name).filter(Boolean),\n        preselectedCourses: d.preselectedCourses\n          .map(id => courses.find(c => Number(id) === c.id)?.name)\n          .filter(Boolean),\n        courses: d.courses?.map(id => courses.find(c => Number(id) === c.id)?.name).filter(Boolean),\n      })),\n    );\n    setCsvResponse(ctx, OK, csv, 'mentors');\n  });\n\n  router.get('/:id', adminGuard, createGetRoute(Registry, logger));\n\n  router.post('/', async (ctx: Router.RouterContext) => {\n    if (!ctx.state.user) {\n      setResponse(ctx, BAD_REQUEST);\n      return;\n    }\n\n    const { githubId, id: userId } = ctx.state.user as IUserSession;\n    const { courseId, type, maxStudentsLimit, experienceInYears } = ctx.request.body;\n\n    if (!githubId || !courseId || !type) {\n      const errorMsg = 'Wrong payload: githubId courseId & type are required';\n\n      handleError({ logger, errorMsg, ctx });\n      return;\n    }\n\n    if (type === 'mentor' && (isNaN(maxStudentsLimit) || maxStudentsLimit < 2)) {\n      const errorMsg = 'Incorrect maxStudentsLimit';\n      handleError({ logger, errorMsg, ctx });\n      return;\n    }\n\n    try {\n      const [user, course, existingRegistry] = (await Promise.all([\n        getRepository(User).findOne({ where: { githubId }, relations: ['mentors', 'students'] }),\n        getRepository(Course).findOneBy({ id: Number(courseId) }),\n        getRepository(Registry).findOne({ where: { userId, courseId: Number(courseId) } }),\n      ])) as [User, Course, Registry];\n\n      if (existingRegistry && existingRegistry.userId === userId) {\n        setResponse(ctx, OK, existingRegistry);\n        return;\n      }\n\n      let registryPayload: Partial<Registry> = {\n        type,\n        user,\n        course,\n        status: 'pending',\n      };\n\n      if (type === 'student') {\n        registryPayload.status = 'approved';\n        if ((user.students || []).every(s => s.courseId !== courseId)) {\n          await getRepository(Student).save({ userId: user!.id, courseId: course!.id, startDate: new Date() });\n        }\n      } else if (type === 'mentor') {\n        registryPayload = {\n          ...registryPayload,\n          attributes: {\n            maxStudentsLimit,\n            experienceInYears,\n          },\n        };\n        if ((user!.mentors || [])!.length > 0) {\n          registryPayload.status = 'approved';\n          await getRepository(Mentor).save({ userId: user!.id, courseId: course!.id, maxStudentsLimit });\n        }\n      }\n\n      const registry = await getRepository(Registry).save(registryPayload);\n      setResponse(ctx, OK, registry);\n    } catch (e) {\n      handleError({ logger, errorMsg: (e as Error).message, ctx });\n    }\n  });\n\n  router.put('/', adminGuard, async (ctx: Router.RouterContext) => {\n    const ids = ctx.request.body.ids as number[];\n    const status = ctx.request.body.status;\n\n    const result = [];\n\n    for await (const id of ids) {\n      const oldRegistry = await getRepository(Registry).findOne({ where: { id: Number(id) }, relations: ['course'] });\n      if (!oldRegistry) {\n        continue;\n      }\n      const registryPayload = { ...oldRegistry, status };\n      const { userId, course, attributes } = registryPayload;\n      await getRepository(Registry).save(registryPayload);\n\n      if (status === 'approved') {\n        const existingMentor = await getRepository(Mentor).findOne({ where: { userId, courseId: course.id } });\n        if (existingMentor == null) {\n          const newMentor = await getRepository(Mentor).save({\n            userId,\n            courseId: course.id,\n            maxStudentsLimit: attributes.maxStudentsLimit,\n          });\n          result.push(newMentor);\n        } else {\n          result.push(existingMentor);\n        }\n      }\n    }\n\n    setResponse(ctx, OK, { registries: result });\n  });\n\n  return router;\n}\n\ninterface LoggingError {\n  logger?: ILogger;\n  errorMsg: string;\n  ctx: Router.RouterContext;\n}\n\nconst handleError = ({ logger, errorMsg, ctx }: LoggingError) => {\n  if (logger) {\n    logger.error(errorMsg);\n  }\n\n  setResponse(ctx, BAD_REQUEST, { message: errorMsg });\n};\n"
  },
  {
    "path": "server/src/routes/repository/events.ts",
    "content": "import { ILogger } from '../../logger';\nimport { RepositoryEvent } from '../../models';\nimport Router from '@koa/router';\nimport { getCustomRepository } from 'typeorm';\nimport { RepositoryEventRepository } from '../../repositories/repositoryEvent.repository';\nimport { setResponse } from '../utils';\nimport { OK } from 'http-status-codes';\nimport { updateRepositoryActivity } from '../../services/student.service';\n\nexport const createRepositoryEvents = (_: ILogger) => async (ctx: Router.RouterContext) => {\n  const data: Pick<RepositoryEvent, 'action' | 'githubId' | 'repositoryUrl'>[] = ctx.request.body;\n  await getCustomRepository(RepositoryEventRepository).save(data);\n\n  await Promise.all(data.map(it => updateRepositoryActivity(it.repositoryUrl)));\n\n  setResponse(ctx, OK);\n};\n"
  },
  {
    "path": "server/src/routes/repository/index.ts",
    "content": "import Router from '@koa/router';\nimport { ILogger } from '../../logger';\nimport { basicAuthAws } from '../guards';\nimport { createRepositoryEvents } from './events';\n\nexport function repositoryRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/repository' });\n\n  router.post('/events', basicAuthAws, createRepositoryEvents(logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/task/index.ts",
    "content": "import Router from '@koa/router';\nimport { BAD_REQUEST, OK } from 'http-status-codes';\nimport { getRepository } from 'typeorm';\nimport { ILogger } from '../../logger';\nimport { TaskVerification } from '../../models';\nimport { ScoreService } from '../../services/score';\nimport { createPostRoute } from '../common';\nimport { adminGuard, basicAuthAws } from '../guards';\nimport { setResponse } from '../utils';\n\nconst validateTaskId = async (ctx: Router.RouterContext, next: any) => {\n  const id = Number(ctx.params.id);\n  if (isNaN(id)) {\n    setResponse(ctx, BAD_REQUEST, 'Incorrect [Task Id]');\n    return;\n  }\n  ctx.params.id = id;\n  await next();\n};\n\nconst updateVerification = (logger?: ILogger) => async (ctx: Router.RouterContext) => {\n  const { createdDate, ...data } = ctx.request.body as {\n    createdDate: string;\n    score: number;\n    details: string;\n    status: any;\n  };\n\n  const id: number = Number(ctx.params.id);\n  try {\n    const score = Math.round(Number(data.score));\n    await getRepository(TaskVerification).save({ ...data, score, id });\n\n    const result = (await getRepository(TaskVerification).findOneBy({ id }))!;\n\n    const service = new ScoreService(0);\n    await service.saveScore(result.studentId, result.courseTaskId, {\n      comment: result.details,\n      score: result.score,\n    });\n\n    setResponse(ctx, OK, result);\n  } catch (err) {\n    if (logger) {\n      logger.error((err as Error).message);\n    }\n    setResponse(ctx, BAD_REQUEST, { message: (err as Error).message });\n  }\n};\n\nexport function taskRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/task' });\n\n  router.post('/verification', adminGuard, createPostRoute(TaskVerification, logger));\n  router.put('/verification/:id', basicAuthAws, validateTaskId, updateVerification(logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/taskVerification/index.ts",
    "content": "import Router from '@koa/router';\nimport { ILogger } from '../../logger';\nimport { TaskVerification } from '../../models';\nimport { createPostRoute } from '../common';\nimport { adminGuard } from '../guards';\n\nexport function taskVerification(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/task-verification' });\n\n  router.post('/', adminGuard, createPostRoute(TaskVerification, logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/tasks/index.ts",
    "content": "import Router from '@koa/router';\nimport { OK } from 'http-status-codes';\nimport * as url from 'url';\nimport { ILogger } from '../../logger';\nimport { Task } from '../../models';\nimport { getRepository } from 'typeorm';\nimport { setResponse } from '../utils';\nimport { adminGuard, RouterContext } from '../guards';\nimport { OperationResult } from '../../services/operationResult';\n\ntype TaskInput = {\n  name: string;\n  descriptionUrl?: string;\n};\n\nconst postTasks = (logger: ILogger) => async (ctx: RouterContext) => {\n  const data: TaskInput[] = ctx.request.body;\n\n  const response: OperationResult[] = [];\n\n  for await (const item of data) {\n    try {\n      if (item.descriptionUrl) {\n        try {\n          new url.URL(item.descriptionUrl);\n        } catch {\n          const message = `[${item.descriptionUrl}] is not url`;\n          response.push({ status: 'failed', value: message });\n          continue;\n        }\n      }\n      const result = await getRepository(Task).save(item);\n      response.push({ status: 'created', value: result.id });\n    } catch (e) {\n      logger.error(e as Error);\n      response.push({ status: 'failed', value: item.name });\n    }\n  }\n\n  setResponse(ctx, OK, response);\n};\n\nconst getTasks = (_: ILogger) => async (ctx: RouterContext) => {\n  const tasks = await getRepository(Task).find({\n    relations: ['discipline'],\n    order: {\n      updatedDate: 'DESC',\n    },\n  });\n  setResponse(ctx, OK, tasks);\n};\n\nexport function tasksRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/tasks' });\n\n  router.post('/', adminGuard, postTasks(logger));\n  router.get('/', getTasks(logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/users/index.ts",
    "content": "import Router from '@koa/router';\nimport { OK, BAD_REQUEST } from 'http-status-codes';\nimport { ILogger } from '../../logger';\nimport { User } from '../../models';\nimport { getRepository } from 'typeorm';\nimport { setResponse } from '../utils';\nimport { adminGuard, guard, RouterContext } from '../guards';\nimport { OperationResult, userService } from '../../services';\n\nconst postUsers = (_: ILogger) => async (ctx: RouterContext) => {\n  const data = ctx.request.body as { githubId: string }[];\n\n  const result: OperationResult[] = [];\n  for await (const item of data) {\n    try {\n      const userRepository = getRepository(User);\n      const entity = await userRepository.findOne({ where: { githubId: item.githubId.toLowerCase() } });\n\n      if (entity == null) {\n        const user = await userRepository.save(item);\n        result.push({ status: 'created', value: `GithubId: ${item.githubId}, UserId: ${user.id}` });\n      } else {\n        const user = await userRepository.save({ ...entity, ...item });\n        result.push({ status: 'updated', value: `GithubId: ${item.githubId}, UserId: ${user.id}` });\n      }\n    } catch (e) {\n      result.push({ status: 'failed', value: `GithubId: ${item.githubId}. Error: ${(e as Error).message}` });\n    }\n  }\n\n  setResponse(ctx, OK, result);\n};\n\ntype SearchConfigItem = {\n  field: string;\n  isCaseSensitive: boolean;\n};\n\nconst isSearchConfigIncludesName = (searchConfig: SearchConfigItem[]): boolean => {\n  return ['firstName', 'lastName'].every(fieldName => searchConfig.find(({ field }) => field === fieldName));\n};\n\nconst generateSearchString = (searchConfig: SearchConfigItem[], parameterName: string): string => {\n  const searchStringParts = searchConfig.map(\n    ({ field, isCaseSensitive }: SearchConfigItem) =>\n      `user.${field} ${isCaseSensitive ? 'like' : 'ilike'} ${parameterName}`,\n  );\n\n  if (isSearchConfigIncludesName(searchConfig)) {\n    searchStringParts.push(`CONCAT(user.firstName, ' ', user.lastName) ilike ${parameterName}`);\n  }\n\n  return searchStringParts.join(' OR ');\n};\n\nconst generateResponse = (user: any, searchConfig: SearchConfigItem[]) =>\n  searchConfig.reduce((response: any, { field }: SearchConfigItem) => ({ ...response, [field]: user[field] }), {});\n\nconst getSearch = (_: ILogger) => (searchConfig: SearchConfigItem[]) => async (ctx: RouterContext) => {\n  const searchText = ctx.params.searchText;\n  if (!searchText) {\n    setResponse(ctx, OK, []);\n    return;\n  }\n\n  const entities = await getRepository(User)\n    .createQueryBuilder('user')\n    .where(generateSearchString(searchConfig, ':text'), {\n      text: searchText.toLowerCase() + '%',\n    })\n    .orWhere(`CAST(user.discord AS jsonb)->>'username' ILIKE :search`, { search: `${searchText}%` })\n    .limit(20)\n    .getMany();\n\n  setResponse(\n    ctx,\n    OK,\n    entities.map(user => {\n      const response = generateResponse(user, searchConfig);\n      const { lastName, firstName, ...other } = response;\n      return {\n        ...other,\n        id: user.id,\n        name: userService.createName({ lastName, firstName }),\n        discord: user.discord ? `${user.discord.username}#${user.discord.discriminator}` : undefined,\n      };\n    }),\n  );\n};\n\nconst getSearchByGithubId = (logger: ILogger) =>\n  getSearch(logger)([\n    { field: 'githubId', isCaseSensitive: true },\n    { field: 'firstName', isCaseSensitive: false },\n    { field: 'lastName', isCaseSensitive: false },\n  ]);\n\nconst postUserActivist = (_: ILogger) => async (ctx: RouterContext) => {\n  const data = ctx.request.body as { activist: boolean };\n  const userId: number = ctx.params.userId;\n  const user = await userService.getUserById(userId);\n  if (user == null) {\n    setResponse(ctx, BAD_REQUEST, 'no user');\n    return;\n  }\n  user.activist = !!data.activist;\n  await userService.saveUser(user);\n  setResponse(ctx, OK);\n};\n\nexport function usersRoute(logger: ILogger) {\n  const router = new Router<any, any>({ prefix: '/users' });\n\n  /**\n   * @swagger\n   *\n   * /users:\n   *   post:\n   *      description: Add/Update users\n   *      security:\n   *        - cookieAuth: []\n   *      produces:\n   *        - application/json\n   *      responses:\n   *        200:\n   *          description: operation status\n   */\n  router.post('/', adminGuard, postUsers(logger));\n\n  /**\n   * @swagger\n   *\n   * /users:\n   *   post:\n   *      description: Update user activist status\n   *      security:\n   *        - cookieAuth: []\n   *      produces:\n   *        - application/json\n   *      responses:\n   *        200:\n   *          description: operation status\n   */\n  router.post('/{userId}/activist', adminGuard, postUserActivist(logger));\n\n  /**\n   * @swagger\n   *\n   * /users/search/:searchText:\n   *   post:\n   *      description: Search users\n   *      security:\n   *        - cookieAuth: []\n   *      produces:\n   *        - application/json\n   *      responses:\n   *        200:\n   *          description: operation status\n   */\n  router.get('/search/:searchText', guard, getSearchByGithubId(logger));\n\n  return router;\n}\n"
  },
  {
    "path": "server/src/routes/utils.ts",
    "content": "import { IApiResponse } from '../models';\nimport Router from '@koa/router';\nimport moment from 'moment-timezone';\n\nexport function setResponse<T>(\n  ctx: Router.RouterContext | Router.RouterContext<any, any>,\n  status: number,\n  data?: T,\n  cacheTimeSeconds: number = 0,\n) {\n  ctx.status = status;\n  ctx.body = { data } as IApiResponse<T>;\n  ctx.res.setHeader('Cache-Control', cacheTimeSeconds > 0 ? `public, max-age=${cacheTimeSeconds}` : 'no-cache');\n  return ctx;\n}\n\nexport function setErrorResponse<T>(\n  ctx: Router.RouterContext | Router.RouterContext<any, any>,\n  status: number,\n  message: string,\n) {\n  ctx.status = status;\n  ctx.body = { error: { message } } as IApiResponse<T>;\n  ctx.res.setHeader('Cache-Control', 'no-cache');\n  return ctx;\n}\n\nexport function setCsvResponse(\n  ctx: Router.RouterContext | Router.RouterContext<any, any>,\n  status: number,\n  data: string,\n  filename = 'csv',\n) {\n  ctx.status = status;\n  ctx.body = data;\n  ctx.res.setHeader('Content-Type', 'text/csv');\n  if (filename) {\n    ctx.res.setHeader('Content-disposition', `filename=\"${filename}.csv\"`);\n  }\n  return ctx;\n}\n\nexport const dateFormatter = (date: string, timeZone: string, format: string) =>\n  date ? moment(date).tz(timeZone).format(format) : '';\n"
  },
  {
    "path": "server/src/routes/validators.ts",
    "content": "import Router from '@koa/router';\nimport { Next } from 'koa';\nimport { setResponse } from './utils';\nimport { BAD_REQUEST, FORBIDDEN } from 'http-status-codes';\nimport { getCourseTask } from '../services/tasks.service';\nimport { DateTime } from 'luxon';\nimport { CourseRole } from '../models';\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst auth = require('basic-auth'); //tslint:disable-line\n\nexport const validateGithubIdAndAccess = async (ctx: Router.RouterContext, next: Next) => {\n  let githubId: string = ctx.params.githubId;\n  if (!githubId) {\n    setResponse(ctx, BAD_REQUEST, 'Incorrect [githubId]');\n    return;\n  }\n  const user = ctx.state.user;\n  if (githubId === 'me' && user) {\n    githubId = user.githubId;\n  } else {\n    githubId = githubId.toLowerCase();\n  }\n  ctx.params.githubId = githubId;\n  if ((user != null && user.isAdmin) || auth(ctx)) {\n    await next();\n    return;\n  }\n  if (user.githubId !== githubId) {\n    setResponse(ctx, FORBIDDEN);\n    return;\n  }\n  await next();\n};\n\n// This validator exists to cover specific case of course manager functionality and leave untouched rest endpoints\n// See https://github.com/rolling-scopes/rsschool-app/issues/2611\n// After migration to nestjs should be replaced with appropriate role check\nexport const validateGithubIdAndAccessForUserOrPowerUser = async (ctx: Router.RouterContext, next: Next) => {\n  let githubId: string = ctx.params.githubId;\n  if (!githubId) {\n    setResponse(ctx, BAD_REQUEST, 'Incorrect [githubId]');\n    return;\n  }\n  const user = ctx.state.user;\n  if (githubId === 'me' && user) {\n    githubId = user.githubId;\n  } else {\n    githubId = githubId.toLowerCase();\n  }\n  ctx.params.githubId = githubId;\n  const isCourseManager = Boolean(\n    ctx.params.courseId && user?.courses[ctx.params.courseId]?.roles?.includes(CourseRole.Manager),\n  );\n  if ((user != null && (user.isAdmin || isCourseManager)) || auth(ctx)) {\n    await next();\n    return;\n  }\n  if (user.githubId !== githubId) {\n    setResponse(ctx, FORBIDDEN);\n    return;\n  }\n  await next();\n};\n\nexport const validateCrossCheckExpirationDate = async (ctx: Router.RouterContext, next: Next) => {\n  const courseTaskId: string = ctx.params.courseTaskId;\n  if (!courseTaskId) {\n    setResponse(ctx, BAD_REQUEST, 'Incorrect [courseTaskId]');\n    return;\n  }\n\n  const task = await getCourseTask(Number(courseTaskId));\n  if (!task || (task.studentEndDate && DateTime.local() > DateTime.fromJSDate(new Date(task.studentEndDate)))) {\n    setResponse(ctx, BAD_REQUEST, 'Cross Check deadline has expired');\n    return;\n  }\n\n  await next();\n};\n\nexport const validateGithubId = async (ctx: Router.RouterContext, next: Next) => {\n  let githubId: string = ctx.params.githubId;\n  if (!githubId) {\n    setResponse(ctx, BAD_REQUEST, 'Incorrect [githubId]');\n    return;\n  }\n  const user = ctx.state.user;\n  if (githubId === 'me' && user) {\n    githubId = user.githubId;\n  } else {\n    githubId = githubId.toLowerCase();\n  }\n  ctx.params.githubId = githubId;\n  await next();\n};\n\nexport const validateExpelledStudent = async (ctx: Router.RouterContext, next: Next) => {\n  const githubId: string = ctx.params.githubId;\n  if (!githubId) {\n    setResponse(ctx, BAD_REQUEST, 'Incorrect [githubId]');\n    return;\n  }\n  const user = ctx.state.user;\n  const courseId = ctx.params.courseId;\n  if (user.courses[courseId].isExpelled) {\n    setResponse(ctx, FORBIDDEN);\n    return;\n  }\n  await next();\n};\n"
  },
  {
    "path": "server/src/rules/__tests__/mentors.test.ts",
    "content": "import { createMentorStudentPairs } from '../mentors';\n\nconst mentors = [\n  { id: 1, maxStudentsLimit: 2, students: [] },\n  { id: 2, maxStudentsLimit: 1, students: [{ id: 99 }] },\n  { id: 3, maxStudentsLimit: 1, students: [] },\n];\n\nconst students = [\n  { id: 99, totalScore: 99, mentor: { id: 1 } },\n  { id: 98, totalScore: 98, mentor: null },\n  { id: 97, totalScore: 97, mentor: null },\n  { id: 96, totalScore: 96, mentor: null },\n];\n\ndescribe('createMentorStudentPairs', () => {\n  it('should create pairs right', () => {\n    const result = createMentorStudentPairs(mentors, students);\n    const mentor1 = result.filter(pair => pair.mentor.id === 1);\n    const mentor2 = result.filter(pair => pair.mentor.id === 2);\n    const mentor3 = result.filter(pair => pair.mentor.id === 3);\n\n    expect(mentor1.length).toBe(2);\n    expect(mentor2.length).toBe(1);\n    expect(mentor3.length).toBe(1);\n\n    expect(mentor2[0]).toMatchObject({ mentor: { id: 2 }, student: { id: 99 } });\n  });\n});\n"
  },
  {
    "path": "server/src/rules/index.ts",
    "content": "export * from './name';\n"
  },
  {
    "path": "server/src/rules/interviews.ts",
    "content": "import { groupBy, sumBy, shuffle, sum, pick, random, filter, sortBy, entries } from 'lodash';\nimport { InterviewInfo } from '../repositories/interview.repository';\nimport { MentorDetails } from '../../../common/models';\n\ntype Mentor = {\n  id: number;\n  githubId: string;\n  cityName: string | null;\n  countryName: string;\n  capacity: number;\n  lowGrade: boolean;\n};\n\ntype Student = {\n  id: number;\n  githubId: string;\n  cityName: string;\n  countryName: string;\n  mentor: { id: number } | null;\n  totalScore: number;\n};\n\ntype DistibutionPair = {\n  student: {\n    id: number;\n  };\n  mentor: {\n    id: number;\n  };\n};\n\nconst MIN_INTERVIEW_COUNT = 4;\n\nexport function createInterviews(allMentors: MentorDetails[], allStudents: Student[], interviews: InterviewInfo[]) {\n  // filter students who has interview already\n  const availableStudents = allStudents.filter(s => !interviews.find(i => i.student.githubId === s.githubId));\n\n  // create pairs if student already has mentor\n  let distibution: DistibutionPair[] = allMentors\n    // filter mentors who has students\n    .filter(m => m.students.length)\n    // create pairs\n    .map(m => m.students.map((s: { id: number }) => ({ student: { id: s.id }, mentor: { id: m.id } })))\n    .flat()\n    // filter students who has interview already\n    .filter(pair => !interviews.find(i => i.interviewer.id === pair.mentor.id && i.student.id === pair.student.id));\n\n  const freeStudents = filterFreeStudents(distibution, availableStudents);\n\n  const cityMentors = groupBy(extractMentors(allMentors, interviews, 'city'), 'cityName');\n  const countryMentors = groupBy(extractMentors(allMentors, interviews, 'country'), 'countryName');\n  const anyMentors = sortBy(filterMentors(filter(allMentors, { studentsPreference: 'any' }), interviews), ['id']);\n\n  let students: typeof freeStudents = [];\n  for (const [cityName, mentors] of entries(cityMentors)) {\n    students = findStudents(mentors, freeStudents, students, cityPredicate(cityName));\n  }\n  for (const [countryName, mentors] of entries(countryMentors)) {\n    students = findStudents(mentors, freeStudents, students, countryPredicate(countryName));\n  }\n  students = findStudents(anyMentors, freeStudents, students);\n\n  for (const [cityName, mentors] of entries(cityMentors)) {\n    const result = assignStudents(mentors, filterFreeStudents(distibution, students), cityPredicate(cityName));\n    distibution = distibution.concat(result);\n  }\n  for (const [countryName, mentors] of entries(countryMentors)) {\n    const result = assignStudents(mentors, filterFreeStudents(distibution, students), countryPredicate(countryName));\n    distibution = distibution.concat(result);\n  }\n  const result = assignStudents(anyMentors, filterFreeStudents(distibution, students));\n  distibution = distibution.concat(result);\n  return distibution;\n}\n\nfunction filterFreeStudents(distibution: DistibutionPair[], students: Student[]) {\n  return students.filter(s => !distibution.find(d => d.student.id === s.id));\n}\n\nfunction assignStudents(mentors: Mentor[], students: Student[], predicate = (_: Student) => true) {\n  const mentorCapacity = sumBy(mentors, 'capacity');\n  const freeStudents = students\n    .filter(predicate)\n    .sort((a, b) => b.totalScore - a.totalScore)\n    .slice(0, mentorCapacity);\n  return distributeStudentsRandomly(mentors, freeStudents);\n}\n\nfunction extractMentors(\n  allMentors: MentorDetails[],\n  interviews: InterviewInfo[],\n  preference: 'city' | 'country' | 'any',\n) {\n  return sortBy(filterMentors(filter(allMentors, { studentsPreference: preference }), interviews), ['id']);\n}\n\nfunction findStudents(\n  mentors: Mentor[],\n  allStudents: Student[],\n  students: Student[],\n  predicate = (_: Student) => true,\n) {\n  const mentorCapacity = sumBy(mentors, 'capacity');\n  const freeStudents = allStudents\n    .filter(s => {\n      const newStudent = !students.find(student => student.id === s.id);\n      return predicate(s) && newStudent;\n    })\n    .sort((a, b) => b.totalScore - a.totalScore)\n    .slice(0, mentorCapacity);\n  students = students.concat(freeStudents);\n  return students;\n}\n\nfunction filterMentors(mentors: MentorDetails[], interviews: InterviewInfo[]): Mentor[] {\n  return mentors\n    .map(m => {\n      const studentsCount = m.students.length;\n\n      let lowGrade = false;\n      const capacity = m.maxStudentsLimit - studentsCount;\n      let maxCapacity: number;\n\n      if (studentsCount + capacity < MIN_INTERVIEW_COUNT) {\n        if (capacity === 0) {\n          lowGrade = true;\n        }\n        maxCapacity = MIN_INTERVIEW_COUNT - studentsCount;\n      } else {\n        maxCapacity = capacity > 1 ? capacity + 2 : capacity === 1 ? capacity + 1 : 0;\n      }\n      const interviewCount = interviews.filter(i => i.interviewer.githubId === m.githubId).length;\n      const leftCapacity = Math.max(0, maxCapacity - interviewCount);\n      return { ...m, capacity: leftCapacity, lowGrade };\n    })\n    .filter(m => m.capacity > 0);\n}\n\nexport function distributeStudentsRandomly(mentors: Mentor[], students: Student[]) {\n  const pairs = [];\n  let capacity = Math.min(sum(mentors.map(m => m.capacity)), students.length);\n  const shuffledStudents = shuffle(students);\n\n  while (capacity > 0) {\n    for (const mentor of mentors) {\n      let index = random(0, shuffledStudents.length - 1);\n\n      if (mentor.lowGrade) {\n        let minScore = 0;\n        shuffledStudents.forEach((student, i) => {\n          if (minScore === 0 || student.totalScore < minScore) {\n            minScore = student.totalScore;\n            index = i;\n          }\n        });\n      }\n\n      const student = shuffledStudents[index];\n      if (mentor.capacity > 0) {\n        mentor.capacity--;\n        shuffledStudents.splice(index, 1);\n        if (student) {\n          pairs.push({\n            mentor: pick(mentor, ['id']),\n            student: pick(student, ['id']),\n          });\n        }\n        capacity--;\n      }\n    }\n  }\n  return pairs;\n}\n\nconst cityPredicate = (cityName: string) => (s: { cityName: string }) => s.cityName === cityName;\nconst countryPredicate = (countryName: string) => (s: { countryName: string }) => s.countryName === countryName;\n"
  },
  {
    "path": "server/src/rules/mentors.ts",
    "content": "import { pick, random, shuffle, sum, sumBy } from 'lodash';\nimport { MentorDetails } from '../../../common/models';\n\ntype Mentor = {\n  id: number;\n  capacity: number;\n};\n\ntype MentorInput = Pick<MentorDetails, 'id' | 'maxStudentsLimit' | 'students'>;\n\ntype Student = {\n  id: number;\n  totalScore: number;\n  mentor: { id: number } | null;\n};\n\nexport function createMentorStudentPairs(allMentors: MentorInput[], allStudents: Student[]) {\n  // filter students who has interview already\n  const availableStudents = allStudents.filter(s => s.mentor?.id == null);\n\n  // create pairs if student already has mentor\n  let distibution = allMentors\n    .filter(m => m.students.length)\n    .map(m => m.students.map(s => ({ student: { id: s.id }, mentor: { id: m.id } })))\n    .flat();\n\n  const filterFreeStudents = (students: Student[]) =>\n    students.filter(s => !distibution.find(d => d.student.id === s.id));\n\n  const freeStudents = filterFreeStudents(availableStudents);\n  const mentors = filterMentors(allMentors);\n\n  const result = assignStudents(mentors, freeStudents);\n  distibution = distibution.concat(result);\n  return distibution;\n}\n\nfunction assignStudents(mentors: Mentor[], students: Student[], predicate = (_: Student) => true) {\n  const mentorCapacity = sumBy(mentors, 'capacity');\n  const freeStudents = students\n    .filter(predicate)\n    .sort((a, b) => b.totalScore - a.totalScore)\n    .slice(0, mentorCapacity);\n  return distributeRandomly(mentors, freeStudents);\n}\n\nfunction filterMentors(mentors: MentorInput[]): Mentor[] {\n  return mentors\n    .map(m => {\n      const studentsCount = m.students.length;\n\n      const capacity = Math.max(0, m.maxStudentsLimit - studentsCount);\n      return { ...m, capacity };\n    })\n    .filter(m => m.capacity > 0);\n}\n\nfunction distributeRandomly(mentors: Mentor[], students: Student[]) {\n  const pairs = [];\n  let capacity = Math.min(sum(mentors.map(m => m.capacity)), students.length);\n  const shuffledStudents = shuffle(students);\n\n  while (capacity > 0) {\n    for (const mentor of mentors) {\n      const index = random(0, shuffledStudents.length - 1);\n\n      const student = shuffledStudents[index];\n      if (mentor.capacity > 0) {\n        mentor.capacity--;\n        shuffledStudents.splice(index, 1);\n        if (student) {\n          pairs.push({\n            mentor: pick(mentor, ['id']),\n            student: pick(student, ['id']),\n          });\n        }\n        capacity--;\n      }\n    }\n  }\n  return pairs;\n}\n"
  },
  {
    "path": "server/src/rules/name.ts",
    "content": "export const getFullName = (firstName: string, lastName: string, githubId: string) =>\n  [firstName, lastName].filter(Boolean).join(' ') || githubId;\n"
  },
  {
    "path": "server/src/rules/types.ts",
    "content": "export type TaskOwnerCourses = {\n  id: number;\n  tasksIds: number[];\n};\n\nexport type TaskOwnerRole = {\n  courses: TaskOwnerCourses[];\n};\n"
  },
  {
    "path": "server/src/schedule.ts",
    "content": "import { scheduleJob } from 'node-schedule';\nimport { ILogger } from './logger';\nimport { ScoreService } from './services/score';\nimport { cancelPendingTasks } from './services/taskVerification.service';\n\nexport function startBackgroundJobs(logger: ILogger) {\n  scheduleJob('0 1 * * *', async () => {\n    logger.info('Starting score update job');\n    await ScoreService.recalculateTotalScore(logger);\n  });\n\n  scheduleJob('0 1/1 * * *', async () => {\n    logger.info('Starting pending tasks cancelling job');\n    await cancelPendingTasks();\n  });\n}\n"
  },
  {
    "path": "server/src/services/aws.service.ts",
    "content": "import axios, { AxiosError } from 'axios';\nimport { config } from '../config';\n\ntype TaskVerificationEvent = {\n  studentId: number;\n  githubId: string;\n  courseTask: {\n    id: number;\n    type: string;\n  };\n};\n\nexport async function postTaskVerification(data: TaskVerificationEvent[]) {\n  try {\n    if (config.isDevMode) {\n      return;\n    }\n    return axios.post(`${config.aws.restApiUrl}/task`, data, {\n      headers: { 'x-api-key': config.aws.restApiKey },\n    });\n  } catch (err) {\n    const error = err as AxiosError;\n    throw error.response?.data ?? error.message;\n  }\n}\n\nexport async function uploadFileByGithubId(githubId: string, key: string, data: string) {\n  try {\n    // if (config.isDevMode) {\n    //   return;\n    // }\n    const response = await axios.post(`${config.aws.restApiUrl}/upload?key=${key}&githubId=${githubId}`, data, {\n      headers: { 'x-api-key': config.aws.restApiKey },\n    });\n    return response.data;\n  } catch (err) {\n    const error = err as AxiosError;\n    throw error.response?.data ?? error.message;\n  }\n}\n"
  },
  {
    "path": "server/src/services/check.service.ts",
    "content": "import { getRepository } from 'typeorm';\nimport { CourseTask, Student, Task, TaskSolutionResult, User } from '../models';\n\nconst LOW_ERROR_RATE = 0.9;\nconst HIGH_ERROR_RATE = 1.1;\nconst MIN_LENGTH_MESSAGE = 70;\n\n/* Get checkers who passed max score for everyone and maybe didn't review task*/\nexport async function getCheckersWithMaxScore(taskId: number) {\n  const data = await getRepository(TaskSolutionResult)\n    .createQueryBuilder('ts')\n    .select('t.name', 'taskName')\n    .addSelect('\"checkerUser\".\"githubId\"', 'checkerGithubId')\n    .addSelect('\"studentUser\".\"githubId\"', 'studentGithubId')\n\n    // Get students whose work has been checked by at least 3 checkers and\n    // calculate the average score using the leave-one-out strategy.\n\n    .addSelect(\n      `\n      CASE\n        WHEN \"studentScoreSumCnt\".\"cnt\" >= 3\n        THEN ROUND((\"studentScoreSumCnt\".\"sum\" - ts.\"score\") / (\"studentScoreSumCnt\".\"cnt\" - 1), 1)\n        ELSE NULL\n      END\n    `,\n      'studentAverageScoreExcludeChecker',\n    )\n    .addSelect('ts.score', 'checkerScore')\n    .innerJoin(\n      qb =>\n        qb\n          .subQuery()\n          .select('tsr.\"studentId\"')\n          .addSelect('SUM(tsr.score)', 'sum')\n          .addSelect('COUNT(*)', 'cnt')\n          .from(TaskSolutionResult, 'tsr')\n          .where('tsr.\"courseTaskId\" = :taskId', { taskId })\n          .groupBy('tsr.\"studentId\"'),\n      'studentScoreSumCnt',\n      'ts.\"studentId\" = \"studentScoreSumCnt\".\"studentId\"',\n    )\n    .innerJoin(CourseTask, 'ct', 'ts.\"courseTaskId\" = ct.id')\n    .innerJoin(Task, 't', 'ct.\"taskId\" = t.id')\n    .innerJoin(Student, 'checker', 'ts.\"checkerId\" = checker.id ')\n    .innerJoin(User, 'checkerUser', 'checker.\"userId\" = \"checkerUser\".id')\n    .innerJoin(Student, 'student', 'ts.\"studentId\" = student.id ')\n    .innerJoin(User, 'studentUser', 'student.\"userId\" = \"studentUser\".id')\n    .where('ts.\"courseTaskId\" = :taskId', { taskId })\n\n    // Get students whose work has been checked by at least 3 checkers and\n    // verify that the score given by the checker does not exceed +/- 10% of the average score.\n\n    .andWhere(\n      `\n      \"studentScoreSumCnt\".\"cnt\" >= 3\n      AND ts.\"score\" NOT BETWEEN\n        ((\"studentScoreSumCnt\".\"sum\" - ts.\"score\")::numeric / (\"studentScoreSumCnt\".\"cnt\" - 1)) * (:low)::numeric\n        AND ((\"studentScoreSumCnt\".\"sum\" - ts.\"score\")::numeric / (\"studentScoreSumCnt\".\"cnt\" - 1)) * (:high)::numeric\n    `,\n      { low: LOW_ERROR_RATE, high: HIGH_ERROR_RATE },\n    )\n    .orderBy('\"checkerUser\".\"githubId\"')\n    .getRawMany();\n\n  return data.map(e => {\n    return {\n      ...e,\n      studentAvgScore: Number(e.studentAverageScoreExcludeChecker),\n      key: `${e.checkerGithubId}.${e.studentGithubId}.${e.taskName}`,\n    };\n  });\n}\n\n/* Get checkers who passed not max score with short comment */\nexport async function getCheckersWithoutComments(taskId: number) {\n  const data = await getRepository(TaskSolutionResult)\n    .createQueryBuilder('ts')\n    .select('t.name', 'taskName')\n    .addSelect('\"checkerUser\".\"githubId\"', 'checkerGithubId')\n    .addSelect('\"studentUser\".\"githubId\"', 'studentGithubId')\n    .addSelect('ts.score', 'checkerScore')\n    .addSelect('ts.comment', 'comment')\n    .innerJoin(CourseTask, 'ct', 'ts.\"courseTaskId\" = ct.id')\n    .innerJoin(Task, 't', 'ct.\"taskId\" = t.id')\n    .innerJoin(Student, 'checker', 'ts.\"checkerId\" = checker.id ')\n    .innerJoin(User, 'checkerUser', 'checker.\"userId\" = \"checkerUser\".id')\n    .innerJoin(Student, 'student', 'ts.\"studentId\" = student.id ')\n    .innerJoin(User, 'studentUser', 'student.\"userId\" = \"studentUser\".id')\n    .where('LENGTH(ts.comment) < :length', { length: MIN_LENGTH_MESSAGE })\n    .andWhere('ts.score < ct.\"maxScore\"')\n    .andWhere('ts.\"courseTaskId\" = :taskId', { taskId })\n    .andWhere('json_array_length(ts.\"historicalScores\") < 2')\n    .orderBy('\"checkerUser\".\"githubId\"')\n    .getRawMany();\n\n  return data.map(e => {\n    return { ...e, key: `${e.checkerGithubId}.${e.studentGithubId}.${e.taskName}` };\n  });\n}\n"
  },
  {
    "path": "server/src/services/course.service.ts",
    "content": "import _ from 'lodash';\nimport { getRepository, getManager, getCustomRepository } from 'typeorm';\nimport { MentorBasic, MentorDetails, StudentBasic } from '../../../common/models';\nimport {\n  Course,\n  CourseTask,\n  Mentor,\n  Student,\n  User,\n  CourseEvent,\n  TaskSolution,\n  CourseUser,\n  TaskSolutionChecker,\n  TaskChecker,\n  TaskSolutionResult,\n  IUserSession,\n  TaskResult,\n  TaskInterviewResult,\n  isAdmin,\n  isManager,\n  isSupervisor,\n} from '../models';\nimport { createName } from './user.service';\nimport { StageInterviewRepository } from '../repositories/stageInterview.repository';\nimport { getStageInterviewRating } from './stageInterview.service';\n\nexport const getPrimaryUserFields = (modelName = 'user') => [\n  `${modelName}.id`,\n  `${modelName}.firstName`,\n  `${modelName}.lastName`,\n  `${modelName}.githubId`,\n  `${modelName}.cityName`,\n  `${modelName}.countryName`,\n  `${modelName}.discord`,\n];\n\nexport const getContactsUserFields = (modelName = 'user') => [\n  `${modelName}.primaryEmail`,\n  `${modelName}.contactsPhone`,\n  `${modelName}.contactsEmail`,\n  `${modelName}.contactsTelegram`,\n  `${modelName}.contactsLinkedIn`,\n  `${modelName}.contactsSkype`,\n  `${modelName}.contactsEpamEmail`,\n];\n\nexport async function getCourseMentor(courseId: number, userId: number): Promise<{ id: number } | null> {\n  return await getRepository(Mentor)\n    .createQueryBuilder('mentor')\n    .where('mentor.\"courseId\" = :courseId AND mentor.\"userId\" = :userId', { userId, courseId })\n    .getOne();\n}\n\nexport interface MentorWithContacts\n  extends\n    MentorBasic,\n    Pick<\n      User,\n      'contactsEmail' | 'contactsPhone' | 'contactsSkype' | 'contactsTelegram' | 'contactsWhatsApp' | 'contactsNotes'\n    > {}\n\nexport interface AssignedStudent extends StudentBasic {\n  courseTaskId: number | null;\n}\n\nexport interface StudentDetails extends StudentBasic {\n  cityName: string;\n  countryName: string;\n  totalScore: number;\n  interviews: { id: number; isCompleted: boolean; interviewer?: { githubId: string } }[];\n  repository: string;\n  assignedChecks: { name: string; id: number }[];\n}\n\nexport interface StudentWithResults {\n  id: number;\n  name: string;\n  githubId: string;\n  mentor?: { name: string; githubId: string };\n  totalScore: number;\n  totalScoreChangeDate: Date;\n  rank: number;\n  cityName: string;\n  countryName: string;\n  isActive: boolean;\n  taskResults: { courseTaskId: number; score: number }[];\n}\n\nexport function convertToMentorBasic(mentor: Mentor): MentorBasic {\n  const user = (mentor.user as User)!;\n  return {\n    isActive: !mentor.isExpelled,\n    name: createName(user),\n    id: mentor.id,\n    githubId: user.githubId,\n    students: mentor.students ? mentor.students.filter(s => !s.isExpelled && !s.isFailed).map(s => ({ id: s.id })) : [],\n    cityName: user.cityName ?? '',\n    countryName: user.countryName ?? '',\n  };\n}\n\nexport function convertToStudentBasic(student: Student): StudentBasic {\n  const user = (student.user as User)!;\n  return {\n    name: createName(user),\n    isActive: !student.isExpelled && !student.isFailed,\n    id: student.id,\n    githubId: user.githubId,\n    mentor: student.mentor ? convertToMentorBasic(student.mentor) : null,\n    cityName: user.cityName ?? '',\n    countryName: user.countryName ?? '',\n    discord: user.discord,\n    totalScore: student.totalScore,\n  };\n}\n\nexport function convertToStudentDetails(student: Student): StudentDetails {\n  const studentBasic = convertToStudentBasic(student);\n  const user = (student.user as User)!;\n  const checkers: TaskChecker[] = student.taskChecker ?? [];\n  const checks = checkers.map(({ courseTask: { id, task } }) => ({ id, name: task.name })) ?? [];\n  return {\n    ...studentBasic,\n    totalScore: student.totalScore,\n    cityName: user.cityName || 'Other',\n    countryName: user.countryName || 'Other',\n    interviews: _.isEmpty(student.stageInterviews)\n      ? []\n      : student.stageInterviews!.map(i => ({\n          id: i.id,\n          isCompleted: i.isCompleted,\n        })),\n    repository: student.repository,\n    assignedChecks: checks,\n  };\n}\n\nexport function convertToMentorDetails(mentor: Mentor): MentorDetails {\n  const mentorBasic = convertToMentorBasic(mentor);\n  const user = (mentor.user as User)!;\n  return {\n    ...mentorBasic,\n    students: mentor.students ?? [],\n    cityName: user.cityName ?? '',\n    countryName: user.countryName ?? '',\n    maxStudentsLimit: mentor.maxStudentsLimit,\n    studentsPreference: mentor.studentsPreference ?? 'any',\n    studentsCount: mentor.students ? mentor.students.length : 0,\n    screenings: {\n      total: mentor.stageInterviews ? mentor.stageInterviews.length : 0,\n    },\n  };\n}\n\nfunction mentorQuery() {\n  return getRepository(Mentor).createQueryBuilder('mentor');\n}\n\nfunction studentQuery() {\n  return getRepository(Student).createQueryBuilder('student');\n}\n\nexport async function getCourses() {\n  const records = await getRepository(Course)\n    .createQueryBuilder('course')\n    .leftJoinAndSelect('course.discordServer', 'discordServer')\n    .where('course.completed = false')\n    .getMany();\n  return records;\n}\n\nexport async function getCourse(id: number) {\n  const records = await getRepository(Course)\n    .createQueryBuilder('course')\n    .leftJoinAndSelect('course.discordServer', 'discordServer')\n    .where({ id })\n    .getOne();\n  return records;\n}\n\nexport async function getMentorByUserId(courseId: number, userId: number): Promise<{ id: number } | null> {\n  const record = await mentorQuery()\n    .where('mentor.\"userId\" = :userId', { userId })\n    .andWhere('mentor.\"courseId\" = :courseId', { courseId })\n    .getOne();\n  return record ?? null;\n}\n\nexport async function expelMentor(courseId: number, githubId: string) {\n  const mentor = await queryMentorByGithubId(courseId, githubId);\n  if (mentor) {\n    await getRepository(Student).update({ mentorId: mentor.id }, { mentorId: null });\n    await getRepository(Mentor).update(mentor.id, { isExpelled: true });\n    await getCustomRepository(StageInterviewRepository).cancelByMentor(courseId, githubId);\n  }\n}\n\nexport async function restoreMentor(courseId: number, githubId: string) {\n  const mentor = await queryMentorByGithubId(courseId, githubId);\n  if (mentor) {\n    await getRepository(Mentor).update(mentor.id, { isExpelled: false });\n  }\n}\n\nexport async function getMentorByGithubId(courseId: number, githubId: string): Promise<MentorBasic | null> {\n  const record = await mentorQuery()\n    .innerJoin('mentor.user', 'user')\n    .addSelect(getPrimaryUserFields())\n    .where('user.githubId = :githubId', { githubId })\n    .andWhere('mentor.\"courseId\" = :courseId', { courseId })\n    .getOne();\n  if (record == null) {\n    return null;\n  }\n  return convertToMentorBasic(record);\n}\n\nexport async function getStudentByGithubId(courseId: number, githubId: string): Promise<Student | null> {\n  const record = await studentQuery()\n    .innerJoin('student.user', 'user')\n    .where('user.githubId = :githubId', { githubId })\n    .andWhere('student.courseId = :courseId', { courseId })\n    .getOne();\n  if (record == null) {\n    return null;\n  }\n  return record;\n}\n\nexport async function queryStudentById(\n  courseId: number,\n  id: number,\n): Promise<{ id: number; name: string; githubId: string; userId: number } | null> {\n  const record = await studentQuery()\n    .innerJoin('student.user', 'user')\n    .addSelect(['user.firstName', 'user.lastName', 'user.githubId', 'user.id'])\n    .where('student.id = :id', { id })\n    .andWhere('student.courseId = :courseId', { courseId })\n    .getOne();\n\n  if (record == null) {\n    return null;\n  }\n\n  return { id: record.id, name: createName(record.user), githubId: record.user.githubId, userId: record.user.id };\n}\n\nexport async function queryStudentByGithubId(\n  courseId: number,\n  githubId: string,\n): Promise<{ id: number; name: string; githubId: string; userId: number } | null> {\n  const record = await studentQuery()\n    .innerJoin('student.user', 'user')\n    .addSelect(['user.firstName', 'user.lastName', 'user.githubId', 'user.id'])\n    .where('user.githubId = :githubId', { githubId })\n    .andWhere('student.courseId = :courseId', { courseId })\n    .getOne();\n  if (record == null) {\n    return null;\n  }\n  return { id: record.id, name: createName(record.user), githubId: record.user.githubId, userId: record.user.id };\n}\n\nexport async function queryMentorByGithubId(\n  courseId: number,\n  githubId: string,\n): Promise<{ id: number; name: string; githubId: string } | null> {\n  const record = await mentorQuery()\n    .innerJoin('mentor.user', 'user')\n    .addSelect(['user.firstName', 'user.lastName', 'user.githubId'])\n    .where('user.githubId = :githubId', { githubId })\n    .andWhere('mentor.courseId = :courseId', { courseId })\n    .getOne();\n  if (record == null) {\n    return null;\n  }\n  return { id: record.id, name: createName(record.user), githubId: record.user.githubId };\n}\n\nexport async function queryMentorById(\n  courseId: number,\n  id: number,\n): Promise<{ id: number; name: string; githubId: string } | null> {\n  const record = await mentorQuery()\n    .innerJoin('mentor.user', 'user')\n    .addSelect(['user.firstName', 'user.lastName', 'user.githubId'])\n    .where('mentor.id = :id', { id })\n    .andWhere('mentor.courseId = :courseId', { courseId })\n    .getOne();\n  if (record == null) {\n    return null;\n  }\n  return { id: record.id, name: createName(record.user), githubId: record.user.githubId };\n}\n\nexport async function getCrossStudentsByMentor(courseId: number, githubId: string) {\n  const records = await studentQuery()\n    .innerJoin('student.user', 'user')\n    .addSelect(getPrimaryUserFields())\n    .innerJoinAndSelect('student.taskChecker', 'taskChecker')\n    .innerJoinAndSelect('taskChecker.mentor', 'mentor')\n    .innerJoin('mentor.user', 'mentorUser')\n    .addSelect(getPrimaryUserFields('mentorUser'))\n    .where('mentorUser.githubId = :githubId', { githubId })\n    .andWhere('student.courseId = :courseId', { courseId })\n    .andWhere('student.isExpelled = false')\n    .getMany();\n\n  const students = records\n    .map<AssignedStudent[]>(record => {\n      const student = convertToStudentBasic(record);\n      student.mentor = record.mentor ? convertToMentorBasic(record.mentor) : null;\n      const checkers: TaskChecker[] = record.taskChecker ?? [];\n      return checkers.map(checker => ({ ...student, courseTaskId: checker?.courseTaskId })) ?? [];\n    })\n    .flat();\n\n  return students;\n}\n\nexport async function getMentorsWithStudents(courseId: number): Promise<MentorDetails[]> {\n  const records = await mentorQuery()\n    .innerJoin('mentor.user', 'user')\n    .addSelect(getPrimaryUserFields())\n    .leftJoinAndSelect('mentor.students', 'students')\n    .where(`mentor.courseId = :courseId`, { courseId })\n    .andWhere('mentor.isExpelled = false')\n    .orderBy('mentor.createdDate')\n    .getMany();\n\n  const mentors = records.map(convertToMentorDetails);\n  return mentors;\n}\n\nexport async function getMentorWithContacts(mentorId: number): Promise<MentorWithContacts> {\n  const record = (await mentorQuery()\n    .innerJoinAndSelect('mentor.user', 'user')\n    .where('mentor.id = :mentorId', { mentorId })\n    .getOne())!;\n  const mentor = convertToMentorBasic(record);\n  const user = record.user as User;\n  const mentorWithContacts: MentorWithContacts = {\n    ...mentor,\n    contactsEmail: user.contactsEmail,\n    contactsSkype: user.contactsSkype,\n    contactsWhatsApp: user.contactsWhatsApp,\n    contactsTelegram: user.contactsTelegram,\n    contactsNotes: user.contactsNotes,\n    contactsPhone: null,\n  };\n  return mentorWithContacts;\n}\n\nexport async function getStudentsWithDetails(courseId: number, activeOnly: boolean) {\n  const records = await studentQuery()\n    .innerJoin('student.user', 'user')\n    .addSelect(getPrimaryUserFields())\n    .innerJoin('student.course', 'course')\n    .leftJoinAndSelect('student.mentor', 'mentor', 'mentor.\"isExpelled\" = FALSE')\n    .leftJoin('mentor.user', 'mentorUser')\n    .leftJoin('student.stageInterviews', 'stageInterviews')\n    .leftJoin('student.taskChecker', 'taskChecker')\n    .leftJoin('taskChecker.courseTask', 'courseTask')\n    .leftJoin('courseTask.task', 'task')\n    .addSelect(getPrimaryUserFields('mentorUser'))\n    .addSelect(['stageInterviews.id', 'stageInterviews.isCompleted'])\n    .addSelect(['taskChecker.id', 'courseTask.id', 'task.id', 'task.name'])\n    .where(`course.id = :courseId ${activeOnly ? 'AND student.\"isExpelled\" = false' : ''}`, { courseId })\n    .orderBy('student.totalScore', 'DESC')\n    .getMany();\n\n  const students = records.map(convertToStudentDetails);\n  return students;\n}\n\nexport async function getStudents(courseId: number, activeOnly: boolean) {\n  const records = await studentQuery()\n    .innerJoin('student.user', 'user')\n    .addSelect(getPrimaryUserFields())\n    .innerJoin('student.course', 'course')\n    .leftJoinAndSelect('student.mentor', 'mentor')\n    .leftJoin('mentor.user', 'mentorUser')\n    .addSelect(getPrimaryUserFields('mentorUser'))\n    .where(`course.id = :courseId ${activeOnly ? 'AND student.\"isExpelled\" = false' : ''}`, { courseId })\n    .orderBy('student.totalScore', 'DESC')\n    .getMany();\n\n  const students = records.map(convertToStudentDetails);\n  return students;\n}\n\nexport async function getStudentScore(studentId: number) {\n  const student = await getRepository(Student)\n    .createQueryBuilder('student')\n    .leftJoinAndSelect('student.taskResults', 'taskResults')\n    .leftJoin('taskResults.courseTask', 'courseTask')\n    .addSelect(['courseTask.disabled', 'courseTask.id'])\n    .leftJoinAndSelect('student.taskInterviewResults', 'taskInterviewResults')\n    .leftJoin('student.stageInterviews', 'si')\n    .leftJoin('si.stageInterviewFeedbacks', 'sif')\n    .addSelect([\n      'sif.stageInterviewId',\n      'sif.json',\n      'si.isCompleted',\n      'si.id',\n      'si.courseTaskId',\n      'si.score',\n      'sif.version',\n    ])\n    .where('student.id = :studentId', { studentId })\n    .getOne();\n\n  if (!student) return null;\n\n  const { taskResults, taskInterviewResults, stageInterviews } = student;\n\n  const toTaskScore = ({ courseTaskId, score = 0 }: TaskResult | TaskInterviewResult) => ({ courseTaskId, score });\n\n  const results = [];\n\n  if (taskResults?.length) {\n    results.push(...(taskResults.filter(taskResult => !taskResult.courseTask.disabled).map(toTaskScore) ?? []));\n  }\n\n  if (taskInterviewResults?.length) {\n    results.push(...taskInterviewResults.map(toTaskScore));\n  }\n\n  // we have a case when technical screening score are set as task result.\n  if (stageInterviews?.length && !results.find(tr => tr.courseTaskId === stageInterviews[0].courseTaskId)) {\n    const feedbackVersion = stageInterviews[0].stageInterviewFeedbacks[0]?.version;\n    const score = !feedbackVersion\n      ? Math.floor(getStageInterviewRating(stageInterviews) ?? 0)\n      : stageInterviews[0].score;\n\n    results.push({\n      score,\n      courseTaskId: stageInterviews[0].courseTaskId,\n    });\n  }\n\n  return {\n    totalScore: student.totalScore ?? 0,\n    rank: student.rank ?? 999999,\n    results,\n  };\n}\n\nexport async function getCourseTask(taskId: number) {\n  const courseTasks = await getRepository(CourseTask)\n    .createQueryBuilder('courseTask')\n    .innerJoinAndSelect('courseTask.task', 'task')\n    .where('courseTask.id = :id', { id: taskId })\n    .andWhere('courseTask.disabled = :disabled', { disabled: false })\n    .getMany();\n  return courseTasks;\n}\n\nexport async function getCourseTasks(courseId: number) {\n  const courseTasks = await getRepository(CourseTask)\n    .createQueryBuilder('courseTask')\n    .innerJoinAndSelect('courseTask.task', 'task')\n    .where('courseTask.courseId = :courseId', { courseId })\n    .andWhere('courseTask.disabled = :disabled', { disabled: false })\n    .getMany();\n  return courseTasks;\n}\n\nexport async function getCourseTasksWithOwner(courseId: number) {\n  const courseTasks = await getRepository(CourseTask)\n    .createQueryBuilder('courseTask')\n    .innerJoinAndSelect('courseTask.task', 'task')\n    .leftJoin('courseTask.taskOwner', 'taskOwner')\n    .addSelect(['taskOwner.githubId', 'taskOwner.id', 'taskOwner.firstName', 'taskOwner.lastName'])\n    .where('courseTask.courseId = :courseId', { courseId })\n    .andWhere('courseTask.disabled = :disabled', { disabled: false })\n    .getMany();\n  return courseTasks;\n}\n\nexport async function updateScoreStudents(data: { id: number; totalScore: number }[]) {\n  const chuncks = _.chunk(data, 500);\n\n  // update score in chunks with delays.\n  for (const chunck of chuncks) {\n    await getRepository(Student).save(chunck);\n    await timeout(10000);\n  }\n}\n\nexport function isPowerUser(courseId: number, session: IUserSession) {\n  return isAdmin(session) || isManager(session, courseId) || isSupervisor(session, courseId);\n}\n\nexport async function getEvent(eventId: number) {\n  const answer = await getRepository(CourseEvent)\n    .createQueryBuilder('courseEvent')\n    .innerJoinAndSelect('courseEvent.event', 'event')\n    .leftJoin('courseEvent.organizer', 'organizer')\n    .addSelect(['organizer.id', 'organizer.firstName', 'organizer.lastName', 'organizer.githubId'])\n    .where('courseEvent.id = :id', { id: eventId })\n    .getOne();\n  return answer;\n}\n\nexport async function getEvents(courseId: number) {\n  return getRepository(CourseEvent)\n    .createQueryBuilder('courseEvent')\n    .innerJoinAndSelect('courseEvent.event', 'event')\n    .leftJoin('courseEvent.organizer', 'organizer')\n    .addSelect(['organizer.id', 'organizer.firstName', 'organizer.lastName', 'organizer.githubId'])\n    .where('courseEvent.courseId = :courseId', { courseId })\n    .orderBy('courseEvent.dateTime')\n    .getMany();\n}\n\nexport async function getTaskSolutionsWithoutChecker(courseTaskId: number) {\n  const records = await getRepository(TaskSolution)\n    .createQueryBuilder('ts')\n    .leftJoin('student', 's', 's.\"id\" = ts.studentId')\n    .leftJoin('task_solution_checker', 'tsc', 'tsc.\"taskSolutionId\" = ts.id')\n    .where(`ts.\"courseTaskId\" = :courseTaskId`, { courseTaskId })\n    .andWhere('tsc.id IS NULL')\n    .andWhere('s.isExpelled = false')\n    .getMany();\n  return records;\n}\n\nexport async function getUsers(courseId: number) {\n  const records = await getRepository(CourseUser)\n    .createQueryBuilder('courseUser')\n    .innerJoin('courseUser.user', 'user')\n    .addSelect(getPrimaryUserFields())\n    .where(`\"courseUser\".\"courseId\" = :courseId`, { courseId })\n    .getMany();\n\n  return records.map(r => ({\n    courseId: r.courseId,\n    id: r.userId,\n    name: createName(r.user),\n    githubId: r.user.githubId,\n    isManager: r.isManager,\n    isSupervisor: r.isSupervisor,\n    isDementor: r.isDementor,\n  }));\n}\n\nexport async function getTaskSolutionCheckers(courseTaskId: number, minCheckedCount: number) {\n  const query = getManager()\n    .createQueryBuilder()\n    .select(['ROUND(AVG(\"score\")) as \"score\"', '\"studentId\" '])\n    .from(qb => {\n      // do sub query to select only top X scores\n      const query = qb\n        .from(TaskSolutionResult, 'tsr')\n        .select([\n          'tsr.studentId as \"studentId\"',\n          'tsr.score as \"score\"',\n          'row_number() OVER (PARTITION by tsr.studentId ORDER BY tsr.score desc) as \"rownum\"',\n        ])\n        .where(qb => {\n          // query students who checked enough tasks\n          const query = qb\n            .subQuery()\n            .select('r.\"checkerId\"')\n            .from(TaskSolutionChecker, 'c')\n            .leftJoin(\n              'task_solution_result',\n              'r',\n              ['r.\"checkerId\" = c.\"checkerId\"', 'r.\"studentId\" = c.\"studentId\"'].join(' AND '),\n            )\n            .where(`c.\"courseTaskId\" = :courseTaskId`, { courseTaskId })\n            .andWhere('r.id IS NOT NULL')\n            .groupBy('r.\"checkerId\"')\n            .having(`COUNT(c.id) >= :count`, { count: minCheckedCount })\n            .getQuery();\n          return `\"studentId\" IN ${query}`;\n        })\n        .andWhere('tsr.\"courseTaskId\" = :courseTaskId', { courseTaskId })\n        .orderBy('tsr.studentId')\n        .orderBy('tsr.score', 'DESC');\n      return query;\n    }, 's')\n    .where('rownum <= :count', { count: minCheckedCount })\n    .groupBy('\"studentId\"');\n\n  const records = await query.getRawMany();\n\n  return records.map(record => ({ studentId: record.studentId, score: Number(record.score) }));\n}\n\nexport async function getInterviewStudentsByMentorId(courseTaskId: number, mentorId: number) {\n  const records = await getRepository(Student)\n    .createQueryBuilder('student')\n    .innerJoin('student.user', 'user')\n    .innerJoin('student.taskChecker', 'taskChecker')\n    .addSelect(getPrimaryUserFields())\n    .where('\"taskChecker\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n    .andWhere('\"taskChecker\".\"mentorId\" = :mentorId', { mentorId })\n    .getMany();\n\n  const students = records.map(record => convertToStudentBasic(record));\n  return students;\n}\n\nexport type StudentCrossMentor = {\n  name: string;\n  mentor: {\n    githubId: string;\n    cityName?: string;\n    contactsPhone?: string | null;\n    contactsTelegram?: string | null;\n    contactsSkype?: string | null;\n    contactsNotes?: string | null;\n    contactsEmail?: string | null;\n  };\n};\n\nexport async function getCrossMentorsByStudent(courseId: number, githubId: string): Promise<StudentCrossMentor[]> {\n  const student = await getStudentByGithubId(courseId, githubId);\n\n  if (student == null) {\n    return [];\n  }\n  const taskCheckers = await getRepository(TaskChecker)\n    .createQueryBuilder('taskChecker')\n    .innerJoin('taskChecker.courseTask', 'courseTask')\n    .innerJoin('courseTask.task', 'task')\n    .innerJoin('taskChecker.mentor', 'mentor')\n    .innerJoin('mentor.user', 'user')\n    .addSelect([\n      'courseTask.id',\n      'courseTask.studentEndDate',\n      'mentor.id',\n      'task.id',\n      'task.name',\n      'user.primaryEmail',\n      'user.contactsNotes',\n      'user.contactsPhone',\n      'user.contactsSkype',\n      'user.contactsTelegram',\n      ...getPrimaryUserFields('user'),\n    ])\n    .where('\"taskChecker\".\"studentId\" = :studentId', { studentId: student.id })\n    .andWhere('task.type <> :type', { type: 'interview' })\n    .getMany();\n\n  if (taskCheckers.length === 0) {\n    return [];\n  }\n\n  const students = taskCheckers.map<StudentCrossMentor>(record => {\n    const { githubId, primaryEmail, contactsNotes, contactsPhone, contactsSkype, contactsTelegram, cityName } =\n      record.mentor.user;\n    return {\n      name: record.courseTask.task.name,\n      mentor: {\n        githubId,\n        primaryEmail,\n        contactsNotes,\n        contactsPhone,\n        contactsSkype,\n        contactsTelegram,\n        cityName: cityName ?? undefined,\n      },\n    };\n  });\n  return students;\n}\n\nconst timeout = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));\n"
  },
  {
    "path": "server/src/services/crossCheck.service.ts",
    "content": "import { getCustomRepository, getRepository } from 'typeorm';\nimport { TaskSolution, CourseTask, TaskSolutionResult, IUserSession } from '../models';\nimport { TaskSolutionComment, TaskSolutionReview } from '../models/taskSolution';\nimport { CrossCheckMessage, CrossCheckMessageAuthorRole } from '../models/taskSolutionResult';\nimport { Discord } from '../../../common/models';\nimport { getTaskSolution, getTaskSolutionResult, getTaskSolutionResultById } from './taskResults.service';\nimport { getCourseTask } from './tasks.service';\nimport { queryStudentByGithubId } from './course.service';\nimport { createName, getUserByGithubId } from './user.service';\nimport { CrossCheckRepository } from '../repositories/crossCheck.repository';\nimport { UserRepository } from '../repositories/user.repository';\nimport { CrossCheckCriteriaData } from '../models/taskSolutionResult';\n\nexport interface CrossCheckSolution {\n  url: string;\n  review?: TaskSolutionReview[];\n  comments: TaskSolutionComment[];\n}\n\nexport interface CrossCheckReviewResult extends CrossCheckSubmitResult {\n  id: number;\n  checkerId: number;\n  studentId: number;\n}\n\nexport interface CrossCheckSubmitResult {\n  score: number;\n  anonymous?: boolean;\n  comment: string;\n  review?: TaskSolutionReview[];\n}\n\nexport class CrossCheckService {\n  constructor(\n    private courseTaskId: number,\n    private repository = getCustomRepository(CrossCheckRepository),\n  ) {}\n\n  public static isCrossCheckTask(courseTask: Partial<CourseTask>) {\n    return courseTask.checker === 'crossCheck';\n  }\n\n  public static isValidTaskSolution(data: Partial<TaskSolution>) {\n    if (!data.url) {\n      return false;\n    }\n    if (data.comments && !Array.isArray(data.comments)) {\n      return false;\n    }\n    if (data.review && !Array.isArray(data.review)) {\n      return false;\n    }\n    return true;\n  }\n\n  public async getStudentAndTask(courseId: number, githubId: string) {\n    const [student, courseTask] = await Promise.all([\n      queryStudentByGithubId(courseId, githubId),\n      getCourseTask(this.courseTaskId),\n    ]);\n    return { student, courseTask };\n  }\n\n  public async getTaskDetails() {\n    const courseTask = await getCourseTask(this.courseTaskId);\n    const studentEndDate = courseTask?.studentEndDate;\n    const criteria = courseTask?.task?.attributes?.criteria ?? [];\n    return { criteria, studentEndDate };\n  }\n\n  public async saveSolution(studentId: number, data: Partial<TaskSolution>) {\n    const existingResult = await getTaskSolution(studentId, this.courseTaskId);\n    if (existingResult != null) {\n      await getRepository(TaskSolution).save({\n        ...existingResult,\n        ...data,\n        comments: existingResult.comments.concat(data.comments ?? []),\n      });\n      return;\n    }\n\n    await getRepository(TaskSolution).save({\n      studentId,\n      courseTaskId: this.courseTaskId,\n      url: data.url,\n      review: data.review,\n      comments: data.comments,\n    });\n  }\n\n  public async deleteSolution(studentId: number) {\n    await getRepository(TaskSolution).delete({ studentId, courseTaskId: this.courseTaskId });\n  }\n\n  public async saveSolutionComments(\n    studentId: number,\n    courseTaskId: number,\n    data: {\n      comments: TaskSolutionComment[];\n      authorId: number;\n      authorGithubId: string;\n      recipientId?: number;\n    },\n  ) {\n    const taskSolution = await getTaskSolution(studentId, courseTaskId);\n    if (taskSolution == null) {\n      throw new Error(`Cross check solution not found StudentId=[${studentId} CourseTask=[${courseTaskId}]`);\n    }\n    const comments = data.comments.reduce((acc, comment) => {\n      if (acc.some(c => c.criteriaId === comment.criteriaId && c.timestamp === comment.timestamp)) {\n        return acc;\n      }\n      return acc.concat([{ ...comment, authorId: data.authorId, recipientId: data.recipientId }]);\n    }, taskSolution.comments);\n\n    await getRepository(TaskSolution).save({ id: taskSolution.id, comments });\n  }\n\n  public async saveResult(\n    studentId: number,\n    checkerId: number,\n    data: CrossCheckSubmitResult,\n    params: { userId: number; criteria: CrossCheckCriteriaData[] },\n  ) {\n    const { userId } = params;\n    const historicalResult = { ...data, criteria: params.criteria, authorId: userId, dateTime: Date.now() };\n\n    const repository = getRepository(TaskSolutionResult);\n    const existing = await getTaskSolutionResult(studentId, checkerId, this.courseTaskId);\n\n    if (existing != null) {\n      const { historicalScores } = existing;\n      const previousScore = { ...existing };\n      historicalScores.push(historicalResult);\n      await repository.update(existing.id, { ...data, historicalScores });\n      if (previousScore.comment !== data.comment || previousScore.score !== data.score) {\n        return previousScore;\n      }\n    } else {\n      await repository.insert({\n        studentId: studentId,\n        checkerId: checkerId,\n        courseTaskId: this.courseTaskId,\n        historicalScores: [historicalResult],\n        messages: [],\n        ...data,\n      });\n    }\n  }\n\n  public async getResult(\n    studentId: number,\n    checkerId: number,\n    checkerGithubId: string,\n  ): Promise<\n    | (CrossCheckReviewResult & {\n        author: {\n          id: number;\n          name: string;\n          discord: Discord | null;\n          githubId: string;\n        };\n        comments?: TaskSolutionComment[];\n        historicalScores: TaskSolutionResult['historicalScores'];\n        messages: CrossCheckMessage[];\n      })\n    | null\n  > {\n    const [reviewResult, solution] = await Promise.all([\n      this.repository.findReviewResult(this.courseTaskId, studentId, checkerId),\n      this.repository.findSolution(this.courseTaskId, studentId),\n    ]);\n    if (reviewResult == null || solution == null) {\n      return null;\n    }\n    let comments =\n      solution.comments\n        ?.filter(c => c.recipientId == null || c.authorId === checkerId || c.recipientId === checkerId)\n        .map(c => ({\n          text: c.text,\n          timestamp: c.timestamp,\n          criteriaId: c.criteriaId,\n          authorId: c.authorId,\n        })) ?? [];\n\n    const data = await getCustomRepository(UserRepository).findByStudentIds(\n      comments.map(c => c.authorId).filter(c => c),\n    );\n\n    const checkerData = await getUserByGithubId(checkerGithubId);\n\n    comments = comments.map(c => ({\n      ...c,\n      authorGithubId:\n        !reviewResult.anonymous || c.authorId === solution.studentId || c.authorId === checkerId\n          ? data.find(d => d.studentId === c.authorId)?.githubId\n          : null,\n    }));\n    return {\n      id: reviewResult.id,\n      score: reviewResult.score,\n      comment: reviewResult.comment ?? '',\n      anonymous: reviewResult.anonymous,\n      review: reviewResult.review,\n      checkerId,\n      studentId,\n      author: {\n        id: checkerData?.id ?? 0,\n        name: createName({\n          firstName: checkerData?.firstName ?? '',\n          lastName: checkerData?.lastName ?? '',\n        }),\n        githubId: checkerGithubId,\n        discord: checkerData?.discord ?? null,\n      },\n      comments,\n      historicalScores: reviewResult.historicalScores ?? [],\n      messages: reviewResult.messages,\n    };\n  }\n\n  public async saveMessage(\n    taskSolutionResultId: number,\n    data: { content: string; role: CrossCheckMessageAuthorRole },\n    params: { user: IUserSession },\n  ) {\n    const { user } = params;\n\n    const message: CrossCheckMessage = {\n      ...data,\n      timestamp: new Date().toISOString(),\n      author: {\n        id: user.id,\n        githubId: user.githubId,\n      },\n      isReviewerRead: data.role === CrossCheckMessageAuthorRole.Reviewer,\n      isStudentRead: data.role === CrossCheckMessageAuthorRole.Student,\n    };\n\n    const repository = getRepository(TaskSolutionResult);\n    const taskSolutionResultById = await getTaskSolutionResultById(taskSolutionResultId);\n\n    if (taskSolutionResultById) {\n      const { messages } = taskSolutionResultById;\n\n      messages.push(message);\n      await repository.update(taskSolutionResultById.id, { messages });\n    }\n  }\n\n  public async updateMessage(taskSolutionResultId: number, data: { role: CrossCheckMessageAuthorRole }) {\n    const { role } = data;\n\n    const repository = getRepository(TaskSolutionResult);\n    const taskSolutionResultById = await getTaskSolutionResultById(taskSolutionResultId);\n\n    if (taskSolutionResultById) {\n      const { messages } = taskSolutionResultById;\n\n      const updatedMessages = messages.map(message => ({\n        ...message,\n        isReviewerRead: CrossCheckMessageAuthorRole.Reviewer === role ? true : message.isReviewerRead,\n        isStudentRead: CrossCheckMessageAuthorRole.Student === role ? true : message.isStudentRead,\n      }));\n\n      await repository.update(taskSolutionResultById.id, { messages: updatedMessages });\n    }\n  }\n}\n"
  },
  {
    "path": "server/src/services/distribution/__tests__/crossCheck.test.ts",
    "content": "import { CrossCheckDistributionService } from '../crossCheckDistribution.service';\nimport { uniq } from 'lodash';\n\nconst persons = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];\nconst pairsCountPerPerson = 3;\n\ndescribe('cross check distribution', () => {\n  it('should return correct amount of pairs', () => {\n    const service = new CrossCheckDistributionService();\n    const result = service.distribute(persons, pairsCountPerPerson);\n    expect(result.length).toBe(persons.length * pairsCountPerPerson);\n  });\n\n  it('should return only uniq pairs', () => {\n    const service = new CrossCheckDistributionService();\n    const result = service.distribute(persons, pairsCountPerPerson);\n    const pairsAsStrings = result.map(it => `${it.checkerId}|${it.studentId}`);\n    const hasDuplicates = uniq(pairsAsStrings).length !== result.length;\n    expect(hasDuplicates).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "server/src/services/distribution/__tests__/crossMentor.test.ts",
    "content": "import { CrossMentorDistributionService, CrossMentor } from '../crossMentorDistribution.service';\n\nconst mentors: CrossMentor[] = [\n  { id: 1, students: [] },\n  { id: 2, students: [{ id: 99 }, { id: 98 }] },\n  { id: 3, students: [{ id: 97 }, { id: 96 }] },\n  { id: 4, students: [{ id: 95 }] },\n  { id: 5, students: [{ id: 94 }, { id: 93 }, { id: 92 }, { id: 91 }] },\n  { id: 6, students: [{ id: 90 }, { id: 89 }] },\n  { id: 7, students: null } as any,\n];\n\nconst tooManyMentors = [\n  { id: 1, students: [{ id: 99 }, { id: 94 }] },\n  { id: 2, students: [{ id: 98 }] },\n  { id: 3, students: [{ id: 97 }, { id: 96 }] },\n  { id: 4, students: [] },\n  { id: 5, students: [{ id: 93 }, { id: 95 }] },\n];\n\ndescribe('cross mentor distribution', () => {\n  let service: CrossMentorDistributionService;\n\n  beforeEach(() => {\n    service = new CrossMentorDistributionService();\n  });\n\n  it('should return correct amount of mentors', () => {\n    const result = service.distribute(mentors, []);\n    expect(result.mentors.length).toBe(mentors.length);\n  });\n\n  it('should return 0 unassigned students', () => {\n    const result = service.distribute(mentors, []);\n    const combineStudents = (acc: any[], m: CrossMentor) => acc.concat(m.students ?? []);\n    const studentsBefore = mentors.reduce(combineStudents, []);\n    const studentsAfter = result.mentors.reduce(combineStudents, []);\n    expect(result.unassignedStudents.length).toBe(0);\n    expect(studentsBefore.length).toBe(studentsAfter.length);\n  });\n\n  it('should assigned students to mentor if it has active students', () => {\n    const result = service.distribute(mentors, []);\n    const hasMentorsWithoutStudents = mentors\n      .filter(m => m.students?.length ?? 0)\n      .some(mentor => (result.mentors.find(m => m.id === mentor.id)?.students?.length ?? 0) > 0);\n    expect(hasMentorsWithoutStudents).toBeTruthy();\n  });\n\n  describe('should distribute students among all mentors', () => {\n    let mentors: CrossMentor[] = [];\n    beforeAll(() => {\n      service = new CrossMentorDistributionService();\n      const result = service.distribute(tooManyMentors, [], [99, 98, 97, 96, 95, 94]);\n      mentors = result.mentors;\n    });\n\n    const cases = [\n      [0, 2],\n      [1, 1],\n      [2, 2],\n      [3, 0],\n      [4, 1],\n    ];\n\n    it.each(cases)('mentor index %s', (index, studentsCount) => {\n      expect(mentors[index].students?.length).toEqual(studentsCount);\n    });\n  });\n});\n"
  },
  {
    "path": "server/src/services/distribution/__tests__/shuffle.test.ts",
    "content": "import { shuffleRec, isShuffledArrays } from '../shuffle';\n\ntest('shuffle', () => {\n  for (let j = 0; j < 100; j++) {\n    const a = ['A', 'B', 'C', 'D', 'E'];\n    const b = shuffleRec(a);\n    expect(b).toEqual(expect.arrayContaining(a));\n    // tslint:disable-next-line:prefer-for-of\n    for (let i = 0; i < a.length; i++) {\n      expect(b[i]).not.toEqual(a[i]);\n    }\n  }\n});\n\ntest('isShuffledArrays', () => {\n  expect(isShuffledArrays(['A', 'B', 'C', 'D', 'E'], ['A', 'B', 'C', 'D', 'E'])).toEqual(false);\n  expect(isShuffledArrays(['A', 'B', 'C', 'D'], ['A', 'B', 'C', 'D', 'E'])).toEqual(false);\n  expect(isShuffledArrays(['A', 'B', 'C', 'K'], ['A', 'B', 'C', 'D', 'E'])).toEqual(false);\n  expect(isShuffledArrays(['B', 'A', 'D', 'C'], ['A', 'B', 'C', 'D', 'E'])).toEqual(true);\n});\n"
  },
  {
    "path": "server/src/services/distribution/crossCheckDistribution.service.ts",
    "content": "type CrossCheckPair = {\n  checkerId: number;\n  studentId: number;\n};\n\nexport class CrossCheckDistributionService {\n  constructor(private defaultCheckersNumber = 4) {}\n\n  public distribute(studentIds: number[], checkersNumber = this.defaultCheckersNumber): CrossCheckPair[] {\n    const pairs: CrossCheckPair[] = [];\n    const shuffledStudentsIds = this.shuffle(studentIds);\n    const studentsNumber = studentIds.length;\n    const shifts = this.createShifts(checkersNumber, Math.floor((studentsNumber - 1) / 2));\n\n    shuffledStudentsIds.forEach((studentId, index) => {\n      for (let i = 0; i < checkersNumber; i++) {\n        const checkerId = shuffledStudentsIds[(index + shifts[i]) % studentsNumber];\n        pairs.push({ checkerId, studentId });\n      }\n    });\n    return pairs;\n  }\n\n  private randomInteger(min: number, max: number): number {\n    return Math.round(min - 0.5 + Math.random() * (max - min + 1));\n  }\n\n  private shuffle<T>(array: T[]): T[] {\n    const arrcopy = [...array];\n\n    for (let i = arrcopy.length - 1; i > 0; i -= 1) {\n      const j = Math.floor(Math.random() * (i + 1));\n      [arrcopy[i], arrcopy[j]] = [arrcopy[j], arrcopy[i]];\n    }\n    return arrcopy;\n  }\n\n  private createShifts(numberOfShifts: number, maxShiftValue: number): number[] {\n    if (numberOfShifts > maxShiftValue) {\n      throw new Error(\n        `It is impossible to distribute crosscheckers. NumberOfShifts: ${numberOfShifts}. MaxShiftValue: ${maxShiftValue}`,\n      );\n    }\n\n    const shifts = new Set<number>();\n\n    while (shifts.size < numberOfShifts) {\n      shifts.add(this.randomInteger(1, maxShiftValue));\n    }\n    return Array.from(shifts);\n  }\n}\n"
  },
  {
    "path": "server/src/services/distribution/crossMentorDistribution.service.ts",
    "content": "import { max } from 'lodash';\nimport { ILogger } from '../../logger';\nimport { shuffleRec } from './shuffle';\n\nexport type CrossMentor = { id: number; students: { id: number }[] | null };\n\nexport class CrossMentorDistributionService {\n  constructor(\n    private defaultMaxStudents = 1,\n    private logger?: ILogger,\n  ) {}\n\n  public distribute(\n    mentors: CrossMentor[],\n    existingPairs: { studentId: number; mentorId: number }[],\n    registeredStudentsIds?: number[],\n  ) {\n    let students = mentors\n      .map(m => m.students ?? [])\n      .reduce((acc, v) => acc.concat(v), [] as { id: number }[])\n      .filter(v => !existingPairs.find(p => p.studentId === v.id))\n      .filter(v => registeredStudentsIds?.includes(v.id) ?? true);\n\n    this.logger?.info(`Initial students: ${students.length}`);\n\n    const maxStudentsPerMentor = mentors.map(({ id, students }) => {\n      const assignedCount = existingPairs.filter(p => p.mentorId === id).length;\n      const maxStudentsCount = Math.max((students?.length ?? 0) - assignedCount, 0);\n      return { id, maxStudents: maxStudentsCount };\n    });\n\n    const maxStudentsTotal = maxStudentsPerMentor.reduce((acc, m) => acc + m.maxStudents, 0);\n\n    if (students.length < maxStudentsTotal && registeredStudentsIds) {\n      students = students.concat(\n        registeredStudentsIds\n          .filter(id => !existingPairs.find(p => p.studentId === id) && !students.find(st => st.id === id))\n          .slice(0, maxStudentsTotal - students.length)\n          .map(id => ({ id })),\n      );\n    }\n\n    const randomStudents = students.length > 1 ? shuffleRec(students) : students;\n\n    // distribute students to mentors by round robin\n    const maxStudentsMap = maxStudentsPerMentor.reduce(\n      (acc, m) => {\n        acc[m.id] = m.maxStudents;\n        return acc;\n      },\n      {} as Record<number, number>,\n    );\n\n    this.logger?.info(`Registered Students ${registeredStudentsIds?.length}. Max Students: ${maxStudentsTotal}`);\n    this.logger?.info(`Selected Students: ${randomStudents.length}`);\n    this.logger?.info(`Mentors Count: ${mentors.length}`);\n\n    if (registeredStudentsIds && randomStudents.length < maxStudentsTotal) {\n      const filteredMentors = mentors.filter(m => (maxStudentsMap[m.id] ?? this.defaultMaxStudents) > 0);\n      const maxStudentsPerMentor = max(filteredMentors.map(m => maxStudentsMap[m.id] ?? 0)) ?? 0;\n      const mentorsQueue: number[] = [];\n      for (let i = 0; i < maxStudentsPerMentor; i++) {\n        filteredMentors.forEach((mentor, idx) => {\n          const student = mentor.students?.[i];\n          if (student) {\n            mentorsQueue.push(idx);\n          }\n        });\n      }\n      mentorsQueue.reverse();\n\n      // nullify students for mentors\n      for (const mentor of mentors) {\n        mentor.students = [];\n      }\n      randomStudents.forEach(student => {\n        const mentorIdx = mentorsQueue.pop();\n        if (mentorIdx == null) return;\n        const mentor = filteredMentors[mentorIdx];\n        mentor.students = mentor.students ? mentor.students.concat([student]) : [student];\n      });\n    } else {\n      for (const mentor of mentors) {\n        const maxStudents = maxStudentsMap[mentor.id] ?? this.defaultMaxStudents;\n        const students = randomStudents.splice(0, maxStudents);\n        mentor.students = students;\n      }\n    }\n\n    const distributedStudents = mentors.reduce((acc, m) => acc.concat(m.students ?? []), [] as { id: number }[]);\n    const mentorsWithStudents = mentors.filter(m => (m.students?.length ?? 0) > 0);\n    this.logger?.info(`Distributed students: ${distributedStudents.length}`);\n    this.logger?.info(`Mentors with students: ${mentorsWithStudents.length}`);\n\n    return {\n      mentors,\n      unassignedStudents: randomStudents,\n    };\n  }\n}\n"
  },
  {
    "path": "server/src/services/distribution/index.ts",
    "content": "export * from './crossCheckDistribution.service';\nexport * from './crossMentorDistribution.service';\n"
  },
  {
    "path": "server/src/services/distribution/shuffle.ts",
    "content": "import { randomBytes } from 'crypto';\n\nclass Generator {\n  buff: Uint8Array;\n  constructor(size: number) {\n    this.buff = new Uint8Array(size);\n    const bytes = randomBytes(this.buff.length);\n    this.buff.set(bytes);\n  }\n\n  next(n: number) {\n    return this.buff[n - 1] % n;\n  }\n}\n\nexport function isShuffledArrays(a: any[], b: any[]): boolean {\n  const len = Math.min(a.length, b.length);\n\n  for (let i = 0; i < len; i++) {\n    if (a[i] === b[i]) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nfunction shuffle(arr: any[]): any[] {\n  const copy = [...arr];\n\n  let n = copy.length;\n  const generator = new Generator(n);\n\n  while (n > 1) {\n    const randomIndex = generator.next(n--);\n    const el = copy[n];\n\n    copy[n] = copy[randomIndex];\n    copy[randomIndex] = el;\n  }\n\n  return copy;\n}\n\nexport function shuffleRec<T = any>(arr: T[]): T[] {\n  const res = shuffle(arr);\n  return isShuffledArrays(arr, res) ? res : shuffleRec(arr);\n}\n"
  },
  {
    "path": "server/src/services/github.service.ts",
    "content": "import { config } from '../config';\nimport { App, Octokit } from 'octokit';\n\nconst { appId, privateKey } = config.github;\nconst app = appId && privateKey ? new App({ appId: Number(appId), privateKey }) : null;\n\nexport class GithubService {\n  public static async initGithub(): Promise<Octokit> {\n    const { installationId } = config.github;\n    if (!app) {\n      throw new Error('GitHub App is not configured');\n    }\n    const octokit = await app.getInstallationOctokit(Number(installationId));\n    return octokit;\n  }\n}\n"
  },
  {
    "path": "server/src/services/index.ts",
    "content": "import * as userService from './user.service';\nimport * as courseService from './course.service';\nimport * as taskResultsService from './taskResults.service';\nimport * as taskService from './tasks.service';\nimport * as awsTaskService from './aws.service';\nimport * as stageInterviewService from './stageInterview.service';\nimport * as notificationService from './notification.service';\nimport * as studentService from './student.service';\n\nexport { InterviewService } from './interview.service';\nexport { RepositoryService } from './repository.service';\nexport { CrossCheckService } from './crossCheck.service';\n\nexport * from './operationResult';\nexport {\n  studentService,\n  userService,\n  taskService,\n  courseService,\n  taskResultsService,\n  awsTaskService,\n  stageInterviewService,\n  notificationService,\n};\n"
  },
  {
    "path": "server/src/services/interview.service.ts",
    "content": "import moment from 'moment-timezone';\nimport { getCustomRepository, getRepository } from 'typeorm';\nimport { CourseTask, TaskChecker } from '../models';\nimport { MentorRepository } from '../repositories/mentor.repository';\nimport { CrossMentorDistributionService } from '../services/distribution';\nimport { InterviewRepository } from '../repositories/interview.repository';\nimport { ILogger } from '../logger';\n\nexport class InterviewService {\n  private interviewRepository = getCustomRepository(InterviewRepository);\n  private crossMentorService: CrossMentorDistributionService;\n\n  constructor(\n    private courseId: number,\n    logger?: ILogger,\n  ) {\n    this.crossMentorService = new CrossMentorDistributionService(undefined, logger);\n  }\n\n  public async createInterviewsAutomatically(\n    courseTaskId: number,\n    options: { clean: boolean; registrationEnabled: boolean },\n  ) {\n    const courseTask = await getRepository(CourseTask).findOne({ where: { id: courseTaskId }, select: ['id'] });\n\n    if (courseTask == null) {\n      return null;\n    }\n\n    const mentorRepository = getCustomRepository(MentorRepository);\n    const mentors = await mentorRepository.findActive(this.courseId, true);\n\n    if (mentors.length === 0) {\n      return [];\n    }\n\n    const checkerRepository = getRepository(TaskChecker);\n\n    if (options.clean) {\n      await checkerRepository.delete({ courseTaskId });\n    }\n\n    let registeredStudentsIds: number[] | undefined = undefined;\n    if (options.registrationEnabled) {\n      const student = await this.interviewRepository.findRegisteredStudents(this.courseId, courseTaskId);\n      registeredStudentsIds = student.map(student => student.id);\n    }\n\n    const existingPairs = await checkerRepository.findBy({ courseTaskId });\n\n    const { mentors: crossMentors } = this.crossMentorService.distribute(mentors, existingPairs, registeredStudentsIds);\n\n    const taskCheckPairs = crossMentors\n      .map(stm => stm.students?.map(s => ({ courseTaskId, mentorId: stm.id, studentId: s.id })) ?? [])\n      .reduce((acc, student) => acc.concat(student), []);\n\n    if (taskCheckPairs.length > 0) {\n      await checkerRepository.insert(taskCheckPairs);\n    }\n\n    return taskCheckPairs;\n  }\n\n  public async cancelInterviewPair(pairId: number) {\n    return this.interviewRepository.cancelById(pairId);\n  }\n\n  public async createInterview(courseTaskId: number, interviewerGithubId: string, studentGithubId: string) {\n    return this.interviewRepository.addPair(this.courseId, courseTaskId, interviewerGithubId, studentGithubId);\n  }\n\n  public async isInterviewStarted(courseTaskId: number) {\n    const courseTask = await getRepository(CourseTask).findOne({\n      where: { id: courseTaskId },\n      select: ['studentStartDate'],\n    });\n    return moment(courseTask?.studentStartDate).isBefore(moment());\n  }\n}\n"
  },
  {
    "path": "server/src/services/notification.service.ts",
    "content": "import axios from 'axios';\nimport { config } from '../config';\nimport { NotificationId } from '../models/notification';\n\nexport async function sendNotification(notification: NotificationV2) {\n  if (config.isDevMode) return;\n\n  const { password, username } = config.users.cloud;\n\n  await axios.post(`${config.host}/api/v2/users/notifications/send`, notification, {\n    auth: {\n      username,\n      password,\n    },\n  });\n}\n\ntype NotificationV2 = {\n  notificationId: NotificationId;\n  userId: number;\n  data?: object;\n};\n"
  },
  {
    "path": "server/src/services/operationResult.ts",
    "content": "export interface OperationResult {\n  status: 'created' | 'updated' | 'deleted' | 'skipped' | 'failed';\n  value: any;\n}\n"
  },
  {
    "path": "server/src/services/repository.service.ts",
    "content": "import { Octokit } from 'octokit';\nimport { OctokitResponse, RequestError } from '@octokit/types';\nimport { getCustomRepository, getRepository } from 'typeorm';\nimport { config } from '../config';\nimport { ILogger } from '../logger';\nimport { Course, CourseUser, Student } from '../models';\nimport { courseService } from '../services';\nimport { StudentRepository } from '../repositories/student.repository';\nimport { MentorRepository } from '../repositories/mentor.repository';\nimport { MentorBasic } from '../../../common/models';\nimport { camelCase, toUpper } from 'lodash';\n\nexport class RepositoryService {\n  constructor(\n    private courseId: number,\n    private github: Octokit,\n    private logger?: ILogger,\n  ) {}\n\n  public async createMany() {\n    const result = [];\n    const course = await getRepository(Course).findOneBy({ id: this.courseId });\n\n    if (course == null || !course.usePrivateRepositories) {\n      return;\n    }\n\n    await this.createTeam(this.github, this.getTeamName(course), course.id);\n\n    const studentRepo = getCustomRepository(StudentRepository);\n    const students = await studentRepo.findActiveByCourseId(this.courseId);\n\n    for (const student of students) {\n      const studentWithMentor = await studentRepo.findAndIncludeMentor(this.courseId, student.githubId);\n\n      const record = await this.createRepositoryInternally(this.github, course, student.githubId);\n      if (studentWithMentor?.mentor) {\n        const { githubId: mentorGithubId } = studentWithMentor.mentor as MentorBasic;\n        if (mentorGithubId) {\n          await this.inviteMentor(mentorGithubId, course);\n        }\n      }\n      await this.addTeamToRepository(this.github, course, student.githubId);\n      if (record?.repository) {\n        result.push({ repository: record.repository });\n      }\n    }\n    return result;\n  }\n\n  public async createSingle(githubId: string) {\n    const course = await getRepository(Course).findOneBy({ id: this.courseId });\n    if (course == null) {\n      return null;\n    }\n    const studentWithMentor = await getCustomRepository(StudentRepository).findAndIncludeMentor(\n      this.courseId,\n      githubId,\n    );\n    const mentorGithubId = (studentWithMentor?.mentor as MentorBasic | undefined)?.githubId;\n    const result = await this.createRepositoryInternally(this.github, course, githubId);\n    if (mentorGithubId) {\n      await this.inviteMentor(mentorGithubId, course);\n    }\n    await this.addTeamToRepository(this.github, course, githubId);\n    return result;\n  }\n\n  public async updateRepositories() {\n    const course = await getRepository(Course).findOneBy({ id: this.courseId });\n    if (!course) {\n      return;\n    }\n    const students = await getCustomRepository(StudentRepository).findAndIncludeRepository(this.courseId);\n    for (const githubId of students) {\n      const owner = config.github.org;\n      const repo = RepositoryService.getRepoName(githubId, course);\n      try {\n        await this.inviteStudent(owner, repo, githubId);\n        await this.addTeamToRepository(this.github, course, githubId);\n        await Promise.all([\n          this.enablePageSite(this.github, owner, repo),\n          this.updateWebhook(this.github, owner, repo),\n        ]);\n      } catch (e) {\n        this.logger?.error(`[${githubId}] Failed update student repository`, e);\n      }\n    }\n  }\n\n  public async updateWebhook(github: Octokit, owner: string, repo: string) {\n    try {\n      const hooks = await github.rest.repos.listWebhooks({ owner, repo });\n      if (hooks.data.length > 0) {\n        this.logger?.info(`[${owner}/${repo}] webhook already exist`);\n        return;\n      }\n      await this.createWebhook(github, owner, repo);\n    } catch (e) {\n      this.logger?.error(`[${owner}/${repo}] failed to add webhook`, e);\n    }\n  }\n\n  public async createWebhook(github: Octokit, owner: string, repo: string) {\n    const ownerRepo = `${owner}/${repo}`;\n    this.logger?.info(`[${ownerRepo}] creating webhook`);\n    try {\n      await github.rest.repos.createWebhook({\n        owner,\n        repo,\n        config: {\n          url: `${config.aws.restApiUrl}/github/repository-event`,\n          secret: config.github.hooksSecret,\n          content_type: 'json',\n        },\n      });\n      this.logger?.info(`[${ownerRepo}] created webhook`);\n    } catch (err) {\n      const error = err as RequestError;\n      if (error.status === 422) {\n        // hook exists already\n        this.logger?.info(error?.errors ?? error);\n        return;\n      }\n      throw error;\n    }\n  }\n\n  public async inviteMentor(githubId: string, course?: Course) {\n    const mentorCourse = course ?? (await getRepository(Course).findOneBy({ id: this.courseId }));\n    if (!mentorCourse) {\n      return;\n    }\n    await this.addMentorToTeam(this.github, mentorCourse, githubId);\n  }\n\n  public async inviteAllMentors() {\n    const course = await getRepository(Course).findOneBy({ id: this.courseId });\n    if (course == null) {\n      return;\n    }\n    const mentors = await getCustomRepository(MentorRepository).findActive(this.courseId);\n    const courseUsers = await getRepository(CourseUser).find({\n      where: { courseId: this.courseId },\n      relations: ['user'],\n    });\n    const githubIds = mentors\n      .map(m => m.githubId)\n      .concat(courseUsers.filter(u => u.isManager || u.isSupervisor || u.isDementor).map(cu => cu.user?.githubId))\n      .filter(Boolean);\n    for (const githubId of githubIds) {\n      await this.addMentorToTeam(this.github, course, githubId);\n    }\n  }\n\n  private async inviteStudent(owner: string, repo: string, githubId: string) {\n    try {\n      await this.github.rest.repos.addCollaborator({ owner, repo, username: githubId, permission: 'maintain' });\n    } catch (err) {\n      const error = err as RequestError;\n      if (error.status === 422) {\n        // ignore any action\n        this.logger?.info(error.errors ?? error);\n        return;\n      }\n      throw err;\n    }\n  }\n\n  private async enablePageSite(github: Octokit, owner: string, repo: string) {\n    const ownerRepo = `${owner}/${repo}`;\n    try {\n      this.logger?.info(`[${ownerRepo}] enabling GitHub Pages`);\n      const pages = await github.rest.repos.getPages({ owner, repo }).catch(() => null);\n      if (pages?.data.source?.branch === 'gh-pages') {\n        this.logger?.info(`[${ownerRepo}] pages already enabled`);\n        return;\n      }\n      const ghPagesRef = await github.rest.git.getRef({ owner, repo, ref: 'heads/gh-pages' }).catch(() => null);\n      if (ghPagesRef === null) {\n        const mainRef = await github.rest.git\n          .getRef({ owner, repo, ref: 'heads/main' })\n          // for backward compatibility\n          .catch(() => github.rest.git.getRef({ owner, repo, ref: 'heads/master' }));\n        await github.rest.git.createRef({ owner, repo, ref: 'refs/heads/gh-pages', sha: mainRef.data.object.sha });\n      }\n      await github.rest.repos\n        .createPagesSite({ owner, repo, source: { branch: 'gh-pages' } })\n        .catch((response: OctokitResponse<unknown>) => {\n          if (response.status !== 409 && response.status !== 500) {\n            throw response;\n          }\n        });\n      this.logger?.info(`[${ownerRepo}] enabled GitHub Pages`);\n    } catch (err) {\n      this.logger?.info(`[${ownerRepo}] failed to enable GitHub Pages`, err);\n    }\n  }\n\n  public static getRepoName(githubId: string, course: { alias: string }) {\n    return `${githubId}-${toUpper(camelCase(course.alias))}`;\n  }\n\n  private async addMentorToTeam(github: Octokit, course: Course, githubId: string) {\n    const owner = config.github.org;\n    const teamName = this.getTeamName(course);\n    const { data: teams } = await github.rest.teams.list({ org: owner });\n    const team = teams.find(team => team.name === teamName);\n    if (!team) {\n      await this.createTeam(github, teamName, course.id);\n    }\n\n    this.logger?.info(`[${teamName}] adding user ${githubId}`);\n    try {\n      await github.rest.teams.addOrUpdateMembershipForUserInOrg({\n        org: owner,\n        team_slug: teamName,\n        username: githubId,\n      });\n    } catch (err) {\n      const error = err as RequestError;\n      if (error.status === 404) {\n        this.logger?.info(`[${teamName}] user ${githubId} does not exist`);\n      } else {\n        throw err;\n      }\n    }\n  }\n\n  private async addTeamToRepository(github: Octokit, course: Course, githubId: string) {\n    const { org } = config.github;\n    const owner = config.github.org;\n    const repo = RepositoryService.getRepoName(githubId, course);\n    const ownerRepo = `${owner}/${repo}`;\n    const teamName = this.getTeamName(course);\n    this.logger?.info(`[${ownerRepo}] adding team ${teamName}`);\n    try {\n      await github.rest.teams.addOrUpdateRepoPermissionsInOrg({\n        permission: 'push',\n        owner,\n        repo,\n        team_slug: teamName,\n        org,\n      });\n    } catch (err) {\n      const error = err as RequestError;\n      this.logger?.info(error.errors ?? error);\n      if (error.status === 404) {\n        await this.createTeam(github, teamName, course.id);\n        await github.rest.teams.addOrUpdateRepoPermissionsInOrg({\n          permission: 'push',\n          owner,\n          repo,\n          team_slug: teamName,\n          org,\n        });\n      } else {\n        throw err;\n      }\n    }\n  }\n\n  private async createRepositoryInternally(github: Octokit, course: Course, githubId: string) {\n    const owner = config.github.org;\n    const repo = RepositoryService.getRepoName(githubId, course);\n    const ownerRepo = `${owner}/${repo}`;\n    this.logger?.info(`[${ownerRepo}] creating`);\n    let repository: string | null;\n    try {\n      const response = await github.rest.repos.createUsingTemplate({\n        template_repo: 'template-repo',\n        template_owner: owner,\n        owner,\n        include_all_branches: true,\n        name: repo,\n        private: true,\n        description: `Private repository for @${githubId}`,\n      });\n      repository = response.data.html_url;\n    } catch (err) {\n      const error = err as RequestError;\n      if (error.status === 422) {\n        // if repository exists\n        repository = `https://github.com/${owner}/${repo}`;\n        this.logger?.info(error.errors ?? error);\n      } else {\n        throw err;\n      }\n    }\n\n    await this.inviteStudent(owner, repo, githubId);\n\n    await this.createWebhook(github, owner, repo);\n\n    await this.enablePageSite(github, owner, repo);\n\n    const student = await courseService.getStudentByGithubId(course.id, githubId);\n    if (student == null) {\n      return null;\n    }\n    student.repository = repository ?? student.repository;\n    this.logger?.info({ repository: student.repository });\n    await getRepository(Student).save(student);\n    return student;\n  }\n\n  getTeamName(course: Course) {\n    return `mentors-${course.alias}`;\n  }\n\n  async createTeam(github: Octokit, teamName: string, courseId: number) {\n    const { org } = config.github;\n    const exists = await this.checkIfTeamExists(github, org, teamName);\n\n    if (exists) {\n      this.logger?.info(`Team ${teamName} exists`);\n      return;\n    }\n\n    const mentors = await getCustomRepository(MentorRepository).findActive(courseId);\n    this.logger?.info('Creating team', teamName);\n    const response = await github.rest.teams.create({ privacy: 'secret', name: teamName, org });\n    const courseTeamSlug = response.data?.slug;\n    for (const maintainer of mentors) {\n      this.logger?.info(`Inviting ${maintainer.githubId}`);\n      await github.rest.teams.addOrUpdateMembershipForUserInOrg({\n        org,\n        team_slug: courseTeamSlug,\n        username: maintainer.githubId,\n      });\n      await this.timeout(1000);\n    }\n  }\n\n  async checkIfTeamExists(github: Octokit, org: string, teamName: string) {\n    try {\n      const { data: team } = await github.rest.teams.getByName({ org, team_slug: teamName });\n      return !!team.slug;\n    } catch {\n      return false;\n    }\n  }\n\n  private timeout = (num: number) => new Promise(resolve => setTimeout(resolve, num));\n}\n"
  },
  {
    "path": "server/src/services/score/index.ts",
    "content": "export * from './score.service';\n"
  },
  {
    "path": "server/src/services/score/score.service.ts",
    "content": "import _ from 'lodash';\nimport { getRepository } from 'typeorm';\nimport { Course, CourseTask, Student, TaskResult } from '../../models';\nimport { createName } from '../user.service';\nimport { getPrimaryUserFields, convertToMentorBasic, getContactsUserFields } from '../course.service';\nimport { getCourseTasks, updateScoreStudents, getCourses } from '../course.service';\nimport { getStageInterviewRating } from '../stageInterview.service';\nimport { round, mapValues, keyBy, sum } from 'lodash';\nimport { ILogger } from '../../logger';\nimport { config } from '../../config';\n\nconst orderByFieldMapping = {\n  rank: 'student.rank',\n  totalScore: 'student.totalScore',\n  crossCheckScore: 'student.crossCheckScore',\n  githubId: 'user.githubId',\n  name: 'user.firstName',\n  cityName: 'user.cityName',\n  mentor: 'mu.githubId',\n  totalScoreChangeDate: 'student.totalScoreChangeDate',\n  repositoryLastActivityDate: 'student.repositoryLastActivityDate',\n};\n\nconst defaultFilter = {\n  activeOnly: false,\n  githubId: '',\n  name: '',\n  'mentor.githubId': '',\n  cityName: '',\n};\n\nconst defaultOrder: { field: keyof typeof orderByFieldMapping; direction: 'ASC' | 'DESC' } = {\n  field: 'rank',\n  direction: 'ASC',\n};\n\ntype TaskResultData = {\n  authorId?: number;\n  score: number;\n  comment: string;\n  githubPrUrl?: string;\n};\n\ntype TaskResultInput = TaskResultData & {\n  studentId: number;\n  courseTaskId: number;\n};\n\nconst KB = 1024;\n\ntype ScoreOptions = {\n  includeContacts?: boolean;\n  includeCertificate?: boolean;\n};\n\nexport class ScoreService {\n  private taskResultRepository = getRepository(TaskResult);\n\n  constructor(\n    private courseId: number,\n    private options: ScoreOptions = {},\n  ) {}\n\n  public static async recalculateTotalScore(logger: ILogger, coursesToUpdate?: Course[]) {\n    const courses = coursesToUpdate ?? (await getCourses());\n\n    for (const course of courses) {\n      const start = Date.now();\n      logger.info({ msg: `Updating course score`, course: course.name });\n\n      const courseId = course.id;\n      const dataStart = Date.now();\n      const service = new ScoreService(courseId);\n      const [students, courseTasks] = await Promise.all([service.getStudentsScore(), getCourseTasks(courseId)]);\n      logger.info({ msg: `Loaded course score`, course: course.name, duration: Date.now() - dataStart });\n      const weightMap = mapValues(keyBy(courseTasks, 'id'), 'scoreWeight');\n      const crossCheckTaskIds = courseTasks.filter(({ checker }) => checker === 'crossCheck').map(({ id }) => id);\n\n      const calculateScore = (t: { courseTaskId: number; score: number }) => t.score * (weightMap[t.courseTaskId] ?? 1);\n\n      const sortedScores = students\n        .map<ScoreRecord>(({ id, rank, taskResults, totalScore, crossCheckScore, totalScoreChangeDate }) => {\n          const score = sum(taskResults.map(calculateScore));\n\n          const newCrossCheckScore = round(\n            sum(taskResults.filter(t => crossCheckTaskIds.includes(t.courseTaskId)).map(calculateScore)),\n            1,\n          );\n          const newTotalScore = round(score, 1);\n          const scoreChanged = totalScore !== newTotalScore || crossCheckScore !== newCrossCheckScore;\n          return {\n            id,\n            rank,\n            changed: scoreChanged,\n            crossCheckScore: newCrossCheckScore,\n            totalScore: newTotalScore,\n            totalScoreChangeDate: scoreChanged ? new Date() : totalScoreChangeDate,\n          };\n        })\n        .sort((a, b) => b.totalScore - a.totalScore); // ['desc'] by totalScore\n\n      const result: ScoreRecord[] = [];\n\n      sortedScores.forEach((it, index) => {\n        const prev = result[index - 1];\n        const rank = prev?.totalScore === it.totalScore ? prev.rank : index + 1;\n        result.push({ ...it, rank, changed: it.changed || it.rank != rank });\n      });\n\n      const scores = result.filter(it => it.changed).map(({ changed, ...value }) => value);\n\n      await updateScoreStudents(scores);\n\n      logger.info({\n        msg: 'Updated course score',\n        course: course.name,\n        itemsCounts: scores.length,\n        duration: Date.now() - start,\n      });\n    }\n  }\n\n  public async getStudentsScore(filter = defaultFilter, orderBy = defaultOrder) {\n    let query = getRepository(Student)\n      .createQueryBuilder('student')\n      .innerJoin('student.user', 'user')\n      .addSelect(getPrimaryUserFields().concat(this.options.includeContacts ? getContactsUserFields() : []))\n      .leftJoin('student.mentor', 'mentor', 'mentor.\"isExpelled\" = FALSE')\n      .leftJoin('user.resume', 'resume')\n      .addSelect(['resume.uuid', 'resume.userId'])\n      .addSelect(['mentor.id', 'mentor.userId'])\n      .leftJoin('student.taskResults', 'tr')\n      .addSelect(['tr.id', 'tr.score', 'tr.courseTaskId', 'tr.studentId', 'tr.courseTask'])\n      .leftJoin('tr.courseTask', 'ct')\n      .addSelect(['ct.disabled', 'ct.id'])\n      .leftJoin('student.taskInterviewResults', 'tir')\n      .addSelect(['tir.id', 'tir.score', 'tir.courseTaskId', 'tr.studentId', 'tir.updatedDate'])\n      .leftJoin('mentor.user', 'mu')\n      .addSelect(getPrimaryUserFields('mu'))\n      .leftJoin('student.stageInterviews', 'si')\n      .leftJoin('si.stageInterviewFeedbacks', 'sif')\n      .addSelect([\n        'sif.stageInterviewId',\n        'sif.json',\n        'sif.updatedDate',\n        'si.isCompleted',\n        'si.id',\n        'si.courseTaskId',\n        'si.score',\n      ])\n      .where('student.\"courseId\" = :courseId', { courseId: this.courseId });\n\n    if (this.options.includeCertificate) {\n      query = query.leftJoin('student.certificate', 'certificate').addSelect('certificate.id');\n    }\n    if (filter.activeOnly) {\n      query = query.andWhere('student.\"isFailed\" = false').andWhere('student.\"isExpelled\" = false');\n    }\n\n    if (filter.name) {\n      query = query.andWhere('(\"user\".\"firstName\" ILIKE :searchText OR \"user\".\"lastName\" ILIKE :searchText)', {\n        searchText: `%${filter.name}%`,\n      });\n    }\n\n    if (filter.cityName) {\n      query = query.andWhere('\"user\".\"cityName\" ILIKE :searchCityNameText', {\n        searchCityNameText: `%${filter.cityName}%`,\n      });\n    }\n\n    if (filter['mentor.githubId']) {\n      query = query.andWhere('\"mu\".\"githubId\" ILIKE :searchMentorGithubIdText', {\n        searchMentorGithubIdText: `%${filter['mentor.githubId']}%`,\n      });\n    }\n\n    if (filter.githubId) {\n      query = query.andWhere('(\"user\".\"githubId\" ILIKE :searchGithubIdText)', {\n        searchGithubIdText: `%${filter.githubId}%`,\n      });\n    }\n\n    const content = await query.orderBy(orderByFieldMapping[orderBy.field], orderBy.direction).getMany();\n\n    const students = content.map(student => {\n      const preScreeningScore = Math.floor(getStageInterviewRating(student.stageInterviews ?? []) ?? 0);\n      const preScreningInterviews = student.stageInterviews?.length\n        ? [{ score: preScreeningScore, courseTaskId: student.stageInterviews[0].courseTaskId }]\n        : [];\n\n      const user = student.user;\n\n      const resumeUuid = student.user.resume?.find(r => r.userId === user.id)?.uuid;\n      const cvLink = resumeUuid ? `${config.host}/cv/${resumeUuid}` : '';\n\n      const interviews = _.values(_.groupBy(student.taskInterviewResults ?? [], 'courseTaskId'))\n        .map(arr => _.first(_.orderBy(arr, 'updatedDate', 'desc'))!)\n        .map(({ courseTaskId, score = 0 }) => ({ courseTaskId, score }));\n\n      let taskResults =\n        student.taskResults\n          ?.filter(({ courseTask: { disabled } }) => !disabled)\n          .map(({ courseTaskId, score }) => ({ courseTaskId, score }))\n          .concat(interviews) ?? [];\n\n      // we have a case when technical screening score are set as task result.\n      taskResults = taskResults.concat(\n        preScreningInterviews.filter(i => !taskResults.find(tr => tr.courseTaskId === i.courseTaskId)),\n      );\n\n      const mentor = student.mentor ? convertToMentorBasic(student.mentor) : undefined;\n      return {\n        id: student.id,\n        rank: student.rank,\n        cvLink,\n        mentor: mentor ? { githubId: mentor.githubId, name: mentor.name } : undefined,\n        name: createName(user),\n        githubId: user.githubId,\n        totalScore: student.totalScore,\n        totalScoreChangeDate: student.totalScoreChangeDate,\n        crossCheckScore: student.crossCheckScore,\n        repositoryLastActivityDate: student.repositoryLastActivityDate,\n        cityName: user.cityName ?? '',\n        countryName: user.countryName ?? 'Other',\n        taskResults,\n        isActive: !student.isExpelled && !student.isFailed,\n        contacts: this.options.includeContacts\n          ? {\n              epamEmail: user.contactsEpamEmail,\n              email: user.primaryEmail || user.contactsEmail,\n              linkedIn: user.contactsLinkedIn,\n              telegram: user.contactsTelegram,\n            }\n          : null,\n        hasCertificate: this.options.includeCertificate ? !!student.certificate?.id : undefined,\n      };\n    });\n\n    return students;\n  }\n\n  public async getStudentsScoreForExport(filters: any) {\n    const students = await this.getStudentsScore(filters);\n    const courseTasks = await getCourseTasks(this.courseId);\n\n    return students.map(student => {\n      return {\n        githubId: student.githubId,\n        name: student.name,\n        cvLink: student.cvLink,\n        locationName: student.cityName,\n        countryName: student.countryName || 'Other',\n        mentorGithubId: student.mentor ? student.mentor.githubId : '',\n        totalScore: student.totalScore,\n        isActive: student.isActive,\n        contacts: student.contacts,\n        hasCertificate: student.hasCertificate,\n        ...this.getTasksResults(student.taskResults, courseTasks),\n      };\n    });\n  }\n\n  public async saveScore(\n    studentId: number,\n    courseTaskId: number,\n    data: TaskResultData,\n  ): Promise<[boolean, TaskResult | undefined] | [boolean]> {\n    const { authorId = 0, githubPrUrl = null } = data;\n\n    const comment = this.trimComment(data.comment ?? '');\n    const score = Math.round(data.score);\n\n    const current = await this.getTaskResult(studentId, courseTaskId);\n\n    if (current == null) {\n      const taskResult = this.createTaskResult({\n        comment,\n        score,\n        studentId,\n        courseTaskId,\n        githubPrUrl: githubPrUrl ?? undefined,\n        authorId,\n      });\n      await this.taskResultRepository.insert(taskResult);\n      return [true];\n    }\n\n    if (current.githubRepoUrl === githubPrUrl && current.comment === comment && current.score === score) {\n      return [true];\n    }\n\n    let previousScore: TaskResult | undefined = undefined;\n    if (current.comment !== comment || current.score !== score) {\n      previousScore = { ...current };\n      current.historicalScores.push(this.createHistoricalRecord(data));\n    }\n\n    if (githubPrUrl) {\n      current.githubPrUrl = githubPrUrl;\n    }\n\n    if (comment) {\n      current.comment = comment;\n    }\n    if (score !== current.score) {\n      if (authorId > 0) {\n        current.lastCheckerId = authorId;\n      }\n      current.score = score;\n    }\n\n    await this.taskResultRepository.update(current.id, {\n      score: current.score,\n      comment: current.comment,\n      githubPrUrl: current.githubPrUrl,\n      historicalScores: current.historicalScores,\n      lastCheckerId: current.lastCheckerId,\n    });\n    return [false, previousScore];\n  }\n\n  private getTaskResult(studentId: number, courseTaskId: number) {\n    return this.taskResultRepository\n      .createQueryBuilder('r')\n      .where('r.\"studentId\" = :studentId', { studentId })\n      .andWhere('r.\"courseTaskId\" = :courseTaskId', { courseTaskId })\n      .getOne();\n  }\n\n  private createTaskResult(data: TaskResultInput): Partial<TaskResult> {\n    const authorId = data.authorId ?? 0;\n    return {\n      comment: data.comment,\n      courseTaskId: data.courseTaskId,\n      studentId: data.studentId,\n      score: data.score,\n      historicalScores: [this.createHistoricalRecord(data)],\n      lastCheckerId: authorId > 0 ? authorId : undefined,\n      githubPrUrl: data.githubPrUrl,\n    };\n  }\n\n  private createHistoricalRecord(data: Pick<TaskResultData, 'authorId' | 'comment' | 'score'>) {\n    return {\n      authorId: data.authorId ?? 0,\n      score: data.score,\n      dateTime: Date.now(),\n      comment: data.comment,\n    };\n  }\n\n  private trimComment(comment: string): string {\n    return comment.substr(0, 8 * KB);\n  }\n\n  private getTasksResults(results: { courseTaskId: number; score: number }[], courseTasks: CourseTask[]) {\n    return courseTasks.reduce(\n      (acc, courseTask) => {\n        const result = results.find(r => r.courseTaskId === courseTask.id);\n        const { name } = courseTask.task;\n        acc[name] = result?.score ?? 0;\n        return acc;\n      },\n      {} as Record<string, number>,\n    );\n  }\n}\n\ntype ScoreRecord = {\n  id: number;\n  rank: number;\n  changed: boolean;\n  crossCheckScore: number;\n  totalScore: number;\n  totalScoreChangeDate: Date;\n};\n"
  },
  {
    "path": "server/src/services/stageInterview.service.ts",
    "content": "import { StageInterview, StageInterviewFeedback } from '../models';\nimport { StageInterviewFeedbackJson } from '../../../common/models';\n\nexport function getInterviewRatings({ skills, programmingTask, resume }: StageInterviewFeedbackJson) {\n  const commonSkills = Object.values(skills?.common ?? {}).filter(Boolean) as number[];\n  const dataStructuresSkills = Object.values(skills?.dataStructures ?? {}).filter(Boolean) as number[];\n\n  const htmlCss = skills?.htmlCss.level;\n  const common = commonSkills.reduce((acc, cur) => acc + cur, 0) / commonSkills.length;\n  const dataStructures = dataStructuresSkills.reduce((acc, cur) => acc + cur, 0) / dataStructuresSkills.length;\n\n  if (resume?.score !== undefined) {\n    const rating = resume.score;\n    return { rating, htmlCss, common, dataStructures };\n  }\n\n  const ratingsCount = 4;\n  const ratings = [htmlCss, common, dataStructures, programmingTask.codeWritingLevel].filter(Boolean) as number[];\n  const rating = (ratings.length === ratingsCount ? ratings.reduce((sum, num) => sum + num) / ratingsCount : 0) * 10;\n\n  return { rating, htmlCss, common, dataStructures };\n}\n\nexport const getStageInterviewRating = (stageInterviews: StageInterview[]) => {\n  const [lastInterview] = stageInterviews\n    .filter((interview: StageInterview) => interview.isCompleted)\n    .map(({ stageInterviewFeedbacks, score }: StageInterview) =>\n      stageInterviewFeedbacks.map((feedback: StageInterviewFeedback) => ({\n        date: feedback.updatedDate,\n        // interviews in new template should have score precalculated\n        rating: score ?? getInterviewRatings(JSON.parse(feedback.json) as StageInterviewFeedbackJson).rating,\n      })),\n    )\n    .reduce((acc, cur) => acc.concat(cur), [])\n    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());\n\n  return lastInterview && lastInterview.rating !== undefined ? lastInterview.rating : null;\n};\n"
  },
  {
    "path": "server/src/services/student.service.ts",
    "content": "import * as courseService from './course.service';\nimport { isDementor, IUserSession } from '../models';\nimport { getCustomRepository } from 'typeorm';\nimport { StageInterviewRepository } from '../repositories/stageInterview.repository';\nimport { StudentRepository } from '../repositories/student.repository';\n\nexport async function canChangeStatus(\n  session: IUserSession,\n  courseId: number,\n  githubId: string,\n): Promise<{ allow: boolean; message?: string }> {\n  const student = await courseService.getStudentByGithubId(courseId, githubId);\n  if (student == null) {\n    return {\n      allow: false,\n      message: 'not valid student',\n    };\n  }\n  if (courseService.isPowerUser(courseId, session) || isDementor(session, courseId)) {\n    return { allow: true };\n  }\n\n  if (!courseService.isPowerUser(courseId, session)) {\n    const repository = getCustomRepository(StageInterviewRepository);\n    const [interviews, mentor] = await Promise.all([\n      repository.findByStudent(courseId, githubId),\n      courseService.getCourseMentor(courseId, session.id),\n    ] as const);\n    if (mentor == null) {\n      return {\n        allow: false,\n        message: 'not valid mentor',\n      };\n    }\n    if (!interviews.some(it => it.interviewer.githubId === session.githubId) && student.mentorId !== mentor.id) {\n      return {\n        allow: false,\n        message: 'incorrect mentor-student relation',\n      };\n    }\n  }\n  return { allow: true };\n}\n\nexport async function updateRepositoryActivity(repositoryUrl: string) {\n  await getCustomRepository(StudentRepository).updateRepositoryActivityDate(repositoryUrl);\n}\n"
  },
  {
    "path": "server/src/services/taskResults.service.ts",
    "content": "import {\n  TaskResult,\n  TaskArtefact,\n  TaskSolution,\n  TaskSolutionResult,\n  TaskSolutionChecker,\n  CourseTask,\n  Student,\n  User,\n} from '../models';\nimport { getRepository } from 'typeorm';\nimport { getPrimaryUserFields } from './course.service';\n\nexport async function getTaskResult(studentId: number, courseTaskId: number) {\n  return getRepository(TaskResult)\n    .createQueryBuilder('taskResult')\n    .where('\"taskResult\".\"studentId\" = :studentId', { studentId })\n    .andWhere('\"taskResult\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n    .getOne();\n}\n\nexport async function getStudentTaskArtefact(studentId: number, courseTaskId: number) {\n  return getRepository(TaskArtefact)\n    .createQueryBuilder('taskArtefact')\n    .where('\"taskResult\".\"studentId\" = :studentId', { studentId })\n    .andWhere('\"taskResult\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n    .getOne();\n}\n\nexport async function getTaskSolution(studentId: number, courseTaskId: number) {\n  return getRepository(TaskSolution)\n    .createQueryBuilder('taskSolution')\n    .where('\"taskSolution\".\"studentId\" = :studentId', { studentId })\n    .andWhere('\"taskSolution\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n    .getOne();\n}\n\nexport async function getTaskSolutionChecker(studentId: number, checkerId: number, courseTaskId: number) {\n  return getRepository(TaskSolutionChecker)\n    .createQueryBuilder('taskSolutionChecker')\n    .where('\"taskSolutionChecker\".\"studentId\" = :studentId', { studentId })\n    .andWhere('\"taskSolutionChecker\".\"checkerId\" = :checkerId', { checkerId })\n    .andWhere('\"taskSolutionChecker\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n    .getOne();\n}\n\nexport async function getTaskSolutionAssignments(checkerId: number, courseTaskId: number) {\n  return getRepository(TaskSolutionChecker)\n    .createQueryBuilder('taskSolutionChecker')\n    .innerJoinAndSelect('taskSolutionChecker.taskSolution', 'taskSolution')\n    .innerJoinAndSelect('taskSolutionChecker.student', 'student')\n    .innerJoin('student.user', 'user')\n    .addSelect(getPrimaryUserFields())\n    .where('\"taskSolutionChecker\".\"checkerId\" = :checkerId', { checkerId })\n    .andWhere('\"taskSolutionChecker\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n    .getMany();\n}\n\nexport async function getTaskSolutionResult(studentId: number, checkerId: number, courseTaskId: number) {\n  return getRepository(TaskSolutionResult)\n    .createQueryBuilder('taskSolutionResult')\n    .where('\"taskSolutionResult\".\"studentId\" = :studentId', { studentId })\n    .andWhere('\"taskSolutionResult\".\"checkerId\" = :checkerId', { checkerId })\n    .andWhere('\"taskSolutionResult\".\"courseTaskId\" = :courseTaskId', { courseTaskId })\n    .getOne();\n}\n\nexport async function getTaskSolutionResultById(id: number) {\n  return getRepository(TaskSolutionResult)\n    .createQueryBuilder('taskSolutionResult')\n    .where('\"taskSolutionResult\".\"id\" = :id', { id })\n    .getOne();\n}\n\nexport async function getCrossCheckData(\n  filter: {\n    checkerStudent?: string;\n    student?: string;\n    task?: string;\n    url?: string;\n    score?: string;\n  },\n  pagination: { current: number; pageSize: number },\n  courseId: number,\n  orderBy: string,\n  orderDirection?: 'ASC' | 'DESC' | undefined,\n) {\n  const orderByFieldMapping: Record<string, string> = {\n    'checkerStudent,githubId': '\"checkerStudent\".\"githubId\"',\n    'student,githubId': '\"student\".\"githubId\"',\n    'task,name': '\"task\".\"name\"',\n    url: 'taskSolution.url',\n    score: 'tsr.score',\n    reviewedDate: 'tsr.updatedDate',\n    submittedDate: 'taskSolution.updatedDate',\n  };\n\n  const query = getRepository(TaskSolutionResult)\n    .createQueryBuilder('tsr')\n    .addSelect(['tsr.score', 'tsr.comment', 'tsr.updatedDate'])\n    .leftJoin(CourseTask, 'courseTask', '\"tsr\".\"courseTaskId\" = \"courseTask\".\"id\"')\n    .addSelect(['courseTask.id', 'courseTask.courseId'])\n    .leftJoin('courseTask.task', 'task')\n    .addSelect(['task.id', 'task.name'])\n    .leftJoin(Student, 'st', '\"tsr\".\"studentId\" = \"st\".\"id\"')\n    .leftJoin(User, 'student', '\"st\".\"userId\" = \"student\".\"id\"')\n    .addSelect(['student.id', 'student.githubId'])\n    .leftJoin(Student, 'ch', '\"tsr\".\"checkerId\" = \"ch\".\"id\"')\n    .leftJoin(User, 'checkerStudent', '\"ch\".\"userId\" = \"checkerStudent\".\"id\"')\n    .addSelect(['checkerStudent.id', 'checkerStudent.githubId'])\n    .leftJoin(\n      TaskSolution,\n      'taskSolution',\n      '\"taskSolution\".\"courseTaskId\" = \"courseTask\".\"id\" AND \"taskSolution\".\"studentId\" = \"st\".\"id\"',\n    )\n    .addSelect(['taskSolution.url', 'taskSolution.updatedDate'])\n    .where(`courseTask.courseId = :courseId`, { courseId })\n    .andWhere('courseTask.checker = :checker', { checker: 'crossCheck' });\n\n  if (filter.checkerStudent) {\n    query.andWhere('\"checkerStudent\".\"githubId\" ILIKE :checkerStudent', {\n      checkerStudent: `%${filter.checkerStudent}%`,\n    });\n  }\n\n  if (filter.student) {\n    query.andWhere('\"student\".\"githubId\" ILIKE :student', {\n      student: `%${filter.student}%`,\n    });\n  }\n\n  if (filter.task) {\n    query.andWhere('\"task\".\"name\" ILIKE :task', {\n      task: `%${filter.task}%`,\n    });\n  }\n\n  if (filter.url) {\n    query.andWhere('\"taskSolution\".\"url\" ILIKE :url', {\n      url: `%${filter.url}%`,\n    });\n  }\n\n  const total = await query.getCount();\n\n  const courseTasks = await query\n    .orderBy(orderByFieldMapping[orderBy], orderDirection)\n    .limit(pagination.pageSize)\n    .offset((pagination.current - 1) * pagination.pageSize)\n    .getRawMany();\n\n  const result = courseTasks.map((e: any) => ({\n    checkerStudent: {\n      githubId: e.checkerStudent_githubId,\n      id: e.checkerStudent_id,\n    },\n    courseTask: {\n      courseId: e.courseTask_courseId,\n      id: e.courseTask_id,\n    },\n    student: {\n      githubId: e.student_githubId,\n      id: e.student_id,\n    },\n    task: {\n      name: e.task_name,\n      id: e.task_id,\n    },\n    url: e.taskSolution_url,\n    score: e.tsr_score,\n    comment: e.tsr_comment,\n    submittedDate: e.taskSolution_updatedDate,\n    reviewedDate: e.tsr_updatedDate,\n    key: `${e.checkerStudent_id}${e.courseTask_id}${e.student_id}${e.task_id}`,\n  }));\n\n  return {\n    content: result,\n    pagination: {\n      current: pagination.current,\n      pageSize: pagination.pageSize,\n      total,\n      totalPages: Math.ceil(total / pagination.pageSize),\n    },\n  };\n}\n\ntype TaskArtefactInput = {\n  studentId: number;\n  courseTaskId: number;\n  comment: string;\n  videoUrl?: string;\n  presentationUrl?: string;\n};\n\nexport function createStudentArtefactTaskResult(data: TaskArtefactInput): Partial<TaskArtefact> {\n  return {\n    courseTaskId: data.courseTaskId,\n    studentId: data.studentId,\n    videoUrl: data.videoUrl,\n    presentationUrl: data.presentationUrl,\n  };\n}\n"
  },
  {
    "path": "server/src/services/taskVerification.service.ts",
    "content": "import { TaskVerification } from '../models';\nimport { getRepository } from 'typeorm';\n\nexport async function cancelPendingTasks() {\n  return getRepository(TaskVerification)\n    .createQueryBuilder()\n    .update()\n    .set({ status: 'cancelled' })\n    .where(`\"updatedDate\" + interval '1 hour' < now()::timestamp AND status = 'pending'`)\n    .execute();\n}\n"
  },
  {
    "path": "server/src/services/tasks.service.ts",
    "content": "import { CourseTask, CrossCheckStatus } from '../models/courseTask';\nimport { getRepository } from 'typeorm';\n\nexport async function getCourseTask(courseTaskId: number, selectCourse = false) {\n  const query = getRepository(CourseTask)\n    .createQueryBuilder('courseTask')\n    .innerJoinAndSelect('courseTask.task', 'task')\n    .where('courseTask.id = :courseTaskId', { courseTaskId });\n  if (selectCourse) {\n    query.innerJoinAndSelect('courseTask.course', 'course');\n  }\n  return query.getOne();\n}\n\nexport async function getCourseTaskOnly(courseTaskId: number): Promise<{ id: number } | null> {\n  return getRepository(CourseTask)\n    .createQueryBuilder('courseTask')\n    .where('courseTask.id = :courseTaskId', { courseTaskId: Number(courseTaskId) })\n    .getOne();\n}\n\nexport async function changeCourseTaskStatus(courseTask: CourseTask, crossCheckStatus: CrossCheckStatus) {\n  await getRepository(CourseTask).save({ ...courseTask, crossCheckStatus });\n}\n\nexport async function changeCourseTaskProcessing(courseTaskId: number, isCreatingInterviewPairs: boolean) {\n  await getRepository(CourseTask).update(courseTaskId, { isCreatingInterviewPairs });\n}\n\nexport function isSubmissionDeadlinePassed({ studentEndDate }: CourseTask) {\n  const currentTimestamp = Date.now();\n  if (!studentEndDate) return false;\n  const submitDeadlineTimestamp = new Date(studentEndDate).getTime();\n  return currentTimestamp > submitDeadlineTimestamp;\n}\n"
  },
  {
    "path": "server/src/services/user.service.ts",
    "content": "import { User } from '../models';\nimport { getRepository, In } from 'typeorm';\n\nexport function getUserByGithubId(id: string) {\n  const githubId = id.toLowerCase();\n  return getRepository(User).findOne({ where: { githubId } });\n}\n\nexport function getUserByProvider(provider: string, providerUserId: string) {\n  return getRepository(User).findOne({\n    where: { provider, providerUserId },\n    relations: ['mentors', 'students', 'mentors.course', 'students.course', 'courseUsers', 'courseUsers.course'],\n  });\n}\n\nexport function getFullUserByGithubId(id: string) {\n  const githubId = id.toLowerCase();\n\n  return getRepository(User).findOne({\n    where: { githubId },\n    relations: ['mentors', 'students', 'mentors.course', 'students.course', 'courseUsers', 'courseUsers.course'],\n  });\n}\n\nexport function getFullUserById(id: number) {\n  return getRepository(User).findOne({\n    where: { id },\n    relations: ['mentors', 'students', 'mentors.course', 'students.course'],\n  });\n}\n\nexport function getUserById(id: number) {\n  return getRepository(User).findOne({ where: { id } });\n}\n\nexport function saveUser(user: Partial<User>) {\n  return getRepository(User).save(user);\n}\n\nexport function getUsersByGithubIds(githubIds: string[]) {\n  return getRepository(User).find({\n    where: { githubId: In(githubIds) },\n  });\n}\n\nexport function createName({ firstName, lastName }: { firstName: string; lastName: string }) {\n  const result = [];\n  if (firstName) {\n    result.push(firstName.trim());\n  }\n  if (lastName) {\n    result.push(lastName.trim());\n  }\n  return result.join(' ');\n}\n"
  },
  {
    "path": "server/swaggerDef.js",
    "content": "module.exports = {\n  info: {\n    title: 'rs.school API',\n    version: '1.0.0',\n    description: '',\n  },\n  basePath: '/api',\n};\n"
  },
  {
    "path": "server/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"emitDecoratorMetadata\": true,\n    \"esModuleInterop\": true,\n    \"experimentalDecorators\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noImplicitAny\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"resolveJsonModule\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"strictPropertyInitialization\": false,\n    \"target\": \"es2020\",\n    \"outDir\": \"./dist\",\n    \"skipLibCheck\": true,\n    \"lib\": [\"es2020\", \"dom\", \"esnext.asynciterable\", \"ES2019.Array\", \"ES2020\"],\n    \"types\": [\"vitest/globals\"]\n  },\n  \"include\": [\"./src/**/*.ts\"],\n  \"exclude\": [\"./node_modules\", \"./dist\"]\n}\n"
  },
  {
    "path": "server/vitest.config.mts",
    "content": "import path from 'node:path';\nimport swc from 'unplugin-swc';\nimport { defineConfig, mergeConfig } from 'vitest/config';\nimport shared from '../vitest.shared.mjs';\n\nexport default mergeConfig(\n  shared,\n  defineConfig({\n    plugins: [\n      swc.vite({\n        module: { type: 'es6' },\n      }),\n    ],\n    resolve: {\n      alias: {\n        src: path.resolve(import.meta.dirname, 'src'),\n      },\n    },\n    test: {\n      include: ['src/**/*.test.ts'],\n      env: {\n        NODE_ENV: 'test',\n      },\n    },\n  }),\n);\n"
  },
  {
    "path": "setup/backup-local.sql",
    "content": "--\n-- PostgreSQL database dump\n--\n\n-- Dumped from database version 15.5 (Debian 15.5-1.pgdg120+1)\n-- Dumped by pg_dump version 15.12 (Debian 15.12-1.pgdg120+1)\n\nSET statement_timeout = 0;\nSET lock_timeout = 0;\nSET idle_in_transaction_session_timeout = 0;\nSET client_encoding = 'UTF8';\nSET standard_conforming_strings = on;\nSELECT pg_catalog.set_config('search_path', '', false);\nSET check_function_bodies = false;\nSET xmloption = content;\nSET client_min_messages = warning;\nSET row_security = off;\n\n--\n-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: -\n--\n\nCREATE EXTENSION IF NOT EXISTS \"uuid-ossp\" WITH SCHEMA public;\n\n\n--\n-- Name: EXTENSION \"uuid-ossp\"; Type: COMMENT; Schema: -; Owner:\n--\n\nCOMMENT ON EXTENSION \"uuid-ossp\" IS 'generate universally unique identifiers (UUIDs)';\n\n\n--\n-- Name: course_task_crosscheckstatus_enum; Type: TYPE; Schema: public; Owner: rs_master\n--\n\nCREATE TYPE public.course_task_crosscheckstatus_enum AS ENUM (\n    'initial',\n    'distributed',\n    'completed'\n);\n\n\nALTER TYPE public.course_task_crosscheckstatus_enum OWNER TO rs_master;\n\n--\n-- Name: user_english_level_enum; Type: TYPE; Schema: public; Owner: rs_master\n--\n\nCREATE TYPE public.user_english_level_enum AS ENUM (\n    'a1',\n    'a2',\n    'b1',\n    'b2',\n    'c1',\n    'c2'\n);\n\n\nALTER TYPE public.user_english_level_enum OWNER TO rs_master;\n\nSET default_tablespace = '';\n\nSET default_table_access_method = heap;\n\n--\n-- Name: alert; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.alert (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    text character varying NOT NULL,\n    \"courseId\" integer,\n    enabled boolean DEFAULT false NOT NULL,\n    type character varying DEFAULT 'info'::character varying NOT NULL\n);\n\n\nALTER TABLE public.alert OWNER TO rs_master;\n\n--\n-- Name: alert_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.alert_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.alert_id_seq OWNER TO rs_master;\n\n--\n-- Name: alert_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.alert_id_seq OWNED BY public.alert.id;\n\n\n--\n-- Name: certificate; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.certificate (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"publicId\" character varying NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"s3Bucket\" character varying DEFAULT 'rsschool-certificates'::character varying NOT NULL,\n    \"s3Key\" character varying NOT NULL,\n    \"issueDate\" timestamp with time zone NOT NULL\n);\n\n\nALTER TABLE public.certificate OWNER TO rs_master;\n\n--\n-- Name: certificate_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.certificate_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.certificate_id_seq OWNER TO rs_master;\n\n--\n-- Name: certificate_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.certificate_id_seq OWNED BY public.certificate.id;\n\n\n--\n-- Name: consent; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.consent (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"channelValue\" character varying NOT NULL,\n    \"channelType\" character varying NOT NULL,\n    \"optIn\" boolean NOT NULL,\n    username character varying\n);\n\n\nALTER TABLE public.consent OWNER TO rs_master;\n\n--\n-- Name: consent_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.consent_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.consent_id_seq OWNER TO rs_master;\n\n--\n-- Name: consent_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.consent_id_seq OWNED BY public.consent.id;\n\n\n--\n-- Name: contributor; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.contributor (\n    id integer NOT NULL,\n    created_date timestamp without time zone DEFAULT now() NOT NULL,\n    updated_date timestamp without time zone DEFAULT now() NOT NULL,\n    deleted_date timestamp without time zone,\n    user_id integer NOT NULL,\n    description character varying NOT NULL\n);\n\n\nALTER TABLE public.contributor OWNER TO rs_master;\n\n--\n-- Name: contributor_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.contributor_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.contributor_id_seq OWNER TO rs_master;\n\n--\n-- Name: contributor_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.contributor_id_seq OWNED BY public.contributor.id;\n\n\n--\n-- Name: course; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.course (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    name character varying NOT NULL,\n    year integer,\n    \"primarySkillId\" character varying,\n    \"primarySkillName\" character varying,\n    \"locationName\" character varying,\n    alias character varying NOT NULL,\n    completed boolean DEFAULT false NOT NULL,\n    description character varying,\n    \"descriptionUrl\" character varying,\n    planned boolean DEFAULT false NOT NULL,\n    \"startDate\" timestamp with time zone,\n    \"endDate\" timestamp with time zone,\n    \"fullName\" character varying NOT NULL,\n    \"registrationEndDate\" timestamp with time zone,\n    \"inviteOnly\" boolean DEFAULT false NOT NULL,\n    \"discordServerId\" integer,\n    \"certificateIssuer\" character varying,\n    \"usePrivateRepositories\" boolean DEFAULT true NOT NULL,\n    \"personalMentoring\" boolean DEFAULT true NOT NULL,\n    logo character varying,\n    \"disciplineId\" integer,\n    \"minStudentsPerMentor\" integer DEFAULT 2,\n    \"certificateThreshold\" integer DEFAULT 70 NOT NULL,\n    \"wearecommunityUrl\" character varying,\n    \"personalMentoringStartDate\" timestamp with time zone,\n    \"personalMentoringEndDate\" timestamp with time zone,\n    \"certificateDisciplines\" text DEFAULT NULL\n);\n\n\nALTER TABLE public.course OWNER TO rs_master;\n\n--\n-- Name: course_event; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.course_event (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"eventId\" integer NOT NULL,\n    \"courseId\" integer NOT NULL,\n    \"stageId\" integer,\n    date date,\n    \"time\" time with time zone,\n    place character varying,\n    coordinator character varying,\n    comment character varying,\n    \"organizerId\" integer,\n    \"detailsUrl\" character varying,\n    \"broadcastUrl\" character varying,\n    \"dateTime\" timestamp with time zone,\n    special character varying DEFAULT ''::character varying NOT NULL,\n    duration integer,\n    \"endTime\" timestamp with time zone\n);\n\n\nALTER TABLE public.course_event OWNER TO rs_master;\n\n--\n-- Name: course_event_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.course_event_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.course_event_id_seq OWNER TO rs_master;\n\n--\n-- Name: course_event_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.course_event_id_seq OWNED BY public.course_event.id;\n\n\n--\n-- Name: course_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.course_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.course_id_seq OWNER TO rs_master;\n\n--\n-- Name: course_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.course_id_seq OWNED BY public.course.id;\n\n\n--\n-- Name: course_manager; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.course_manager (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseId\" integer,\n    \"userId\" integer\n);\n\n\nALTER TABLE public.course_manager OWNER TO rs_master;\n\n--\n-- Name: course_manager_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.course_manager_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.course_manager_id_seq OWNER TO rs_master;\n\n--\n-- Name: course_manager_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.course_manager_id_seq OWNED BY public.course_manager.id;\n\n\n--\n-- Name: course_task; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.course_task (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"mentorStartDate\" timestamp without time zone,\n    \"mentorEndDate\" timestamp without time zone,\n    \"maxScore\" integer,\n    \"taskId\" integer NOT NULL,\n    \"scoreWeight\" double precision DEFAULT 1,\n    checker character varying DEFAULT 'mentor'::character varying NOT NULL,\n    \"taskOwnerId\" integer,\n    \"studentStartDate\" timestamp with time zone,\n    \"studentEndDate\" timestamp with time zone,\n    \"courseId\" integer,\n    \"pairsCount\" integer,\n    type character varying,\n    disabled boolean DEFAULT false NOT NULL,\n    \"crossCheckEndDate\" timestamp with time zone,\n    \"submitText\" character varying(1024),\n    validations text,\n    \"crossCheckStatus\" public.course_task_crosscheckstatus_enum DEFAULT 'initial'::public.course_task_crosscheckstatus_enum NOT NULL,\n    \"teamDistributionId\" integer,\n    \"studentRegistrationStartDate\" timestamp with time zone\n);\n\n\nALTER TABLE public.course_task OWNER TO rs_master;\n\n--\n-- Name: course_task_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.course_task_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.course_task_id_seq OWNER TO rs_master;\n\n--\n-- Name: course_task_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.course_task_id_seq OWNED BY public.course_task.id;\n\n\n--\n-- Name: course_user; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.course_user (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseId\" integer NOT NULL,\n    \"userId\" integer NOT NULL,\n    \"isManager\" boolean DEFAULT false NOT NULL,\n    \"isJuryActivist\" boolean DEFAULT false NOT NULL,\n    \"isSupervisor\" boolean DEFAULT false NOT NULL,\n    \"isDementor\" boolean DEFAULT false NOT NULL,\n    \"isActivist\" boolean DEFAULT false NOT NULL\n);\n\n\nALTER TABLE public.course_user OWNER TO rs_master;\n\n--\n-- Name: course_user_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.course_user_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.course_user_id_seq OWNER TO rs_master;\n\n--\n-- Name: course_user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.course_user_id_seq OWNED BY public.course_user.id;\n\n\n--\n-- Name: cv; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.cv (\n    id integer NOT NULL,\n    \"githubId\" text NOT NULL,\n    name text,\n    \"selfIntroLink\" text,\n    \"startFrom\" text,\n    \"fullTime\" boolean,\n    expires numeric,\n    \"militaryService\" text,\n    \"englishLevel\" text,\n    \"avatarLink\" text,\n    \"desiredPosition\" text,\n    notes text,\n    phone text,\n    email text,\n    skype text,\n    telegram text,\n    linkedin text,\n    location text,\n    \"githubUsername\" text,\n    website text\n);\n\n\nALTER TABLE public.cv OWNER TO rs_master;\n\n--\n-- Name: cv_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.cv_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.cv_id_seq OWNER TO rs_master;\n\n--\n-- Name: cv_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.cv_id_seq OWNED BY public.cv.id;\n\n\n--\n-- Name: discipline; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.discipline (\n    id integer NOT NULL,\n    created_date timestamp without time zone DEFAULT now() NOT NULL,\n    updated_date timestamp without time zone DEFAULT now() NOT NULL,\n    deleted_date timestamp without time zone,\n    name character varying NOT NULL\n);\n\n\nALTER TABLE public.discipline OWNER TO rs_master;\n\n--\n-- Name: discipline_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.discipline_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.discipline_id_seq OWNER TO rs_master;\n\n--\n-- Name: discipline_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.discipline_id_seq OWNED BY public.discipline.id;\n\n\n--\n-- Name: discord_server; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.discord_server (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    name character varying NOT NULL,\n    \"gratitudeUrl\" character varying NOT NULL,\n    \"mentorsChatUrl\" text\n);\n\n\nALTER TABLE public.discord_server OWNER TO rs_master;\n\n--\n-- Name: discord_server_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.discord_server_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.discord_server_id_seq OWNER TO rs_master;\n\n--\n-- Name: discord_server_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.discord_server_id_seq OWNED BY public.discord_server.id;\n\n\n--\n-- Name: event; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.event (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    name character varying NOT NULL,\n    \"descriptionUrl\" character varying,\n    description character varying,\n    type character varying DEFAULT 'regular'::character varying NOT NULL,\n    \"disciplineId\" integer\n);\n\n\nALTER TABLE public.event OWNER TO rs_master;\n\n--\n-- Name: event_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.event_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.event_id_seq OWNER TO rs_master;\n\n--\n-- Name: event_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.event_id_seq OWNED BY public.event.id;\n\n\n--\n-- Name: feedback; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.feedback (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"badgeId\" character varying DEFAULT 'Thank_you'::character varying,\n    \"fromUserId\" integer NOT NULL,\n    \"toUserId\" integer NOT NULL,\n    \"courseId\" integer,\n    comment character varying\n);\n\n\nALTER TABLE public.feedback OWNER TO rs_master;\n\n--\n-- Name: feedback_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.feedback_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.feedback_id_seq OWNER TO rs_master;\n\n--\n-- Name: feedback_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.feedback_id_seq OWNED BY public.feedback.id;\n\n\n--\n-- Name: history; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.history (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    event character varying NOT NULL,\n    \"entityId\" integer,\n    operation character varying NOT NULL,\n    update json,\n    previous json\n);\n\n\nALTER TABLE public.history OWNER TO rs_master;\n\n--\n-- Name: history_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.history_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.history_id_seq OWNER TO rs_master;\n\n--\n-- Name: history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.history_id_seq OWNED BY public.history.id;\n\n\n--\n-- Name: interview_question; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.interview_question (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    title character varying NOT NULL,\n    question character varying NOT NULL\n);\n\n\nALTER TABLE public.interview_question OWNER TO rs_master;\n\n--\n-- Name: interview_question_categories_interview_question_category; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.interview_question_categories_interview_question_category (\n    \"interviewQuestionId\" integer NOT NULL,\n    \"interviewQuestionCategoryId\" integer NOT NULL\n);\n\n\nALTER TABLE public.interview_question_categories_interview_question_category OWNER TO rs_master;\n\n--\n-- Name: interview_question_category; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.interview_question_category (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    name character varying NOT NULL\n);\n\n\nALTER TABLE public.interview_question_category OWNER TO rs_master;\n\n--\n-- Name: interview_question_category_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.interview_question_category_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.interview_question_category_id_seq OWNER TO rs_master;\n\n--\n-- Name: interview_question_category_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.interview_question_category_id_seq OWNED BY public.interview_question_category.id;\n\n\n--\n-- Name: interview_question_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.interview_question_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.interview_question_id_seq OWNER TO rs_master;\n\n--\n-- Name: interview_question_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.interview_question_id_seq OWNED BY public.interview_question.id;\n\n\n--\n-- Name: login_state; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.login_state (\n    id character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    data text NOT NULL,\n    \"userId\" integer,\n    expires timestamp without time zone\n);\n\n\nALTER TABLE public.login_state OWNER TO rs_master;\n\n--\n-- Name: mentor; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.mentor (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"maxStudentsLimit\" integer,\n    \"courseId\" integer,\n    \"userId\" integer NOT NULL,\n    \"studentsPreference\" character varying,\n    \"isExpelled\" boolean DEFAULT false NOT NULL\n);\n\n\nALTER TABLE public.mentor OWNER TO rs_master;\n\n--\n-- Name: mentor_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.mentor_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.mentor_id_seq OWNER TO rs_master;\n\n--\n-- Name: mentor_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.mentor_id_seq OWNED BY public.mentor.id;\n\n\n--\n-- Name: mentor_registry; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.mentor_registry (\n    id integer NOT NULL,\n    \"userId\" integer NOT NULL,\n    \"preferedCourses\" text DEFAULT ''::text NOT NULL,\n    \"maxStudentsLimit\" integer NOT NULL,\n    \"englishMentoring\" boolean NOT NULL,\n    \"preferedStudentsLocation\" character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"technicalMentoring\" text DEFAULT ''::text NOT NULL,\n    \"preselectedCourses\" text DEFAULT ''::text NOT NULL,\n    canceled boolean DEFAULT false NOT NULL,\n    \"languagesMentoring\" text DEFAULT ''::text NOT NULL,\n    comment character varying,\n    \"receivedDate\" timestamp without time zone,\n    \"sendDate\" timestamp without time zone\n);\n\n\nALTER TABLE public.mentor_registry OWNER TO rs_master;\n\n--\n-- Name: mentor_registry_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.mentor_registry_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.mentor_registry_id_seq OWNER TO rs_master;\n\n--\n-- Name: mentor_registry_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.mentor_registry_id_seq OWNED BY public.mentor_registry.id;\n\n\n--\n-- Name: migrations; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.migrations (\n    id integer NOT NULL,\n    \"timestamp\" bigint NOT NULL,\n    name character varying NOT NULL\n);\n\n\nALTER TABLE public.migrations OWNER TO rs_master;\n\n--\n-- Name: migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.migrations_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.migrations_id_seq OWNER TO rs_master;\n\n--\n-- Name: migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id;\n\n\n--\n-- Name: notification; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.notification (\n    id character varying NOT NULL,\n    name character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    type character varying DEFAULT 'event'::character varying NOT NULL,\n    enabled boolean DEFAULT false NOT NULL,\n    \"parentId\" character varying\n);\n\n\nALTER TABLE public.notification OWNER TO rs_master;\n\n--\n-- Name: notification_channel; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.notification_channel (\n    id character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL\n);\n\n\nALTER TABLE public.notification_channel OWNER TO rs_master;\n\n--\n-- Name: notification_channel_settings; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.notification_channel_settings (\n    \"notificationId\" character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"channelId\" character varying NOT NULL,\n    template text\n);\n\n\nALTER TABLE public.notification_channel_settings OWNER TO rs_master;\n\n--\n-- Name: notification_user_connection; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.notification_user_connection (\n    \"userId\" integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"channelId\" character varying NOT NULL,\n    \"externalId\" character varying NOT NULL,\n    enabled boolean DEFAULT true NOT NULL\n);\n\n\nALTER TABLE public.notification_user_connection OWNER TO rs_master;\n\n--\n-- Name: notification_user_settings; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.notification_user_settings (\n    \"notificationId\" character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    enabled boolean NOT NULL,\n    \"userId\" integer NOT NULL,\n    \"channelId\" character varying NOT NULL\n);\n\n\nALTER TABLE public.notification_user_settings OWNER TO rs_master;\n\n--\n-- Name: private_feedback; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.private_feedback (\n    id integer NOT NULL,\n    comment character varying,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseId\" integer,\n    \"fromUserId\" integer,\n    \"toUserId\" integer\n);\n\n\nALTER TABLE public.private_feedback OWNER TO rs_master;\n\n--\n-- Name: private_feedback_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.private_feedback_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.private_feedback_id_seq OWNER TO rs_master;\n\n--\n-- Name: private_feedback_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.private_feedback_id_seq OWNED BY public.private_feedback.id;\n\n\n--\n-- Name: profile_permissions; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.profile_permissions (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"userId\" integer NOT NULL,\n    \"isProfileVisible\" json DEFAULT '{\"all\":false}'::json NOT NULL,\n    \"isAboutVisible\" json DEFAULT '{\"mentor\":false,\"student\":false,\"all\":false}'::json NOT NULL,\n    \"isEducationVisible\" json DEFAULT '{\"mentor\":false,\"student\":false,\"all\":false}'::json NOT NULL,\n    \"isEnglishVisible\" json DEFAULT '{\"student\":false,\"all\":false}'::json NOT NULL,\n    \"isEmailVisible\" json DEFAULT '{\"student\":true,\"all\":false}'::json NOT NULL,\n    \"isTelegramVisible\" json DEFAULT '{\"student\":true,\"all\":false}'::json NOT NULL,\n    \"isSkypeVisible\" json DEFAULT '{\"student\":true,\"all\":false}'::json NOT NULL,\n    \"isPhoneVisible\" json DEFAULT '{\"student\":true,\"all\":false}'::json NOT NULL,\n    \"isContactsNotesVisible\" json DEFAULT '{\"student\":true,\"all\":false}'::json NOT NULL,\n    \"isLinkedInVisible\" json DEFAULT '{\"mentor\":false,\"student\":false,\"all\":false}'::json NOT NULL,\n    \"isPublicFeedbackVisible\" json DEFAULT '{\"mentor\":false,\"student\":false,\"all\":false}'::json NOT NULL,\n    \"isMentorStatsVisible\" json DEFAULT '{\"mentor\":false,\"student\":false,\"all\":false}'::json NOT NULL,\n    \"isStudentStatsVisible\" json DEFAULT '{\"student\":false,\"all\":false}'::json NOT NULL\n);\n\n\nALTER TABLE public.profile_permissions OWNER TO rs_master;\n\n--\n-- Name: profile_permissions_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.profile_permissions_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.profile_permissions_id_seq OWNER TO rs_master;\n\n--\n-- Name: profile_permissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.profile_permissions_id_seq OWNED BY public.profile_permissions.id;\n\n\n--\n-- Name: prompt; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.prompt (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    type character varying(256) NOT NULL,\n    text character varying NOT NULL,\n    temperature double precision NOT NULL\n);\n\n\nALTER TABLE public.prompt OWNER TO rs_master;\n\n--\n-- Name: prompt_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.prompt_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.prompt_id_seq OWNER TO rs_master;\n\n--\n-- Name: prompt_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.prompt_id_seq OWNED BY public.prompt.id;\n\n\n--\n-- Name: registry; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.registry (\n    id integer NOT NULL,\n    type character varying NOT NULL,\n    status character varying DEFAULT 'pending'::character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"userId\" integer NOT NULL,\n    \"courseId\" integer NOT NULL,\n    attributes json DEFAULT '{}'::json NOT NULL\n);\n\n\nALTER TABLE public.registry OWNER TO rs_master;\n\n--\n-- Name: registry_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.registry_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.registry_id_seq OWNER TO rs_master;\n\n--\n-- Name: registry_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.registry_id_seq OWNED BY public.registry.id;\n\n\n--\n-- Name: repository_event; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.repository_event (\n    id integer NOT NULL,\n    \"repositoryUrl\" character varying NOT NULL,\n    action character varying NOT NULL,\n    \"githubId\" character varying NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"userId\" integer\n);\n\n\nALTER TABLE public.repository_event OWNER TO rs_master;\n\n--\n-- Name: repository_event_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.repository_event_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.repository_event_id_seq OWNER TO rs_master;\n\n--\n-- Name: repository_event_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.repository_event_id_seq OWNED BY public.repository_event.id;\n\n\n--\n-- Name: resume; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.resume (\n    id integer NOT NULL,\n    \"githubId\" character varying(256) NOT NULL,\n    name character varying(256),\n    \"selfIntroLink\" character varying(256),\n    \"startFrom\" character varying(32),\n    \"fullTime\" boolean DEFAULT false NOT NULL,\n    expires numeric,\n    \"militaryService\" character varying(32),\n    \"englishLevel\" character varying(8),\n    \"avatarLink\" character varying(512),\n    \"desiredPosition\" character varying(256),\n    notes text,\n    phone character varying(32),\n    email character varying(256),\n    skype character varying(128),\n    telegram character varying(128),\n    linkedin character varying(512),\n    locations character varying(512),\n    \"githubUsername\" character varying(256),\n    website character varying(512),\n    \"isHidden\" boolean DEFAULT false NOT NULL,\n    \"visibleCourses\" integer[] DEFAULT '{}'::integer[] NOT NULL,\n    uuid uuid DEFAULT public.uuid_generate_v4(),\n    \"userId\" integer NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL\n);\n\n\nALTER TABLE public.resume OWNER TO rs_master;\n\n--\n-- Name: resume_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.resume_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.resume_id_seq OWNER TO rs_master;\n\n--\n-- Name: resume_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.resume_id_seq OWNED BY public.resume.id;\n\n\n--\n-- Name: stage; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.stage (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    name character varying NOT NULL,\n    \"courseId\" integer NOT NULL,\n    status character varying DEFAULT 'OPEN'::character varying NOT NULL,\n    \"startDate\" timestamp with time zone,\n    \"endDate\" timestamp with time zone\n);\n\n\nALTER TABLE public.stage OWNER TO rs_master;\n\n--\n-- Name: stage_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.stage_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.stage_id_seq OWNER TO rs_master;\n\n--\n-- Name: stage_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.stage_id_seq OWNED BY public.stage.id;\n\n\n--\n-- Name: stage_interview; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.stage_interview (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"mentorId\" integer NOT NULL,\n    \"stageId\" integer,\n    \"isCompleted\" boolean DEFAULT false NOT NULL,\n    decision character varying,\n    \"isGoodCandidate\" boolean,\n    \"courseId\" integer,\n    \"courseTaskId\" integer,\n    \"isCanceled\" boolean DEFAULT false NOT NULL,\n    score integer\n);\n\n\nALTER TABLE public.stage_interview OWNER TO rs_master;\n\n--\n-- Name: stage_interview_feedback; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.stage_interview_feedback (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"stageInterviewId\" integer NOT NULL,\n    json character varying NOT NULL,\n    version integer\n);\n\n\nALTER TABLE public.stage_interview_feedback OWNER TO rs_master;\n\n--\n-- Name: stage_interview_feedback_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.stage_interview_feedback_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.stage_interview_feedback_id_seq OWNER TO rs_master;\n\n--\n-- Name: stage_interview_feedback_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.stage_interview_feedback_id_seq OWNED BY public.stage_interview_feedback.id;\n\n\n--\n-- Name: stage_interview_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.stage_interview_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.stage_interview_id_seq OWNER TO rs_master;\n\n--\n-- Name: stage_interview_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.stage_interview_id_seq OWNED BY public.stage_interview.id;\n\n\n--\n-- Name: stage_interview_student; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.stage_interview_student (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"courseId\" integer\n);\n\n\nALTER TABLE public.stage_interview_student OWNER TO rs_master;\n\n--\n-- Name: stage_interview_student_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.stage_interview_student_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.stage_interview_student_id_seq OWNER TO rs_master;\n\n--\n-- Name: stage_interview_student_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.stage_interview_student_id_seq OWNED BY public.stage_interview_student.id;\n\n\n--\n-- Name: student; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.student (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"isExpelled\" boolean DEFAULT false NOT NULL,\n    \"expellingReason\" character varying,\n    \"courseCompleted\" boolean DEFAULT false NOT NULL,\n    \"isTopPerformer\" boolean DEFAULT false NOT NULL,\n    \"preferedMentorGithubId\" character varying,\n    \"readyFullTime\" boolean,\n    \"courseId\" integer,\n    \"userId\" integer NOT NULL,\n    \"mentorId\" integer,\n    \"cvUrl\" character varying,\n    \"hiredById\" character varying,\n    \"hiredByName\" character varying,\n    \"isFailed\" boolean DEFAULT false NOT NULL,\n    \"totalScore\" double precision DEFAULT 0 NOT NULL,\n    \"startDate\" timestamp with time zone DEFAULT '1970-01-01 00:00:00+00'::timestamp with time zone NOT NULL,\n    \"endDate\" timestamp with time zone,\n    repository character varying,\n    \"totalScoreChangeDate\" timestamp with time zone,\n    \"repositoryLastActivityDate\" timestamp with time zone,\n    rank integer DEFAULT 999999 NOT NULL,\n    \"crossCheckScore\" double precision DEFAULT '0'::double precision NOT NULL,\n    \"unassigningComment\" text,\n    mentoring boolean DEFAULT true\n);\n\n\nALTER TABLE public.student OWNER TO rs_master;\n\n--\n-- Name: student_feedback; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.student_feedback (\n    id integer NOT NULL,\n    created_date timestamp without time zone DEFAULT now() NOT NULL,\n    updated_date timestamp without time zone DEFAULT now() NOT NULL,\n    deleted_date timestamp without time zone,\n    student_id integer NOT NULL,\n    mentor_id integer,\n    content json NOT NULL,\n    recommendation character varying(64) NOT NULL,\n    english_level character varying(8),\n    author_id integer NOT NULL\n);\n\n\nALTER TABLE public.student_feedback OWNER TO rs_master;\n\n--\n-- Name: student_feedback_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.student_feedback_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.student_feedback_id_seq OWNER TO rs_master;\n\n--\n-- Name: student_feedback_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.student_feedback_id_seq OWNED BY public.student_feedback.id;\n\n\n--\n-- Name: student_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.student_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.student_id_seq OWNER TO rs_master;\n\n--\n-- Name: student_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.student_id_seq OWNED BY public.student.id;\n\n\n--\n-- Name: student_team_distribution_team_distribution; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.student_team_distribution_team_distribution (\n    \"studentId\" integer NOT NULL,\n    \"teamDistributionId\" integer NOT NULL\n);\n\n\nALTER TABLE public.student_team_distribution_team_distribution OWNER TO rs_master;\n\n--\n-- Name: student_teams_team; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.student_teams_team (\n    \"studentId\" integer NOT NULL,\n    \"teamId\" integer NOT NULL\n);\n\n\nALTER TABLE public.student_teams_team OWNER TO rs_master;\n\n--\n-- Name: task; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    name character varying NOT NULL,\n    \"descriptionUrl\" character varying,\n    description character varying,\n    verification character varying,\n    \"githubPrRequired\" boolean,\n    \"useJury\" boolean DEFAULT false NOT NULL,\n    \"allowStudentArtefacts\" boolean DEFAULT false NOT NULL,\n    \"githubRepoName\" character varying,\n    \"sourceGithubRepoUrl\" character varying,\n    type character varying,\n    tags text DEFAULT ''::text NOT NULL,\n    attributes json DEFAULT '{}'::json NOT NULL,\n    skills text DEFAULT ''::text NOT NULL,\n    \"disciplineId\" integer,\n    \"criteriaId\" integer,\n    \"deletedDate\" timestamp without time zone\n);\n\n\nALTER TABLE public.task OWNER TO rs_master;\n\n--\n-- Name: task_artefact; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_artefact (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"videoUrl\" character varying,\n    \"presentationUrl\" character varying,\n    comment character varying\n);\n\n\nALTER TABLE public.task_artefact OWNER TO rs_master;\n\n--\n-- Name: task_artefact_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_artefact_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_artefact_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_artefact_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_artefact_id_seq OWNED BY public.task_artefact.id;\n\n\n--\n-- Name: task_checker; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_checker (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"mentorId\" integer NOT NULL\n);\n\n\nALTER TABLE public.task_checker OWNER TO rs_master;\n\n--\n-- Name: task_checker_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_checker_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_checker_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_checker_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_checker_id_seq OWNED BY public.task_checker.id;\n\n\n--\n-- Name: task_criteria; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_criteria (\n    \"taskId\" integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    criteria jsonb DEFAULT '[]'::jsonb NOT NULL\n);\n\n\nALTER TABLE public.task_criteria OWNER TO rs_master;\n\n--\n-- Name: task_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_id_seq OWNED BY public.task.id;\n\n\n--\n-- Name: task_interview_result; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_interview_result (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"mentorId\" integer NOT NULL,\n    \"formAnswers\" json DEFAULT '[]'::json NOT NULL,\n    score integer,\n    comment character varying\n);\n\n\nALTER TABLE public.task_interview_result OWNER TO rs_master;\n\n--\n-- Name: task_interview_result_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_interview_result_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_interview_result_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_interview_result_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_interview_result_id_seq OWNED BY public.task_interview_result.id;\n\n\n--\n-- Name: task_interview_student; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_interview_student (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"courseId\" integer,\n    \"courseTaskId\" integer NOT NULL\n);\n\n\nALTER TABLE public.task_interview_student OWNER TO rs_master;\n\n--\n-- Name: task_interview_student_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_interview_student_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_interview_student_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_interview_student_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_interview_student_id_seq OWNED BY public.task_interview_student.id;\n\n\n--\n-- Name: task_result; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_result (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"githubPrUrl\" character varying,\n    \"githubRepoUrl\" character varying,\n    score integer NOT NULL,\n    comment character varying,\n    \"studentId\" integer NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    \"historicalScores\" json DEFAULT '[]'::json NOT NULL,\n    \"juryScores\" json DEFAULT '[]'::json NOT NULL,\n    \"lastCheckerId\" integer\n);\n\n\nALTER TABLE public.task_result OWNER TO rs_master;\n\n--\n-- Name: task_result_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_result_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_result_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_result_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_result_id_seq OWNED BY public.task_result.id;\n\n\n--\n-- Name: task_solution; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_solution (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    \"studentId\" integer NOT NULL,\n    url character varying NOT NULL,\n    review json DEFAULT '[]'::json NOT NULL,\n    comments json DEFAULT '[]'::json NOT NULL\n);\n\n\nALTER TABLE public.task_solution OWNER TO rs_master;\n\n--\n-- Name: task_solution_checker; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_solution_checker (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    \"taskSolutionId\" integer NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"checkerId\" integer NOT NULL\n);\n\n\nALTER TABLE public.task_solution_checker OWNER TO rs_master;\n\n--\n-- Name: task_solution_checker_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_solution_checker_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_solution_checker_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_solution_checker_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_solution_checker_id_seq OWNED BY public.task_solution_checker.id;\n\n\n--\n-- Name: task_solution_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_solution_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_solution_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_solution_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_solution_id_seq OWNED BY public.task_solution.id;\n\n\n--\n-- Name: task_solution_result; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_solution_result (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"checkerId\" integer NOT NULL,\n    score integer NOT NULL,\n    \"historicalScores\" json DEFAULT '[]'::json NOT NULL,\n    comment character varying,\n    anonymous boolean DEFAULT true NOT NULL,\n    review json DEFAULT '[]'::json NOT NULL,\n    messages json DEFAULT '[]'::json NOT NULL\n);\n\n\nALTER TABLE public.task_solution_result OWNER TO rs_master;\n\n--\n-- Name: task_solution_result_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_solution_result_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_solution_result_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_solution_result_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_solution_result_id_seq OWNED BY public.task_solution_result.id;\n\n\n--\n-- Name: task_verification; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.task_verification (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"courseTaskId\" integer NOT NULL,\n    details character varying,\n    status character varying DEFAULT 'pending'::character varying NOT NULL,\n    score integer NOT NULL,\n    metadata json DEFAULT '[]'::json NOT NULL,\n    answers json DEFAULT '[]'::json NOT NULL\n);\n\n\nALTER TABLE public.task_verification OWNER TO rs_master;\n\n--\n-- Name: task_verification_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.task_verification_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.task_verification_id_seq OWNER TO rs_master;\n\n--\n-- Name: task_verification_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.task_verification_id_seq OWNED BY public.task_verification.id;\n\n\n--\n-- Name: team; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.team (\n    id integer NOT NULL,\n    name character varying NOT NULL,\n    description character varying DEFAULT ''::character varying NOT NULL,\n    \"chatLink\" character varying,\n    password character varying NOT NULL,\n    \"teamDistributionId\" integer NOT NULL,\n    \"teamLeadId\" integer\n);\n\n\nALTER TABLE public.team OWNER TO rs_master;\n\n--\n-- Name: team_distribution; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.team_distribution (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"courseId\" integer,\n    \"startDate\" timestamp with time zone NOT NULL,\n    \"endDate\" timestamp with time zone NOT NULL,\n    name character varying NOT NULL,\n    description character varying DEFAULT ''::character varying NOT NULL,\n    \"minTotalScore\" integer DEFAULT 0 NOT NULL,\n    \"descriptionUrl\" character varying DEFAULT ''::character varying NOT NULL,\n    \"minTeamSize\" integer DEFAULT 2 NOT NULL,\n    \"maxTeamSize\" integer DEFAULT 4 NOT NULL,\n    \"strictTeamSize\" integer DEFAULT 3 NOT NULL,\n    \"strictTeamSizeMode\" boolean DEFAULT true NOT NULL\n);\n\n\nALTER TABLE public.team_distribution OWNER TO rs_master;\n\n--\n-- Name: team_distribution_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.team_distribution_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.team_distribution_id_seq OWNER TO rs_master;\n\n--\n-- Name: team_distribution_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.team_distribution_id_seq OWNED BY public.team_distribution.id;\n\n\n--\n-- Name: team_distribution_student; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.team_distribution_student (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"studentId\" integer NOT NULL,\n    \"courseId\" integer,\n    \"teamDistributionId\" integer NOT NULL,\n    distributed boolean DEFAULT false NOT NULL,\n    active boolean DEFAULT true NOT NULL\n);\n\n\nALTER TABLE public.team_distribution_student OWNER TO rs_master;\n\n--\n-- Name: team_distribution_student_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.team_distribution_student_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.team_distribution_student_id_seq OWNER TO rs_master;\n\n--\n-- Name: team_distribution_student_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.team_distribution_student_id_seq OWNED BY public.team_distribution_student.id;\n\n\n--\n-- Name: team_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.team_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.team_id_seq OWNER TO rs_master;\n\n--\n-- Name: team_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.team_id_seq OWNED BY public.team.id;\n\n\n--\n-- Name: typeorm_metadata; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.typeorm_metadata (\n    type character varying NOT NULL,\n    database character varying,\n    schema character varying,\n    \"table\" character varying,\n    name character varying,\n    value text\n);\n\n\nALTER TABLE public.typeorm_metadata OWNER TO rs_master;\n\n--\n-- Name: user; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.\"user\" (\n    id integer NOT NULL,\n    \"githubId\" character varying NOT NULL,\n    \"firstName\" character varying,\n    \"lastName\" character varying,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"firstNameNative\" character varying,\n    \"lastNameNative\" character varying,\n    \"tshirtSize\" character varying,\n    \"tshirtFashion\" character varying,\n    \"dateOfBirth\" date,\n    \"locationName\" character varying,\n    \"locationId\" character varying,\n    \"educationHistory\" json DEFAULT '[]'::json NOT NULL,\n    \"employmentHistory\" json DEFAULT '[]'::json NOT NULL,\n    \"contactsEpamEmail\" character varying,\n    \"contactsPhone\" character varying,\n    \"contactsEmail\" character varying,\n    \"externalAccounts\" json DEFAULT '[]'::json NOT NULL,\n    \"epamApplicantId\" character varying,\n    activist boolean,\n    \"englishLevel\" character varying,\n    \"lastActivityTime\" bigint DEFAULT 0 NOT NULL,\n    \"isActive\" boolean DEFAULT true NOT NULL,\n    \"primaryEmail\" character varying,\n    \"contactsTelegram\" character varying,\n    \"contactsSkype\" character varying,\n    \"contactsNotes\" character varying,\n    \"aboutMyself\" character varying,\n    \"contactsLinkedIn\" character varying,\n    \"profilePermissionsId\" integer,\n    \"countryName\" character varying,\n    \"cityName\" character varying,\n    \"opportunitiesConsent\" boolean DEFAULT false NOT NULL,\n    \"cvLink\" text,\n    \"militaryService\" text,\n    discord json,\n    \"providerUserId\" character varying(64),\n    provider character varying(32),\n    \"contactsWhatsApp\" character varying,\n    languages text[] DEFAULT '{}'::text[] NOT NULL,\n    obfuscated boolean DEFAULT false NOT NULL,\n    contributor_id integer\n);\n\n\nALTER TABLE public.\"user\" OWNER TO rs_master;\n\n--\n-- Name: user_group; Type: TABLE; Schema: public; Owner: rs_master\n--\n\nCREATE TABLE public.user_group (\n    id integer NOT NULL,\n    \"createdDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    \"updatedDate\" timestamp without time zone DEFAULT now() NOT NULL,\n    name character varying NOT NULL,\n    users integer[] NOT NULL,\n    roles text[] NOT NULL\n);\n\n\nALTER TABLE public.user_group OWNER TO rs_master;\n\n--\n-- Name: user_group_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.user_group_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.user_group_id_seq OWNER TO rs_master;\n\n--\n-- Name: user_group_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.user_group_id_seq OWNED BY public.user_group.id;\n\n\n--\n-- Name: user_id_seq; Type: SEQUENCE; Schema: public; Owner: rs_master\n--\n\nCREATE SEQUENCE public.user_id_seq\n    AS integer\n    START WITH 1\n    INCREMENT BY 1\n    NO MINVALUE\n    NO MAXVALUE\n    CACHE 1;\n\n\nALTER TABLE public.user_id_seq OWNER TO rs_master;\n\n--\n-- Name: user_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: rs_master\n--\n\nALTER SEQUENCE public.user_id_seq OWNED BY public.\"user\".id;\n\n\n--\n-- Name: alert id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.alert ALTER COLUMN id SET DEFAULT nextval('public.alert_id_seq'::regclass);\n\n\n--\n-- Name: certificate id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.certificate ALTER COLUMN id SET DEFAULT nextval('public.certificate_id_seq'::regclass);\n\n\n--\n-- Name: consent id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.consent ALTER COLUMN id SET DEFAULT nextval('public.consent_id_seq'::regclass);\n\n\n--\n-- Name: contributor id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.contributor ALTER COLUMN id SET DEFAULT nextval('public.contributor_id_seq'::regclass);\n\n\n--\n-- Name: course id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course ALTER COLUMN id SET DEFAULT nextval('public.course_id_seq'::regclass);\n\n\n--\n-- Name: course_event id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_event ALTER COLUMN id SET DEFAULT nextval('public.course_event_id_seq'::regclass);\n\n\n--\n-- Name: course_manager id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_manager ALTER COLUMN id SET DEFAULT nextval('public.course_manager_id_seq'::regclass);\n\n\n--\n-- Name: course_task id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_task ALTER COLUMN id SET DEFAULT nextval('public.course_task_id_seq'::regclass);\n\n\n--\n-- Name: course_user id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_user ALTER COLUMN id SET DEFAULT nextval('public.course_user_id_seq'::regclass);\n\n\n--\n-- Name: cv id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.cv ALTER COLUMN id SET DEFAULT nextval('public.cv_id_seq'::regclass);\n\n\n--\n-- Name: discipline id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.discipline ALTER COLUMN id SET DEFAULT nextval('public.discipline_id_seq'::regclass);\n\n\n--\n-- Name: discord_server id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.discord_server ALTER COLUMN id SET DEFAULT nextval('public.discord_server_id_seq'::regclass);\n\n\n--\n-- Name: event id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.event ALTER COLUMN id SET DEFAULT nextval('public.event_id_seq'::regclass);\n\n\n--\n-- Name: feedback id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.feedback ALTER COLUMN id SET DEFAULT nextval('public.feedback_id_seq'::regclass);\n\n\n--\n-- Name: history id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.history ALTER COLUMN id SET DEFAULT nextval('public.history_id_seq'::regclass);\n\n\n--\n-- Name: interview_question id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question ALTER COLUMN id SET DEFAULT nextval('public.interview_question_id_seq'::regclass);\n\n\n--\n-- Name: interview_question_category id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question_category ALTER COLUMN id SET DEFAULT nextval('public.interview_question_category_id_seq'::regclass);\n\n\n--\n-- Name: mentor id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor ALTER COLUMN id SET DEFAULT nextval('public.mentor_id_seq'::regclass);\n\n\n--\n-- Name: mentor_registry id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor_registry ALTER COLUMN id SET DEFAULT nextval('public.mentor_registry_id_seq'::regclass);\n\n\n--\n-- Name: migrations id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass);\n\n\n--\n-- Name: private_feedback id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.private_feedback ALTER COLUMN id SET DEFAULT nextval('public.private_feedback_id_seq'::regclass);\n\n\n--\n-- Name: profile_permissions id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.profile_permissions ALTER COLUMN id SET DEFAULT nextval('public.profile_permissions_id_seq'::regclass);\n\n\n--\n-- Name: prompt id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.prompt ALTER COLUMN id SET DEFAULT nextval('public.prompt_id_seq'::regclass);\n\n\n--\n-- Name: registry id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.registry ALTER COLUMN id SET DEFAULT nextval('public.registry_id_seq'::regclass);\n\n\n--\n-- Name: repository_event id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.repository_event ALTER COLUMN id SET DEFAULT nextval('public.repository_event_id_seq'::regclass);\n\n\n--\n-- Name: resume id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.resume ALTER COLUMN id SET DEFAULT nextval('public.resume_id_seq'::regclass);\n\n\n--\n-- Name: stage id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage ALTER COLUMN id SET DEFAULT nextval('public.stage_id_seq'::regclass);\n\n\n--\n-- Name: stage_interview id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview ALTER COLUMN id SET DEFAULT nextval('public.stage_interview_id_seq'::regclass);\n\n\n--\n-- Name: stage_interview_feedback id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_feedback ALTER COLUMN id SET DEFAULT nextval('public.stage_interview_feedback_id_seq'::regclass);\n\n\n--\n-- Name: stage_interview_student id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_student ALTER COLUMN id SET DEFAULT nextval('public.stage_interview_student_id_seq'::regclass);\n\n\n--\n-- Name: student id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student ALTER COLUMN id SET DEFAULT nextval('public.student_id_seq'::regclass);\n\n\n--\n-- Name: student_feedback id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_feedback ALTER COLUMN id SET DEFAULT nextval('public.student_feedback_id_seq'::regclass);\n\n\n--\n-- Name: task id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task ALTER COLUMN id SET DEFAULT nextval('public.task_id_seq'::regclass);\n\n\n--\n-- Name: task_artefact id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_artefact ALTER COLUMN id SET DEFAULT nextval('public.task_artefact_id_seq'::regclass);\n\n\n--\n-- Name: task_checker id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_checker ALTER COLUMN id SET DEFAULT nextval('public.task_checker_id_seq'::regclass);\n\n\n--\n-- Name: task_interview_result id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_result ALTER COLUMN id SET DEFAULT nextval('public.task_interview_result_id_seq'::regclass);\n\n\n--\n-- Name: task_interview_student id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_student ALTER COLUMN id SET DEFAULT nextval('public.task_interview_student_id_seq'::regclass);\n\n\n--\n-- Name: task_result id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_result ALTER COLUMN id SET DEFAULT nextval('public.task_result_id_seq'::regclass);\n\n\n--\n-- Name: task_solution id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution ALTER COLUMN id SET DEFAULT nextval('public.task_solution_id_seq'::regclass);\n\n\n--\n-- Name: task_solution_checker id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_checker ALTER COLUMN id SET DEFAULT nextval('public.task_solution_checker_id_seq'::regclass);\n\n\n--\n-- Name: task_solution_result id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_result ALTER COLUMN id SET DEFAULT nextval('public.task_solution_result_id_seq'::regclass);\n\n\n--\n-- Name: task_verification id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_verification ALTER COLUMN id SET DEFAULT nextval('public.task_verification_id_seq'::regclass);\n\n\n--\n-- Name: team id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team ALTER COLUMN id SET DEFAULT nextval('public.team_id_seq'::regclass);\n\n\n--\n-- Name: team_distribution id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution ALTER COLUMN id SET DEFAULT nextval('public.team_distribution_id_seq'::regclass);\n\n\n--\n-- Name: team_distribution_student id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution_student ALTER COLUMN id SET DEFAULT nextval('public.team_distribution_student_id_seq'::regclass);\n\n\n--\n-- Name: user id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\" ALTER COLUMN id SET DEFAULT nextval('public.user_id_seq'::regclass);\n\n\n--\n-- Name: user_group id; Type: DEFAULT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.user_group ALTER COLUMN id SET DEFAULT nextval('public.user_group_id_seq'::regclass);\n\n\n--\n-- Data for Name: alert; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.alert (id, \"createdDate\", \"updatedDate\", text, \"courseId\", enabled, type) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: certificate; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.certificate (id, \"createdDate\", \"updatedDate\", \"publicId\", \"studentId\", \"s3Bucket\", \"s3Key\", \"issueDate\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: consent; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.consent (id, \"createdDate\", \"updatedDate\", \"channelValue\", \"channelType\", \"optIn\", username) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: contributor; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.contributor (id, created_date, updated_date, deleted_date, user_id, description) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: course; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.course (id, \"createdDate\", \"updatedDate\", name, year, \"primarySkillId\", \"primarySkillName\", \"locationName\", alias, completed, description, \"descriptionUrl\", planned, \"startDate\", \"endDate\", \"fullName\", \"registrationEndDate\", \"inviteOnly\", \"discordServerId\", \"certificateIssuer\", \"usePrivateRepositories\", \"personalMentoring\", logo, \"disciplineId\", \"minStudentsPerMentor\", \"certificateThreshold\", \"wearecommunityUrl\", \"personalMentoringStartDate\", \"personalMentoringEndDate\", \"certificateDisciplines\") FROM stdin;\n13\t2019-10-21 08:05:31.068833\t2020-04-06 15:14:44.116961\tRS 2020 Q1\t\\N\tjavascript\tJavaScript\t\\N\trs-2020-q1\tf\tJavascript / Frontend Курс.\\nВводное занятие - 2 февраля\\nОрганизационный вебинар начнется 2 февраля в 12:00 по минскому времени (GMT+3). Мы расскажем о процессе обучения в RS School и выдадим задания для первого этапа обучения.\\n\\nВебинар будет транслироваться на канале https://www.youtube.com/c/rollingscopesschool.\\nРекомендуем подписаться на канал и нажать колокольчик, чтобы не пропустить начало трансляции. \\n\\nЕсли у вас не будет возможности присоединиться к онлайн-трансляции, не переживайте! \\nЗапись вебинара будет размещена на канале в открытом доступе.\\n\\nОписание тренинга\\nОсновной сайт: https://rs.school/js/\\n\\nПодробная информация о школе:  https://docs.rs.school\t\\N\tf\t2020-02-02 09:01:56.398+00\t2020-07-31 08:01:56.398+00\tRolling Scopes School 2020 Q1: JavaScript/Front-end\t2020-04-15 08:40:46.24+00\tf\t\\N\t\\N\tt\tt\t\\N\t\\N\t2\t70\t\\N\t\\N\t\\N\t\\N\n11\t2019-08-27 07:36:13.565873\t2025-01-30 19:25:33.198749\tRS 2019 Q3\t\\N\tjavascript\tJavaScript\t\\N\trs-2019-q3\tt\tRS 2019 Q3\thttps://rs.school/courses/javascript-preschool-ru\tf\t2019-09-09 00:00:00+00\t2020-01-31 00:00:00+00\tRolling Scopes School 2019 Q3\t\\N\tf\t2\t\\N\tt\tt\t\\N\t1\t2\t70\t\\N\t2019-09-23 00:00:00+00\t2020-01-31 00:00:00+00\t\\N\n23\t2020-02-25 09:28:08.842897\t2025-05-06 07:02:01.153175\tTEST COURSE\t\\N\tjavascript\tJavaScript\t\\N\ttest-course\tf\tTEST COURSE\thttps://rs.school/courses/angular\tf\t2021-05-31 00:00:00+00\t2023-06-30 00:00:00+00\tTEST COURSE\t\\N\tt\t2\t\\N\tt\tt\tangular\t3\t2\t70\t\\N\t2021-05-31 00:00:00+00\t2023-06-30 00:00:00+00\t3\n\\.\n\n\n--\n-- Data for Name: course_event; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.course_event (id, \"createdDate\", \"updatedDate\", \"eventId\", \"courseId\", \"stageId\", date, \"time\", place, coordinator, comment, \"organizerId\", \"detailsUrl\", \"broadcastUrl\", \"dateTime\", special, duration, \"endTime\") FROM stdin;\n2\t2019-09-18 13:27:50.246961\t2019-09-29 22:36:05.391483\t2\t11\t\\N\t2019-09-13\t20:00:00+03\tYoutube Live\tSergey Shalyapin\t\t3961\t\\N\thttps://www.youtube.com/watch?v=2iCgf03rx1I\t2019-09-13 17:00:00+00\t\t\\N\t\\N\n10\t2019-09-19 08:06:38.306347\t2019-09-29 22:36:37.450973\t10\t11\t\\N\t2019-09-23\t12:00:41+03\tDiscord >> announcement\tDzianis Sheka\t\\N\t1328\t\\N\t\\N\t2019-09-23 09:00:41+00\t\t\\N\t\\N\n32\t2019-10-15 11:39:32.584641\t2019-10-15 11:48:54.960496\t34\t11\t\\N\t2019-11-05\t18:00:47+02\tYoutube Live\t\\N\t\\N\t2444\t\\N\t\\N\t2019-11-05 16:00:47+00\t\t\\N\t\\N\n9\t2019-09-19 08:01:19.744354\t2019-09-29 22:36:52.324181\t9\t11\t\\N\t2019-09-25\t20:00:39+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-09-25 17:00:39+00\t\t\\N\t\\N\n31\t2019-10-15 11:34:38.555567\t2019-10-15 11:49:16.569959\t33\t11\t\\N\t2019-11-04\t18:00:58+02\tYoutube Live\t\\N\t\\N\t1090\t\\N\t\\N\t2019-11-04 16:00:58+00\t\t\\N\t\\N\n28\t2019-10-14 14:01:29.842633\t2019-10-15 11:49:46.776533\t30\t11\t\\N\t2019-10-26\t06:00:16+02\tYoutube Live\t\\N\t\\N\t1328\t\\N\t\\N\t2019-10-26 04:00:16+00\t\t\\N\t\\N\n8\t2019-09-19 07:56:40.52603\t2019-09-29 22:37:40.366214\t8\t11\t\\N\t2019-09-23\t19:00:52+03\tYoutube Live\tAnton Bely, Pavel Razuvalov\t\\N\t2444\t\\N\t\\N\t2019-09-23 16:00:52+00\t\t\\N\t\\N\n11\t2019-09-19 08:15:42.170571\t2019-09-29 22:37:44.992841\t11\t11\t\\N\t2019-09-27\t20:00:54+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-09-27 17:00:54+00\t\t\\N\t\\N\n12\t2019-09-19 08:25:12.648501\t2019-09-29 22:37:58.19294\t12\t11\t\\N\t2019-09-30\t20:00:25+03\tYoutube Live\tViktoriya Vorozhun\t\\N\t2693\t\\N\t\\N\t2019-09-30 17:00:25+00\t\t\\N\t\\N\n13\t2019-09-19 08:27:16.85243\t2019-09-29 22:38:11.029827\t13\t11\t\\N\t2019-10-01\t20:00:32+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-10-01 17:00:32+00\t\t\\N\t\\N\n14\t2019-09-19 08:58:14.462505\t2019-09-29 22:38:15.108254\t14\t11\t\\N\t2019-10-02\t20:00:20+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-10-02 17:00:20+00\t\t\\N\t\\N\n15\t2019-09-19 09:01:29.234793\t2019-09-29 22:38:18.967522\t15\t11\t\\N\t2019-10-04\t20:00:18+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-10-04 17:00:18+00\t\t\\N\t\\N\n16\t2019-09-19 09:04:00.058482\t2019-09-29 22:38:24.161396\t16\t11\t\\N\t2019-10-07\t20:00:52+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-10-07 17:00:52+00\t\t\\N\t\\N\n17\t2019-09-19 09:10:34.094844\t2019-09-29 22:38:30.112146\t17\t11\t\\N\t2019-10-09\t20:00:19+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-10-09 17:00:19+00\t\t\\N\t\\N\n20\t2019-09-19 09:18:06.890022\t2019-09-29 22:38:43.832965\t20\t11\t\\N\t2019-10-11\t20:00:11+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-10-11 17:00:11+00\t\t\\N\t\\N\n18\t2019-09-19 09:15:26.553437\t2019-09-29 22:38:50.345041\t18\t11\t\\N\t2019-10-10\t19:00:17+03\tYoutube Live\tAnton Bely\t\\N\t2444\t\\N\t\\N\t2019-10-10 16:00:17+00\t\t\\N\t\\N\n19\t2019-09-19 09:16:44.454815\t2019-09-29 22:39:00.633497\t19\t11\t\\N\t2019-10-14\t19:00:17+03\tYoutube Live\tAnton Bely\t\\N\t2444\t\\N\t\\N\t2019-10-14 16:00:17+00\t\t\\N\t\\N\n21\t2019-09-19 09:20:29.557356\t2019-09-29 22:39:11.116858\t21\t11\t\\N\t2019-10-15\t20:00:42+03\tYoutube Live\tDzianis Sheka\t\\N\t1328\t\\N\t\\N\t2019-10-15 17:00:42+00\t\t\\N\t\\N\n22\t2019-09-19 09:27:50.542211\t2019-09-29 22:39:18.865932\t22\t11\t\\N\t2019-10-16\t20:00:03+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-10-16 17:00:03+00\t\t\\N\t\\N\n23\t2019-09-19 09:32:15.883718\t2019-09-29 22:39:31.265399\t23\t11\t\\N\t2019-10-18\t21:00:27+03\tYoutube Live\tDzmitry Varabei\t\\N\t2084\t\\N\t\\N\t2019-10-18 18:00:27+00\t\t\\N\t\\N\n25\t2019-10-14 13:38:33.036547\t2019-10-14 13:42:06.839216\t27\t11\t\\N\t2019-10-23\t\\N\tSelf-Studying\t\\N\t\t\\N\t\\N\thttps://www.youtube.com/watch?v=CAvqa6Lj_Rg&list=PLe--kalBDwjj81fKdWlvpLsizajSAK-lh&index=18\t2019-10-23 06:00:00+00\t\t\\N\t\\N\n26\t2019-10-14 13:51:28.629935\t2019-10-14 13:51:28.629935\t28\t11\t\\N\t2019-10-25\t18:00:11+02\tYoutube Live\t\\N\t\\N\t6776\t\\N\t\\N\t2019-10-25 16:00:11+00\t\t\\N\t\\N\n27\t2019-10-14 13:52:21.215211\t2019-10-14 13:53:05.258274\t29\t11\t\\N\t2019-10-25\t19:00:11+02\tYoutube Live\t\\N\t\\N\t6776\t\\N\t\\N\t2019-10-25 17:00:11+00\t\t\\N\t\\N\n29\t2019-10-14 14:10:56.691953\t2019-10-14 14:10:56.691953\t31\t11\t\\N\t2019-10-28\t\\N\tSelf-Studying\t\\N\t\\N\t\\N\t\\N\thttps://www.youtube.com/watch?v=H0XScE08hy8\t2019-10-28 06:00:00+00\t\t\\N\t\\N\n40\t2019-10-15 12:03:50.220574\t2019-10-15 12:03:50.220574\t41\t11\t\\N\t2019-11-25\t18:00:11+02\tYoutube Live\t\\N\t\\N\t2612\t\\N\t\\N\t2019-11-25 16:00:11+00\t\t\\N\t\\N\n41\t2019-10-15 12:05:11.008733\t2019-10-15 12:05:11.008733\t42\t11\t\\N\t2019-11-27\t\\N\tSelf-Studying\t\\N\t\\N\t\\N\t\\N\t\\N\t2019-11-27 06:00:00+00\t\t\\N\t\\N\n7\t2019-09-19 07:53:46.050222\t2019-09-29 13:41:51.301574\t7\t11\t\\N\t2019-09-21\t19:00:19+03\tTwich\tViktor Kovalev\t\\N\t4749\t\\N\t\\N\t2019-09-21 16:00:19+00\t\t\\N\t\\N\n6\t2019-09-18 13:38:43.043751\t2019-09-29 13:39:46.636834\t6\t11\t\\N\t2019-09-20\t20:00:00+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-09-20 17:00:00+00\t\t\\N\t\\N\n5\t2019-09-18 13:36:41.630053\t2019-09-29 13:39:56.720457\t5\t11\t\\N\t2019-09-18\t19:00:00+03\tYoutube Live\tAnton Bely\t\\N\t2444\t\\N\t\\N\t2019-09-18 16:00:00+00\t\t\\N\t\\N\n3\t2019-09-18 13:29:31.396492\t2019-09-29 13:39:36.356333\t3\t11\t\\N\t2019-09-14\t19:00:00+03\tTwich\tViktor Kovalev\t\\N\t4749\t\\N\t\\N\t2019-09-14 16:00:00+00\t\t\\N\t\\N\n1\t2019-09-18 13:25:10.446065\t2019-09-29 13:39:03.156556\t1\t11\t\\N\t2019-09-11\t20:00:00+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-09-11 17:00:00+00\t\t\\N\t\\N\n24\t2019-09-20 08:13:05.071726\t2019-09-29 22:35:36.7697\t24\t11\t\\N\t2019-09-09\t19:00:20+03\tYoutube Live\tDzmitry Varabei\t\\N\t2084\t\\N\t\\N\t2019-09-09 16:00:20+00\t\t\\N\t\\N\n30\t2019-10-14 14:14:48.89067\t2019-10-29 11:02:52.806588\t32\t11\t\\N\t2019-10-30\t17:00:34+01\tYoutube Live\t\\N\t\\N\t2549\t\\N\t\t2019-10-30 16:00:34+00\t\t\\N\t\\N\n56\t2019-11-13 07:58:22.70613\t2019-11-20 10:30:55.29591\t37\t11\t\\N\t2019-11-14\t17:00:09+01\tYoutube Live\t\\N\tPart 2\t4476\t\\N\t\\N\t2019-11-14 16:00:09+00\t\t\\N\t\\N\n34\t2019-10-15 11:47:37.525411\t2019-10-15 11:48:07.708192\t36\t11\t\\N\t2019-11-11\t\\N\tSelf-Studying\t\\N\t\\N\t\\N\t\\N\t\\N\t2019-11-11 06:00:00+00\t\t\\N\t\\N\n52\t2019-10-15 13:48:04.643143\t2019-10-15 13:48:04.643143\t49\t11\t\\N\t2019-12-18\t21:00:24+02\tYoutube Live\t\\N\t\\N\t1328\t\\N\t\\N\t2019-12-18 19:00:24+00\t\t\\N\t\\N\n54\t2019-10-16 09:35:26.303099\t2019-10-16 09:38:41.390559\t51\t11\t\\N\t2020-01-10\t21:00:30+02\tYoutube Live\t\\N\t\"Monday Mentor\"\t1328\t\\N\t\\N\t2020-01-10 19:00:30+00\t\t\\N\t\\N\n53\t2019-10-16 08:55:38.580672\t2019-10-16 09:38:47.92149\t50\t11\t\\N\t2019-12-30\t21:00:18+02\tYoutube Live\t\\N\t\"Monday Mentor\"\t1328\t\\N\t\\N\t2019-12-30 19:00:18+00\t\t\\N\t\\N\n43\t2019-10-15 13:19:27.167531\t2019-10-16 09:39:12.634215\t44\t11\t\\N\t2019-12-09\t18:00:39+02\tYoutube Live\t\\N\t\"Monday Mentor\"\t2612\t\\N\t\\N\t2019-12-09 16:00:39+00\t\t\\N\t\\N\n55\t2019-10-17 08:39:24.313773\t2019-10-17 08:59:37.788018\t52\t11\t\\N\t2019-10-22\t07:00:49+02\tDiscord >> announcement\t\\N\t\\N\t1328\t\\N\t\\N\t2019-10-22 05:00:49+00\t\t\\N\t\\N\n33\t2019-10-15 11:41:49.437101\t2019-11-04 08:05:30.353745\t35\t11\t\\N\t2019-11-06\t\\N\t\t\\N\t\\N\t\\N\t\\N\t\\N\t2019-11-06 06:00:00+00\t\t\\N\t\\N\n57\t2019-11-13 10:00:57.263816\t2019-11-13 10:00:57.263816\t38\t11\t\\N\t2019-11-15\t17:00:13+01\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2019-11-15 16:00:13+00\t\t\\N\t\\N\n45\t2019-10-15 13:22:46.522679\t2019-11-19 10:24:53.907876\t45\t11\t\\N\t2019-12-10\t18:00:23+01\tYoutube Live\t\\N\tAndre Gloukhmantchouk\t\\N\t\\N\t\\N\t2019-12-10 17:00:23+00\t\t\\N\t\\N\n37\t2019-10-15 11:57:45.893502\t2019-11-13 10:16:05.257876\t39\t11\t\\N\t2019-11-19\t20:00:59+01\tYoutube Live\t\\N\t\\N\t1328\t\\N\t\\N\t2019-11-19 19:00:59+00\t\t\\N\t\\N\n58\t2019-11-13 10:41:26.703281\t2019-11-13 10:41:26.703281\t40\t11\t\\N\t2019-11-19\t17:00:35+01\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2019-11-19 16:00:35+00\t\t\\N\t\\N\n59\t2019-11-13 10:45:10.752653\t2019-11-13 10:45:10.752653\t53\t11\t\\N\t2019-11-20\t17:00:59+01\tImaguru\t\\N\t\\N\t\\N\t\\N\thttps://www.youtube.com/user/ImaguruHub/videos\t2019-11-20 16:00:59+00\t\t\\N\t\\N\n61\t2019-11-13 15:03:10.873277\t2019-11-13 15:03:10.873277\t55\t11\t\\N\t2019-11-21\t19:00:58+01\tDiscord >> announcement\t\\N\tOptional test without score and deadline\t1328\t\\N\t\\N\t2019-11-21 18:00:58+00\t\t\\N\t\\N\n51\t2019-10-15 13:46:51.156727\t2019-11-14 08:04:43.997755\t46\t11\t\\N\t2019-12-20\t17:00:03+01\tImaguru + Youtube Live\t\\N\thttps://community-z.com/events/rss2019q3-presentations-5\t\\N\t\\N\thttps://www.youtube.com/user/ImaguruHub/videos\t2019-12-20 16:00:03+00\t\t\\N\t\\N\n50\t2019-10-15 13:46:25.188954\t2019-11-14 08:05:21.714914\t46\t11\t\\N\t2019-12-19\t17:00:03+01\tImaguru + Youtube Live\t\\N\thttps://community-z.com/events/rss2019q3-presentations-4\t\\N\t\\N\thttps://www.youtube.com/user/ImaguruHub/videos\t2019-12-19 16:00:03+00\t\t\\N\t\\N\n49\t2019-10-15 13:45:26.160284\t2019-11-14 08:05:57.063452\t46\t11\t\\N\t2019-12-17\t17:00:03+01\tImaguru + Youtube Live\t\\N\thttps://community-z.com/events/rss2019q3-presentations-3\t\\N\t\\N\thttps://www.youtube.com/user/ImaguruHub/videos\t2019-12-17 16:00:03+00\t\t\\N\t\\N\n46\t2019-10-15 13:38:17.289871\t2019-11-14 08:06:34.523225\t46\t11\t\\N\t2019-12-12\t17:00:08+01\t\t\\N\thttps://community-z.com/events/rss2019q3-presentations-2\t\\N\t\\N\thttps://www.youtube.com/user/ImaguruHub/videos\t2019-12-12 16:00:08+00\t\t\\N\t\\N\n62\t2019-11-14 08:08:21.712392\t2019-11-14 08:08:40.889422\t46\t11\t\\N\t2019-12-11\t17:00:18+01\tImaguru + Youtube Live\t\\N\thttps://community-z.com/events/rss2019q3-presentations-1\t\\N\t\\N\thttps://www.youtube.com/user/ImaguruHub/videos\t2019-12-11 16:00:18+00\t\t\\N\t\\N\n47\t2019-10-15 13:40:23.348495\t2019-11-19 10:25:27.58625\t47\t11\t\\N\t2019-12-13\t18:00:40+01\tYoutube Live\t\\N\tAndre Gloukhmantchouk\t\\N\t\\N\t\\N\t2019-12-13 17:00:40+00\t\t\\N\t\\N\n60\t2019-11-13 14:32:00.780799\t2019-11-19 08:46:13.282679\t54\t11\t\\N\t2019-11-21\t06:00:43+01\tDiscord >> announcement\t\\N\tOptional test without score and deadline\t1328\t\\N\t\\N\t2019-11-21 05:00:43+00\t\t\\N\t\\N\n63\t2019-11-19 13:03:55.859842\t2019-11-19 13:03:55.859842\t56\t11\t\\N\t2019-12-23\t18:00:20+01\tYoutube Live\t\\N\t\\N\t1328\t\\N\thttps://www.youtube.com/c/RollingScopesSchool\t2019-12-23 17:00:20+00\t\t\\N\t\\N\n35\t2019-10-15 11:52:24.439929\t2019-11-20 10:30:47.532359\t37\t11\t\\N\t2019-11-13\t17:00:37+01\tYoutube Live\t\\N\tPart 1\t4476\t\\N\t\\N\t2019-11-13 16:00:37+00\t\t\\N\t\\N\n4\t2019-09-18 13:32:30.103621\t2019-09-29 22:36:22.6367\t4\t11\t\\N\t2019-09-16\t20:00:00+03\tYoutube Live\tSergey Shalyapin\t\\N\t3961\t\\N\t\\N\t2019-09-16 17:00:00+00\t\t\\N\t\\N\n64\t2019-11-20 10:31:56.663441\t2019-11-20 10:31:56.663441\t37\t11\t\\N\t2019-11-26\t17:00:32+01\tYoutube Live\t\\N\tPart 3\t4476\t\\N\t\\N\t2019-11-26 16:00:32+00\t\t\\N\t\\N\n65\t2019-11-20 10:46:52.962706\t2019-11-20 10:46:52.962706\t57\t11\t\\N\t2019-12-16\t17:00:37+01\tYoutube Live\t\\N\t\\N\t1328\t\\N\t\\N\t2019-12-16 16:00:37+00\t\t\\N\t\\N\n66\t2019-11-20 11:06:19.515961\t2019-11-20 11:06:19.515961\t59\t11\t\\N\t2020-01-31\t07:00:31+01\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2020-01-31 06:00:31+00\t\t\\N\t\\N\n365\t2021-05-24 07:20:56.788715\t2021-05-24 07:20:56.788715\t184\t23\t\\N\t\\N\t\\N\tYouTube\t\\N\t\\N\t2084\t\\N\t\\N\t2021-05-27 14:00:52.55+00\t\t2\t\\N\n366\t2021-06-22 11:42:36.951384\t2021-06-22 11:42:36.951384\t185\t23\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2021-06-24 14:00:00+00\t\t\\N\t\\N\n367\t2021-06-22 14:07:40.909358\t2021-06-22 14:07:40.909358\t186\t23\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2021-07-25 21:59:15.201+00\t\t\\N\t\\N\n372\t2021-06-25 11:17:49.097994\t2021-07-02 14:10:23.571015\t189\t23\t\\N\t\\N\t\\N\tYouTube\t\\N\t\\N\t2084\t\\N\t\\N\t2021-07-06 15:30:00+00\t\t\\N\t\\N\n375\t2021-06-30 12:43:57.602426\t2021-07-01 07:32:06.927318\t192\t23\t\\N\t\\N\t\\N\tyoutube\t\\N\t\\N\t2084\t\\N\t\\N\t2021-07-01 16:30:00+00\t\t\\N\t\\N\n398\t2021-07-05 20:58:39.710814\t2021-07-07 15:21:56.684306\t201\t23\t\\N\t\\N\t\\N\tyoutube\t\\N\t\\N\t2084\t\\N\t\\N\t2021-07-08 15:00:00+00\t\t\\N\t\\N\n399\t2021-07-06 09:39:48.224795\t2021-07-08 06:13:01.681283\t202\t23\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2084\t\\N\t\\N\t2021-07-12 23:59:04.648+00\t\t\\N\t\\N\n409\t2021-07-16 11:12:11.214905\t2021-07-22 05:33:46.72208\t212\t23\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2084\t\\N\t\\N\t2021-07-24 23:59:00+00\t\t\\N\t\\N\n410\t2021-07-20 13:47:53.823319\t2021-07-20 13:49:26.410219\t213\t23\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t2021-07-24 23:59:00+00\t\t\\N\t\\N\n\\.\n\n\n--\n-- Data for Name: course_manager; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.course_manager (id, \"createdDate\", \"updatedDate\", \"courseId\", \"userId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: course_task; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.course_task (id, \"createdDate\", \"updatedDate\", \"mentorStartDate\", \"mentorEndDate\", \"maxScore\", \"taskId\", \"scoreWeight\", checker, \"taskOwnerId\", \"studentStartDate\", \"studentEndDate\", \"courseId\", \"pairsCount\", type, disabled, \"crossCheckEndDate\", \"submitText\", validations, \"crossCheckStatus\", \"teamDistributionId\", \"studentRegistrationStartDate\") FROM stdin;\n387\t2020-02-24 06:42:44.772736\t2020-02-25 10:28:14.611904\t\\N\t\\N\t54\t434\t0.1\ttaskOwner\t587\t2020-02-22 15:00:00+00\t2020-02-23 15:00:00+00\t13\t\\N\ttest\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n426\t2020-03-31 11:04:53.472383\t2020-03-31 11:04:53.472383\t\\N\t\\N\t100\t129\t0.01\tauto-test\t\\N\t2020-03-30 20:59:00+00\t2020-04-25 20:59:00+00\t13\t\\N\tcodewars:stage2\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n399\t2020-03-02 13:25:46.327431\t2020-03-17 08:04:28.635812\t\\N\t\\N\t100\t421\t0.2\tmentor\t2103\t2020-03-02 13:25:00+00\t2020-03-22 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n383\t2020-02-19 15:19:31.540441\t2020-03-22 19:02:59.763044\t\\N\t\\N\t100\t472\t0.2\tmentor\t2103\t2020-02-19 15:19:00+00\t2020-03-23 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n321\t2019-10-15 12:42:42.1037\t2019-10-15 12:43:35.36623\t\\N\t\\N\t100\t435\t0.5\ttaskOwner\t3961\t2019-10-06 00:00:00+00\t2019-10-08 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n337\t2019-11-13 08:21:59.44239\t2019-11-19 08:47:29.701909\t\\N\t\\N\t100\t446\t1\tmentor\t1328\t2019-11-14 17:00:00+00\t2019-11-18 20:49:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n348\t2019-11-19 10:52:33.333176\t2019-11-19 10:52:33.333176\t\\N\t\\N\t100\t350\t1\tmentor\t1328\t2019-12-23 17:00:00+00\t2020-01-02 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n350\t2019-11-20 10:40:56.936083\t2020-01-20 20:56:08.618894\t\\N\t\\N\t280\t448\t0.7\tmentor\t1328\t2019-11-03 08:00:00+00\t2019-12-18 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n346\t2019-11-19 09:32:03.882014\t2020-01-20 21:16:18.023264\t\\N\t\\N\t100\t349\t5\tassigned\t\\N\t2020-01-08 15:00:00+00\t2020-01-20 15:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n342\t2019-11-18 07:49:09.892108\t2020-01-29 10:07:18.716975\t\\N\t\\N\t100\t447\t1\tmentor\t\\N\t2020-01-28 10:07:00+00\t2020-02-20 10:07:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n302\t2019-09-19 10:04:08.320328\t2019-11-20 21:51:46.684981\t\\N\t\\N\t100\t423\t0.02\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n306\t2019-09-20 09:59:01.071936\t2019-11-20 21:52:10.896805\t\\N\t\\N\t100\t428\t0.01\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n309\t2019-09-22 09:57:59.933548\t2019-11-20 21:52:27.065892\t\\N\t\\N\t100\t429\t0.04\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n315\t2019-09-30 08:20:14.840054\t2019-11-20 21:54:03.067127\t\\N\t\\N\t100\t434\t0.01\ttaskOwner\t2032\t2019-09-28 00:00:00+00\t2019-09-28 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n318\t2019-10-06 11:21:27.376684\t2019-11-20 21:54:20.53693\t\\N\t\\N\t100\t437\t0.01\tmentor\t\\N\t2019-09-16 00:00:00+00\t2019-09-22 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n388\t2020-02-24 06:43:57.26983\t2020-02-25 10:28:23.927547\t\\N\t\\N\t50\t432\t0.1\ttaskOwner\t2480\t2020-02-22 15:00:00+00\t2020-02-23 15:00:00+00\t13\t\\N\ttest\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n374\t2020-02-15 14:44:37.656023\t2020-03-12 07:20:40.425622\t\\N\t\\N\t100\t467\t0.2\tmentor\t5481\t2020-02-15 14:00:00+00\t2020-03-22 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n380\t2020-02-19 15:16:59.219399\t2020-03-22 19:08:34.853331\t\\N\t\\N\t100\t475\t0.2\tmentor\t2103\t2020-02-19 15:15:00+00\t2020-03-23 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n408\t2020-03-15 23:12:19.237073\t2020-03-30 07:23:21.073835\t\\N\t\\N\t100\t484\t1\ttaskOwner\t2084\t2020-03-22 21:00:00+00\t2020-04-11 20:59:00+00\t13\t\\N\tstage-interview\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n430\t2020-04-04 18:29:20.218081\t2020-04-04 19:44:07.634629\t\\N\t\\N\t100\t435\t0.1\tauto-test\t3961\t2020-04-02 19:00:00+00\t2020-04-05 20:59:00+00\t13\t\\N\ttest\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n303\t2019-09-19 10:04:35.673232\t2019-11-20 21:51:53.750426\t\\N\t\\N\t100\t422\t0.03\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n343\t2019-11-19 08:57:16.511397\t2019-11-26 06:57:02.144395\t\\N\t\\N\t100\t246\t1\ttaskOwner\t2612\t2019-11-23 09:00:00+00\t2019-11-23 13:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n401\t2020-03-09 08:21:51.143582\t2020-03-10 08:46:07.22067\t\\N\t\\N\t100\t433\t0.1\ttaskOwner\t3961\t2020-03-08 19:00:00+00\t2020-03-08 19:00:00+00\t13\t\\N\ttest\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n417\t2020-03-21 19:19:58.863021\t2020-03-21 19:19:58.863021\t\\N\t\\N\t100\t484\t1\tmentor\t\\N\t2019-09-30 21:00:00+00\t2019-11-30 21:00:00+00\t11\t\\N\tstage-interview\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n381\t2020-02-19 15:17:32.07091\t2020-03-22 19:09:12.677292\t\\N\t\\N\t100\t474\t0.2\tmentor\t2103\t2020-02-19 15:17:00+00\t2020-03-23 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n397\t2020-03-02 13:24:09.075432\t2020-03-22 19:12:20.05552\t\\N\t\\N\t100\t426\t0.2\tmentor\t2103\t2020-03-20 13:20:00+00\t2020-03-22 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n423\t2020-03-31 10:19:16.141261\t2020-04-06 07:07:06.10971\t\\N\t\\N\t110\t444\t0.7\tmentor\t1090\t2020-03-23 21:00:00+00\t2020-04-07 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n300\t2019-09-17 08:15:35.715649\t2020-04-06 10:49:35.519015\t\\N\t\\N\t100\t417\t0.01\tmentor\t\\N\t2019-09-09 00:00:00+00\t2019-09-19 00:00:00+00\t11\t\\N\thtmlcssacademy\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n344\t2019-11-19 09:04:18.469854\t2019-11-28 17:17:02.674641\t\\N\t\\N\t128\t129\t1\tmentor\t\\N\t2019-09-09 08:00:00+00\t2019-11-24 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n327\t2019-10-28 07:42:02.903354\t2019-11-15 12:34:30.259197\t\\N\t\\N\t100\t418\t1\tmentor\t\\N\t2019-09-20 17:00:00+00\t2019-09-29 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n331\t2019-11-04 08:15:10.985127\t2019-11-15 12:37:57.067586\t\\N\t\\N\t110\t444\t1\tmentor\t\\N\t2019-11-01 16:00:00+00\t2019-11-06 20:39:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n354\t2019-12-07 14:35:20.567268\t2019-12-11 16:33:41.983256\t\\N\t\\N\t60\t96\t1\tjury\t2084\t2019-12-07 12:31:00+00\t2019-12-28 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n313\t2019-09-30 08:17:27.15297\t2019-11-20 21:53:55.352852\t\\N\t\\N\t100\t432\t0.01\ttaskOwner\t2480\t2019-09-22 00:00:00+00\t2019-09-22 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n316\t2019-09-30 08:22:03.026072\t2019-11-20 21:54:11.847779\t\\N\t\\N\t100\t433\t0.05\ttaskOwner\t2032\t2019-09-26 00:00:00+00\t2019-09-26 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n319\t2019-10-13 13:51:52.830672\t2019-11-20 21:55:14.344517\t\\N\t\\N\t100\t439\t0.3\tmentor\t1328\t2019-10-13 00:00:00+00\t2019-10-20 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n310\t2019-09-22 09:58:21.070871\t2019-11-20 21:52:32.957984\t\\N\t\\N\t100\t430\t0.04\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n325\t2019-10-27 12:09:53.130143\t2019-11-15 12:31:01.943109\t\\N\t\\N\t50\t442\t1\tmentor\t\\N\t2019-10-24 17:00:00+00\t2019-10-27 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n307\t2019-09-20 09:59:22.00868\t2019-11-20 21:52:16.13903\t\\N\t\\N\t100\t427\t0.04\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n369\t2020-02-02 03:55:35.429745\t2020-03-12 07:11:39.495304\t\\N\t\\N\t100\t437\t0.1\tmentor\t\\N\t2020-02-02 01:54:00+00\t2020-02-16 20:59:00+00\t13\t\\N\tcv:markdown\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n373\t2020-02-09 18:18:59.381025\t2020-03-12 07:13:13.223671\t\\N\t\\N\t60\t465\t0.2\tmentor\t\\N\t2020-02-01 21:00:00+00\t2020-03-15 20:59:00+00\t13\t\\N\tcodewars:stage1\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n368\t2020-02-01 20:13:13.966515\t2020-03-12 07:10:32.0252\t\\N\t\\N\t100\t417\t0.1\tmentor\t2032\t2020-02-02 09:00:00+00\t2020-02-23 20:59:00+00\t13\t\\N\thtmlcssacademy\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n336\t2019-11-13 07:47:34.232721\t2019-11-15 12:40:11.757945\t\\N\t\\N\t120\t445\t1\tmentor\t1328\t2019-11-08 05:00:00+00\t2019-11-11 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n328\t2019-10-28 07:48:01.625307\t2019-11-15 12:42:26.150687\t\\N\t\\N\t100\t443\t1\tmentor\t\\N\t2019-10-01 17:00:00+00\t2019-12-01 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n345\t2019-11-19 09:23:27.67568\t2019-12-23 21:01:53.560053\t\\N\t\\N\t100\t83\t1\tmentor\t2032\t2019-11-30 17:00:00+00\t2019-12-24 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n320\t2019-10-13 13:52:22.151208\t2019-11-16 13:10:56.094496\t\\N\t\\N\t100\t438\t0.3\tmentor\t1328\t2019-10-13 00:00:00+00\t2019-10-20 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n349\t2019-11-19 11:04:25.743014\t2020-01-14 08:52:31.860422\t\\N\t\\N\t450\t352\t1\tassigned\t1328\t2019-12-18 19:00:00+00\t2020-01-08 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n347\t2019-11-19 10:18:28.401575\t2019-11-19 10:18:28.401575\t\\N\t\\N\t100\t351\t1\ttaskOwner\t2612\t2019-12-07 09:00:00+00\t2019-12-07 13:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n332\t2019-11-05 11:51:40.950343\t2019-11-19 10:21:01.444201\t\\N\t\\N\t120\t89\t1\tmentor\t\\N\t2019-11-03 21:00:00+00\t2019-12-08 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n351\t2019-11-20 11:37:02.922582\t2019-11-20 11:37:02.922582\t\\N\t\\N\t100\t407\t1\tmentor\t\\N\t2020-01-01 08:00:00+00\t2020-01-17 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n367\t2020-01-19 16:51:46.691809\t2020-01-19 16:51:46.691809\t\\N\t\\N\t100\t88\t1\ttaskOwner\t1328\t2020-01-18 21:00:00+00\t2020-01-19 21:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n301\t2019-09-17 13:42:41.220995\t2019-11-20 21:51:18.507183\t\\N\t\\N\t100\t421\t0.02\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n304\t2019-09-20 09:45:08.623688\t2019-11-20 21:51:58.821689\t\\N\t\\N\t100\t424\t0.05\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n305\t2019-09-20 09:45:31.423306\t2019-11-20 21:52:03.967525\t\\N\t\\N\t100\t425\t0.03\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n308\t2019-09-20 09:59:54.237603\t2019-11-20 21:52:21.418289\t\\N\t\\N\t100\t426\t0.02\tmentor\t\\N\t2019-09-23 00:00:00+00\t2019-10-19 00:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n382\t2020-02-19 15:18:06.945157\t2020-03-22 19:03:14.201634\t\\N\t\\N\t100\t473\t0.2\tmentor\t2103\t2020-02-19 15:17:00+00\t2020-03-23 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n370\t2020-02-02 04:03:10.255065\t2020-03-12 07:11:48.755187\t\\N\t\\N\t100\t84\t0.1\tautoTest\t\\N\t2020-02-02 02:02:00+00\t2020-02-18 20:59:00+00\t13\t\\N\tcv:html\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n398\t2020-03-02 13:24:43.551181\t2020-03-17 08:05:11.649945\t\\N\t\\N\t100\t424\t0.5\tmentor\t2103\t2020-03-02 13:24:00+00\t2020-03-22 20:59:00+00\t13\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n718\t2020-02-24 06:43:57.27\t2020-12-19 07:08:38.178221\t\\N\t\\N\t50\t432\t0.2\ttaskOwner\t2084\t2021-03-19 15:00:00+00\t2021-03-20 15:00:00+00\t23\t\\N\ttest\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n719\t2020-03-15 23:12:19.237\t2021-05-17 17:21:40.075257\t\\N\t\\N\t50\t484\t1\ttaskOwner\t2084\t2021-05-04 00:00:00+00\t2021-05-18 23:59:00+00\t23\t\\N\tstage-interview\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n720\t2020-02-19 15:16:59.219\t2021-03-06 09:11:06.762852\t\\N\t\\N\t100\t475\t0.05\tauto-test\t2084\t2021-02-28 21:59:00+00\t2021-03-15 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n721\t2020-03-02 13:25:46.327\t2021-03-06 09:11:43.622874\t\\N\t\\N\t100\t421\t0.05\tmentor\t2084\t2021-02-28 21:59:00+00\t2021-03-15 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n722\t2020-02-19 15:19:31.54\t2021-03-06 09:12:16.168284\t\\N\t\\N\t100\t472\t0.05\tmentor\t2084\t2021-02-28 21:59:00+00\t2021-03-15 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n723\t2020-03-02 13:24:09.075\t2020-12-27 07:57:56.442267\t\\N\t\\N\t100\t426\t0.1\tmentor\t2084\t2021-04-06 13:20:00+00\t2021-04-18 21:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n724\t2020-02-19 15:17:32.071\t2021-03-06 09:28:28.111453\t\\N\t\\N\t100\t474\t0.05\tmentor\t2084\t2021-02-28 23:59:00+00\t2021-03-15 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n726\t2020-02-02 04:03:10.255\t2021-03-06 09:05:38.409628\t\\N\t\\N\t100\t84\t0.1\tautoTest\t\\N\t2021-02-27 03:02:00+00\t2021-03-08 23:59:00+00\t23\t\\N\tcv:html\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n727\t2020-03-09 08:21:51.144\t2020-12-19 07:08:30.734975\t\\N\t\\N\t100\t433\t0.2\ttaskOwner\t2084\t2021-04-03 19:00:00+00\t2021-04-03 19:00:00+00\t23\t\\N\ttest\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n728\t2020-09-09 16:31:08.778\t2020-12-27 07:05:14.675656\t\\N\t\\N\t100\t568\t0.1\tauto-test\t2084\t2021-03-02 20:59:00+00\t2021-04-25 20:59:00+00\t23\t\\N\tselfeducation\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n729\t2020-09-09 16:32:20.373\t2020-12-27 07:05:25.568066\t\\N\t\\N\t100\t567\t0.1\tauto-test\t2084\t2021-03-02 20:00:00+00\t2021-04-25 20:59:00+00\t23\t\\N\tselfeducation\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n730\t2020-09-09 16:33:07.413\t2020-12-27 07:05:31.951659\t\\N\t\\N\t100\t569\t0.1\tauto-test\t2084\t2021-03-02 20:59:00+00\t2021-04-25 20:59:00+00\t23\t\\N\tselfeducation\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n731\t2020-02-15 14:44:37.656\t2021-03-30 05:41:33.668199\t\\N\t\\N\t100\t467\t0.5\tmentor\t2084\t2021-01-20 16:00:00+00\t2021-03-30 22:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n732\t2020-02-09 18:18:59.381\t2020-12-19 07:08:19.25978\t\\N\t\\N\t60\t465\t1\tmentor\t\\N\t2021-02-26 19:00:00+00\t2021-04-04 20:59:00+00\t23\t\\N\tcodewars:stage1\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n733\t2020-02-02 03:55:35.43\t2021-03-06 09:04:46.985011\t\\N\t\\N\t100\t437\t0.1\tauto-test\t\\N\t2021-02-27 02:54:00+00\t2021-03-08 23:59:00+00\t23\t\\N\tcv:markdown\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n734\t2020-02-24 06:42:44.773\t2020-12-19 07:06:44.394231\t\\N\t\\N\t100\t434\t0.2\ttaskOwner\t2084\t2021-03-13 15:00:00+00\t2021-03-14 15:00:00+00\t23\t\\N\ttest\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n736\t2020-09-28 15:59:54.118\t2020-12-14 11:25:09.139521\t\\N\t\\N\t210\t577\t0.2\tmentor\t\\N\t2021-04-26 01:59:00+00\t2021-05-10 23:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n740\t2020-11-20 07:16:10.732\t2020-12-14 11:24:43.632522\t\\N\t\\N\t210\t500\t0.2\tmentor\t\\N\t2021-05-10 06:15:00+00\t2021-05-31 23:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n743\t2020-11-03 15:25:45.804\t2020-12-19 07:07:17.139765\t\\N\t\\N\t128\t129\t0.2\tauto-test\t\\N\t2021-04-27 23:59:00+00\t2021-05-17 23:59:00+00\t23\t\\N\tcodewars\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n745\t2020-11-20 07:22:07.672\t2020-12-14 11:24:35.798531\t\\N\t\\N\t210\t584\t0.2\tmentor\t\\N\t2021-05-10 06:21:00+00\t2021-05-31 23:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n747\t2020-11-30 08:13:18.401\t2020-12-14 11:24:26.882041\t\\N\t\\N\t100\t83\t0.5\tauto-test\t2084\t2021-05-24 08:12:00+00\t2021-06-07 22:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n748\t2020-11-29 19:28:52.429\t2020-12-14 11:24:54.693418\t\\N\t\\N\t100\t229\t0.1\ttaskOwner\t2084\t2021-05-22 15:00:00+00\t2021-05-23 15:00:00+00\t23\t\\N\ttest\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n749\t2020-12-01 14:39:15.604\t2020-12-14 11:24:51.175695\t\\N\t\\N\t120\t89\t1\tmentor\t\\N\t2021-04-23 23:59:00+00\t2021-05-25 23:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n750\t2020-12-04 09:25:44.758\t2020-12-14 11:24:47.561727\t\\N\t\\N\t76\t531\t0.149999999999999\ttaskOwner\t2084\t2021-05-28 18:00:00+00\t2021-05-30 18:00:00+00\t23\t\\N\ttest\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n751\t2020-12-09 12:07:23.808\t2020-12-14 11:24:13.461012\t\\N\t\\N\t10\t349\t10\tmentor\t2084\t2021-06-07 00:00:00+00\t2021-06-21 23:59:00+00\t23\t\\N\tinterview\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n752\t2020-12-11 12:22:20.579\t2020-12-14 11:24:23.203131\t\\N\t\\N\t280\t589\t0.2\tmentor\t\\N\t2021-06-01 12:21:00+00\t2021-06-17 00:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n764\t2020-12-19 13:05:37.532114\t2021-03-16 04:44:14.87901\t\\N\t\\N\t15\t592\t1\tauto-test\t2084\t2021-02-28 13:04:00+00\t2021-03-09 00:59:00+00\t23\t\\N\tcodewars\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n767\t2020-12-26 18:38:48.344647\t2021-03-06 09:02:35.081732\t\\N\t\\N\t100\t596\t0.1\tauto-test\t2084\t2020-12-25 21:59:00+00\t2021-03-08 23:59:00+00\t23\t\\N\tselfeducation\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n768\t2020-12-26 18:39:33.065223\t2021-03-06 09:03:26.018831\t\\N\t\\N\t100\t597\t0.1\tauto-test\t2084\t2020-12-25 21:59:00+00\t2021-03-08 23:59:00+00\t23\t\\N\tselfeducation\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n833\t2021-03-02 11:27:43.699601\t2021-03-08 09:16:47.157628\t\\N\t\\N\t100\t615\t0\tmentor\t2084\t2021-03-01 22:59:00+00\t2021-03-14 22:59:00+00\t23\t\\N\ttest\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n845\t2021-03-16 04:27:15.526178\t2021-03-30 05:41:07.113384\t\\N\t\\N\t65\t465\t0.5\tauto-test\t2084\t2021-03-16 05:22:00+00\t2021-03-30 22:59:00+00\t23\t\\N\tcodewars\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n864\t2021-04-02 16:07:20.055716\t2021-04-02 16:07:20.055716\t\\N\t\\N\t100\t432\t0.1\ttaskOwner\t2084\t2021-04-02 16:00:00+00\t2021-04-04 16:00:00+00\t23\t\\N\ttest\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n866\t2021-04-05 15:41:10.480048\t2021-04-07 11:36:23.008047\t\\N\t\\N\t100\t639\t0.149999999999999\tauto-test\t\\N\t2021-04-06 00:00:00+00\t2021-04-12 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n871\t2021-04-11 09:24:28.636388\t2021-04-11 09:24:28.636388\t\\N\t\\N\t100\t433\t0.1\tauto-test\t2084\t2021-04-10 09:10:00+00\t2021-04-11 09:10:00+00\t23\t\\N\ttest\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n888\t2021-04-19 15:25:43.189874\t2021-04-29 07:10:23.047691\t\\N\t\\N\t50\t484\t1\ttaskOwner\t2084\t2021-05-04 23:59:00+00\t2021-05-18 23:59:00+00\t23\t\\N\tstage-interview\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n891\t2021-04-20 07:59:31.355572\t2021-04-20 08:00:02.758902\t\\N\t\\N\t100\t641\t0.1\tauto-test\t2084\t2021-04-20 08:00:00+00\t2021-04-26 23:59:00+00\t23\t\\N\tselfeducation\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n913\t2021-05-04 13:42:38.3986\t2021-05-07 11:39:02.702583\t\\N\t\\N\t128\t129\t0.5\tauto-test\t2084\t2021-05-04 15:42:00+00\t2021-05-31 23:59:00+00\t23\t\\N\tcodewars\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n916\t2021-05-07 11:44:06.354446\t2021-05-07 11:44:06.354446\t\\N\t\\N\t81\t671\t0.5\tauto-test\t2084\t2021-05-07 14:00:00+00\t2021-05-31 23:59:00+00\t23\t\\N\tcodewars\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n924\t2021-05-20 12:03:27.067163\t2021-05-20 12:03:27.067163\t\\N\t\\N\t200\t677\t1\tauto-test\t2084\t2021-03-23 20:00:00+00\t2021-04-23 23:59:00+00\t23\t\\N\thtmltask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n928\t2021-05-26 14:04:17.496156\t2021-05-26 14:05:56.930802\t\\N\t\\N\t160\t679\t1\tmentor\t2084\t2021-05-11 00:01:00+00\t2021-05-31 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n929\t2021-05-26 14:30:24.6811\t2021-06-13 13:50:58.160679\t\\N\t\\N\t150\t680\t1\tmentor\t2084\t2021-06-01 23:59:00+00\t2021-06-16 23:59:00+00\t23\t\\N\tJS task\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n945\t2021-06-01 07:37:00.990005\t2021-06-01 07:37:00.990005\t\\N\t\\N\t120\t89\t1\tmentor\t2084\t2021-05-21 10:36:00+00\t2021-06-21 23:59:00+00\t23\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n946\t2021-06-01 07:41:10.17798\t2021-06-01 07:41:10.17798\t\\N\t\\N\t50\t96\t1\tjury\t2084\t2021-06-22 10:39:00+00\t2021-06-28 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n959\t2021-06-05 07:43:45.453015\t2021-07-20 16:03:20.793296\t\\N\t\\N\t10\t349\t10\tmentor\t2084\t2021-06-17 23:59:00+00\t2021-07-26 23:59:00+00\t23\t\\N\tinterview\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n976\t2021-06-22 14:41:23.614155\t2021-06-29 13:01:39.053409\t\\N\t\\N\t360\t693\t1\tmentor\t2084\t2021-06-17 00:00:00+00\t2021-07-19 23:59:00+00\t23\t\\N\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n977\t2021-06-22 14:42:01.434232\t2021-06-27 14:55:32.785863\t\\N\t\\N\t360\t692\t1\tmentor\t2084\t2021-06-17 00:00:00+00\t2021-07-07 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n979\t2021-06-23 09:54:07.833539\t2021-07-26 21:01:38.322408\t\\N\t\\N\t715\t697\t1\ttaskOwner\t2084\t2021-06-30 00:00:00+00\t2021-07-19 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n981\t2021-06-23 09:57:51.078547\t2021-07-17 12:45:15.908329\t\\N\t\\N\t355\t696\t1\tmentor\t2084\t2021-07-08 00:00:00+00\t2021-07-15 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n431\t2022-03-27 11:50:14.908491\t2022-03-27 11:50:14.908491\t\\N\t\\N\t100\t498\t1\tmentor\t\\N\t2022-03-27 11:50:00+00\t2022-03-31 11:50:00+00\t23\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n410\t2020-03-16 12:51:21.596135\t2020-03-31 11:05:14.454307\t\\N\t\\N\t100\t485\t0.01\tcrossCheck\t3961\t2020-03-10 16:00:00+00\t2020-03-30 20:59:00+00\t13\t4\thtmltask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n353\t2019-12-03 16:51:35.631349\t2019-12-03 16:51:35.631349\t\\N\t\\N\t100\t450\t1\tcrossCheck\t\\N\t2019-09-30 21:00:00+00\t2019-12-01 20:59:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n424\t2020-03-31 10:21:55.660987\t2020-03-31 10:21:55.660987\t\\N\t\\N\t75\t493\t0.3\tcrossCheck\t1090\t2020-03-24 20:59:00+00\t2020-04-07 20:59:00+00\t13\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n356\t2019-12-16 09:41:27.698435\t2019-12-24 10:13:38.728977\t\\N\t\\N\t210\t452\t0.3\tcrossCheck\t606\t2019-12-03 07:39:00+00\t2019-12-22 21:00:00+00\t11\t\\N\t\\N\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n425\t2020-03-31 10:25:14.33142\t2020-03-31 10:25:14.33142\t\\N\t\\N\t100\t494\t0.1\tcrossCheck\t1090\t2020-03-26 20:59:00+00\t2020-04-07 20:59:00+00\t13\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n386\t2020-02-21 10:26:08.19839\t2020-09-24 18:52:15.030419\t\\N\t\\N\t100\t476\t1\tcrossCheck\t677\t2020-02-11 16:00:00+00\t2020-03-11 20:59:00+00\t13\t1\thtmltask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n735\t2020-09-28 15:55:30.264\t2020-12-14 11:25:14.054069\t\\N\t\\N\t60\t573\t1\tcrossCheck\t2084\t2021-04-19 23:59:00+00\t2021-04-26 23:59:00+00\t23\t4\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n737\t2020-09-28 16:02:42.88\t2020-12-14 11:25:04.586605\t\\N\t\\N\t170\t494\t0.8\tcrossCheck\t\\N\t2021-04-26 00:59:00+00\t2021-05-10 23:59:00+00\t23\t4\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n738\t2020-10-15 15:45:25.182\t2020-12-19 07:07:37.406537\t\\N\t\\N\t50\t572\t1\tcrossCheck\t\\N\t2021-04-12 14:00:00+00\t2021-04-21 20:59:00+00\t23\t4\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n739\t2020-09-19 08:04:36.752\t2020-12-19 07:07:06.937443\t\\N\t\\N\t100\t570\t0.5\tcrossCheck\t2084\t2021-03-08 19:59:00+00\t2021-03-17 20:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n741\t2020-09-28 15:44:17.135\t2020-12-19 07:07:46.233906\t\\N\t\\N\t100\t576\t1\tcrossCheck\t2084\t2021-03-22 20:59:00+00\t2021-04-19 18:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n742\t2020-09-21 11:23:02.753\t2020-12-19 07:08:07.900162\t\\N\t\\N\t40\t571\t1\tcrossCheck\t2084\t2021-03-15 10:22:00+00\t2021-04-08 21:59:00+00\t23\t4\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n746\t2020-11-20 07:27:41.543\t2020-12-14 11:24:32.174007\t\\N\t\\N\t170\t585\t0.8\tcrossCheck\t\\N\t2021-05-10 06:27:00+00\t2021-05-31 23:59:00+00\t23\t4\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n753\t2020-12-11 12:23:16.641\t2020-12-14 11:24:19.601754\t\\N\t\\N\t240\t590\t0.8\tcrossCheck\t\\N\t2021-06-01 12:22:00+00\t2021-06-17 00:59:00+00\t23\t4\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n763\t2020-12-19 13:03:24.071742\t2020-12-20 17:50:47.299196\t\\N\t\\N\t100\t593\t0.2\tcrossCheck\t\\N\t2021-02-28 12:00:00+00\t2021-03-14 20:59:00+00\t23\t4\tcv:html\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n765\t2020-12-19 13:07:08.321506\t2020-12-20 17:50:54.307731\t\\N\t\\N\t50\t594\t0.5\tcrossCheck\t\\N\t2021-02-28 12:06:00+00\t2021-03-14 20:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n841\t2021-03-07 16:23:23.776238\t2021-03-07 16:23:23.776238\t\\N\t\\N\t50\t594\t0.5\tcrossCheck\t2084\t2021-02-28 16:22:00+00\t2021-03-15 23:59:00+00\t23\t4\thtmltask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n853\t2021-03-29 09:21:34.603711\t2021-04-06 07:55:48.280397\t\\N\t\\N\t45\t630\t1\tcrossCheck\t2084\t2021-03-23 01:59:00+00\t2021-04-06 23:59:00+00\t23\t4\thtmltask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n854\t2021-03-29 20:04:59.336453\t2021-05-20 12:08:18.903322\t\\N\t\\N\t80\t631\t1\tcrossCheck\t2084\t2021-03-23 19:00:00+00\t2021-04-06 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n846\t2021-03-16 04:33:39.267072\t2023-01-31 20:08:42.176458\t\\N\t\\N\t100\t473\t0.05\tmentor\t2084\t2021-02-28 23:59:00+00\t2021-03-19 23:59:00+00\t23\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n766\t2020-12-26 18:38:03.97028\t2023-01-31 20:09:55.304166\t\\N\t\\N\t100\t595\t0.1\tauto-test\t2084\t2020-12-25 21:59:00+00\t2021-03-10 23:59:00+00\t23\t\\N\tselfeducation\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n855\t2021-03-29 20:05:01.298383\t2021-05-20 12:08:24.067047\t\\N\t\\N\t80\t632\t1\tcrossCheck\t2084\t2021-03-24 00:00:00+00\t2021-04-06 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n856\t2021-03-29 20:05:04.039062\t2021-05-20 12:08:28.287282\t\\N\t\\N\t80\t633\t1\tcrossCheck\t2084\t2021-03-23 19:00:00+00\t2021-04-06 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n857\t2021-03-29 20:05:05.992341\t2021-05-20 12:08:32.894051\t\\N\t\\N\t80\t634\t1\tcrossCheck\t2084\t2021-03-24 00:59:00+00\t2021-04-06 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n858\t2021-03-29 20:05:08.056901\t2021-05-20 12:08:38.903202\t\\N\t\\N\t80\t635\t1\tcrossCheck\t2084\t2021-03-23 19:00:00+00\t2021-04-06 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n859\t2021-03-29 20:05:09.938253\t2021-05-20 12:08:43.683559\t\\N\t\\N\t80\t636\t1\tcrossCheck\t2084\t2021-03-23 19:00:00+00\t2021-04-06 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n872\t2021-04-12 08:19:58.924614\t2021-04-19 18:00:57.980059\t\\N\t\\N\t100\t642\t1\tcrossCheck\t2084\t2021-04-09 15:00:00+00\t2021-04-19 23:59:00+00\t23\t4\thtmltask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n882\t2021-04-18 16:15:04.361321\t2021-05-20 12:08:50.387079\t\\N\t\\N\t40\t645\t1\tcrossCheck\t2084\t2021-04-18 19:15:00+00\t2021-04-20 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n883\t2021-04-18 16:15:41.93413\t2021-05-20 12:08:55.719784\t\\N\t\\N\t40\t646\t1\tcrossCheck\t2084\t2021-04-18 19:15:00+00\t2021-04-20 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n884\t2021-04-18 16:16:24.075893\t2021-05-20 12:09:00.206885\t\\N\t\\N\t40\t647\t1\tcrossCheck\t2084\t2021-04-18 19:15:00+00\t2021-04-20 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n885\t2021-04-18 16:17:18.716694\t2021-05-20 12:09:04.763147\t\\N\t\\N\t40\t648\t1\tcrossCheck\t2084\t2021-04-18 19:15:00+00\t2021-04-20 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n886\t2021-04-18 16:18:01.426812\t2021-05-20 12:09:08.62373\t\\N\t\\N\t40\t649\t1\tcrossCheck\t2084\t2021-04-18 19:15:00+00\t2021-04-20 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n887\t2021-04-18 16:18:36.220548\t2021-05-20 12:09:12.541483\t\\N\t\\N\t40\t650\t1\tcrossCheck\t2084\t2021-04-18 19:15:00+00\t2021-04-20 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n890\t2021-04-20 07:25:12.840047\t2021-04-20 07:25:12.840047\t\\N\t\\N\t60\t652\t1\tcrossCheck\t2084\t2021-04-20 10:23:00+00\t2021-04-26 23:59:00+00\t23\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n907\t2021-05-04 12:48:38.401297\t2021-05-20 12:09:16.982884\t\\N\t\\N\t80\t664\t1\tcrossCheck\t2084\t2021-04-20 20:20:00+00\t2021-05-09 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n908\t2021-05-04 12:48:40.48278\t2021-05-20 12:09:21.390411\t\\N\t\\N\t80\t665\t1\tcrossCheck\t2084\t2021-04-20 20:20:00+00\t2021-05-09 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n909\t2021-05-04 12:48:42.458949\t2021-05-20 12:09:25.327968\t\\N\t\\N\t80\t666\t1\tcrossCheck\t2084\t2021-04-20 20:20:00+00\t2021-05-09 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n910\t2021-05-04 12:48:44.805426\t2021-05-20 12:09:29.619037\t\\N\t\\N\t80\t667\t1\tcrossCheck\t2084\t2021-04-20 20:20:00+00\t2021-05-09 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n911\t2021-05-04 12:48:47.087144\t2021-05-20 12:09:35.059152\t\\N\t\\N\t80\t668\t1\tcrossCheck\t2084\t2021-04-20 20:20:00+00\t2021-05-09 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n912\t2021-05-04 12:48:50.871115\t2021-05-20 12:09:40.298413\t\\N\t\\N\t80\t669\t1\tcrossCheck\t2084\t2021-04-20 20:20:00+00\t2021-05-09 23:59:00+00\t23\t4\thtmltask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n927\t2021-05-24 07:28:24.474899\t2021-06-01 17:05:25.323429\t\\N\t\\N\t110\t396\t1\tcrossCheck\t2084\t2021-05-11 00:00:00+00\t2021-06-01 23:59:00+00\t23\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n967\t2021-06-11 08:06:12.698836\t2021-06-13 13:51:19.609064\t\\N\t\\N\t190\t688\t1\tcrossCheck\t2084\t2021-06-01 23:59:00+00\t2021-06-16 23:59:00+00\t23\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n972\t2021-06-20 16:42:18.069437\t2021-06-27 14:54:54.598599\t\\N\t\\N\t275\t690\t1\tcrossCheck\t2084\t2021-06-16 23:59:00+00\t2021-07-07 23:59:00+00\t23\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n973\t2021-06-20 16:44:52.184361\t2021-06-28 23:15:32.104315\t\\N\t\\N\t275\t691\t1\tcrossCheck\t2084\t2021-06-16 23:59:00+00\t2021-07-19 23:59:00+00\t23\t4\tjstask\tt\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n978\t2021-06-22 14:47:29.005674\t2021-07-28 09:44:54.70492\t\\N\t\\N\t480\t695\t1\tcrossCheck\t2084\t2021-06-30 00:00:00+00\t2021-07-19 23:59:00+00\t23\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n980\t2021-06-23 09:56:42.176771\t2021-07-09 06:19:59.834533\t\\N\t\\N\t205\t698\t1\tcrossCheck\t2084\t2021-07-08 00:00:00+00\t2021-07-15 23:59:00+00\t23\t4\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n725\t2020-02-19 15:18:06.945\t2023-01-31 20:06:50.347384\t\\N\t\\N\t100\t473\t0.05\tmentor\t2084\t2021-02-28 23:59:00+00\t2021-03-17 23:59:00+00\t23\t\\N\tjstask\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n821\t2021-02-28 09:07:38.664142\t2023-01-31 20:16:09.773572\t\\N\t\\N\t100\t593\t0.2\tcrossCheck\t2084\t2021-02-28 10:00:00+00\t2021-03-09 23:59:00+00\t23\t4\thtmltask\tf\t2023-01-18 23:59:00+00\t\\N\t{}\tinitial\t\\N\t\\N\n432\t2024-06-19 18:49:00.876624\t2024-06-19 18:49:00.876624\t\\N\t\\N\t10\t499\t1\tmentor\t11569\t2024-06-19 00:00:00+00\t2027-07-16 23:59:59+00\t23\t\\N\tinterview\tf\t\\N\t\\N\t\\N\tinitial\t\\N\t\\N\n\\.\n\n\n--\n-- Data for Name: course_user; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.course_user (id, \"createdDate\", \"updatedDate\", \"courseId\", \"userId\", \"isManager\", \"isJuryActivist\", \"isSupervisor\", \"isDementor\", \"isActivist\") FROM stdin;\n121\t2023-02-02 14:54:28.484043\t2023-02-02 14:54:28.484043\t23\t2098\tf\tf\tf\tt\tf\n122\t2023-02-02 14:54:28.484043\t2023-02-02 14:54:28.484043\t23\t2103\tf\tf\tf\tt\tf\n123\t2023-02-02 14:54:28.484043\t2023-02-02 14:54:28.484043\t23\t5481\tf\tf\tf\tt\tf\n125\t2023-02-03 07:46:43.309521\t2023-02-03 08:05:32.461954\t13\t2098\tf\tf\tf\tt\tf\n124\t2023-02-03 07:46:36.33884\t2023-02-03 08:36:06.162812\t13\t11569\tf\tf\tf\tt\tf\n\\.\n\n\n--\n-- Data for Name: cv; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.cv (id, \"githubId\", name, \"selfIntroLink\", \"startFrom\", \"fullTime\", expires, \"militaryService\", \"englishLevel\", \"avatarLink\", \"desiredPosition\", notes, phone, email, skype, telegram, linkedin, location, \"githubUsername\", website) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: discipline; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.discipline (id, created_date, updated_date, deleted_date, name) FROM stdin;\n1\t2023-02-02 14:54:47.349888\t2023-02-02 14:54:47.349888\t\\N\tJavaScript\n2\t2023-02-02 14:54:58.640241\t2023-02-02 14:54:58.640241\t\\N\tReact\n3\t2023-02-02 14:55:05.070751\t2023-02-02 14:55:05.070751\t\\N\tAngular\n4\t2023-02-02 14:55:14.334551\t2023-02-02 14:55:14.334551\t\\N\tNodeJs\n\\.\n\n\n--\n-- Data for Name: discord_server; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.discord_server (id, \"createdDate\", \"updatedDate\", name, \"gratitudeUrl\", \"mentorsChatUrl\") FROM stdin;\n2\t2021-07-28 20:43:54.177877\t2021-07-28 20:43:54.177877\tCoreJS\thttps://example.com\thttps://t.me\n\\.\n\n\n--\n-- Data for Name: event; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.event (id, \"createdDate\", \"updatedDate\", name, \"descriptionUrl\", description, type, \"disciplineId\") FROM stdin;\n1\t2019-09-12 09:03:02.219291\t2019-09-12 09:03:02.219291\tBrowsers and IDEs + FAQ\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/html-css-basics.md\t\\N\tlecture\t\\N\n5\t2019-09-12 09:05:27.226044\t2019-09-12 09:05:27.226044\tGit Basics\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/git.md\t\\N\tlecture\t\\N\n6\t2019-09-12 09:08:12.602314\t2019-09-12 09:08:12.602314\tPhotoshop and Figma for Web Developers\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/photoshop-basics.md\t\\N\tlecture\t\\N\n3\t2019-09-12 09:04:42.695298\t2019-09-12 09:09:17.59549\tRSSchool для гуманитария. Выпуск №1\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/rsschool_for_humanities.md\t\\N\tlecture\t\\N\n7\t2019-09-12 09:09:38.948815\t2019-09-12 09:09:38.948815\tRSSchool для гуманитария. Выпуск №2\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/rsschool_for_humanities.md\t\\N\tlecture\t\\N\n8\t2019-09-12 09:23:12.401849\t2019-09-12 09:23:12.401849\tРазбор теста и таска по Git. FAQ \thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/codejam-cv.md\t\\N\tlecture\t\\N\n9\t2019-09-19 07:59:24.096881\t2019-09-19 07:59:24.096881\tHTML&CSS. Responsive\t\\N\t\\N\tlecture\t\\N\n10\t2019-09-19 08:05:36.969405\t2019-09-19 08:05:36.969405\tВыдача алгоритмических заданий Stage#1\t\\N\t\\N\tlecture\t\\N\n11\t2019-09-19 08:14:45.141279\t2019-09-19 08:14:45.141279\tHTML&CSS. Best Practices.\t\\N\t\\N\tlecture\t\\N\n2\t2019-09-12 09:03:32.223067\t2019-09-19 08:18:20.690872\tHTML&CSS. Basics + FAQ\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/html-css-basics.md\t\\N\tlecture\t\\N\n12\t2019-09-19 08:23:21.884826\t2019-09-19 08:23:21.884826\tPreprocessors. Sass\t\\N\t\\N\tlecture\t\\N\n13\t2019-09-19 08:26:27.077797\t2019-09-19 08:26:27.077797\tРазбор теста по HTML&CSS\t\\N\t\\N\tlecture\t\\N\n14\t2019-09-19 08:31:20.979391\t2019-09-19 08:31:20.979391\tAdvanced HTML&CSS. BEM\t\\N\t\\N\tlecture\t\\N\n16\t2019-09-19 09:02:38.77527\t2019-09-19 09:02:38.77527\tJS Intro\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/js-intro.md\t\\N\tlecture\t\\N\n17\t2019-09-19 09:07:16.559406\t2019-09-19 09:07:16.559406\tJS Data Types\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/js-data-types.md\t\\N\tlecture\t\\N\n18\t2019-09-19 09:11:18.941673\t2019-09-19 09:11:18.941673\tJS Arrays\t\\N\t\\N\tlecture\t\\N\n20\t2019-09-19 09:12:16.974124\t2019-09-19 09:12:16.974124\tJavaScript DOM\t\\N\t\\N\tlecture\t\\N\n21\t2019-09-19 09:19:37.484274\t2019-09-19 09:19:37.484274\tРазбор алгоритмических заданий Stage#1\t\\N\t\\N\tlecture\t\\N\n22\t2019-09-19 09:26:59.759546\t2019-09-19 09:26:59.759546\tJS Events\t\\N\t\\N\tlecture\t\\N\n23\t2019-09-19 09:30:16.440205\t2019-09-19 09:30:16.440205\tРезультаты первого этапа. Ответы на вопросы. Планы на второй этап\t\\N\t\\N\tlecture\t\\N\n24\t2019-09-20 08:11:06.308753\t2019-09-20 08:11:06.308753\tCourse overview\thttps://docs.rs.school/#/\t\\N\tlecture\t\\N\n15\t2019-09-19 09:00:03.042423\t2019-09-20 08:25:06.472219\tAdvanced HTML&CSS. Animations\t\\N\t\\N\tlecture\t\\N\n19\t2019-09-19 09:11:47.471902\t2019-09-20 13:18:49.343646\tData Structures in JavaScript\t\\N\t\\N\tlecture\t\\N\n27\t2019-10-14 13:35:10.786219\t2019-10-14 13:38:25.576464\tNPM & Node.js Basics\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/rs-online-development.md\t\tlecture\t\\N\n28\t2019-10-14 13:48:15.522269\t2019-10-14 13:48:59.712154\tJS Scope\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/js-scope.md\t\\N\tlecture\t\\N\n30\t2019-10-14 13:59:15.547123\t2019-10-14 13:59:15.547123\tCodeJam Canvas. Q&A\t\\N\t\\N\tlecture\t\\N\n31\t2019-10-14 14:09:36.422681\t2019-10-14 14:09:36.422681\tChrome DevTools\thttps://developers.google.com/web/tools/chrome-devtools/javascript/\t\\N\tlecture\t\\N\n32\t2019-10-14 14:13:18.279154\t2019-10-14 14:13:18.279154\tJS Functions. Part 2\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/js-functions-part-two.md\t\\N\tlecture\t\\N\n29\t2019-10-14 13:48:50.052181\t2019-10-14 14:13:26.039738\tJS Functions. Part 1\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/js-functions.md\t\\N\tlecture\t\\N\n36\t2019-10-15 11:46:57.892947\t2019-10-15 11:46:57.892947\tInheritance in JavaScript. ES6 Classes.\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/inheritance-in-js-and-es6-classes.md\t\\N\tlecture\t\\N\n42\t2019-10-15 12:04:38.891898\t2019-10-15 12:04:38.891898\tWebpack. Assets management. Project Structure.\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/webpack.md\t\\N\tlecture\t\\N\n43\t2019-10-15 12:05:48.213974\t2019-10-15 12:05:48.213974\tYouTube Bootstrap\t\\N\t\\N\tlecture\t\\N\n41\t2019-10-15 12:03:06.56827\t2019-10-15 13:02:52.918044\tJS Test Retro\t\\N\t\\N\tlecture\t\\N\n39\t2019-10-15 11:56:46.945285\t2019-10-15 13:03:02.676299\t Code Jam \"DRAW API\" Retro. FAQ Promises & http\t\\N\t\\N\tlecture\t\\N\n38\t2019-10-15 11:53:52.332397\t2019-11-13 09:57:07.382732\tEvent Loop. Animation\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/event-loop-and-animations.md\t\\N\tlecture_self_study\t\\N\n44\t2019-10-15 13:18:35.530519\t2019-10-15 13:18:35.530519\tInterview Q&A / Stage#2 Lectures Test. Retro\t\\N\t\\N\tlecture\t\\N\n45\t2019-10-15 13:20:19.465082\t2019-10-15 13:39:15.779199\tTDD, Unit Tests, Quality control. Part 1\t\\N\t\\N\tlecture\t\\N\n47\t2019-10-15 13:39:23.817166\t2019-10-15 13:39:23.817166\tTDD, Unit Tests, Quality control. Part 2\t\\N\t\\N\tlecture\t\\N\n48\t2019-10-15 13:42:59.192228\t2019-10-15 13:42:59.192228\tTask \"YouTube\" Retro\t\\N\t\\N\tlecture\t\\N\n49\t2019-10-15 13:47:21.601106\t2019-10-15 13:47:21.601106\tPiskel bootstrap\t\\N\t\\N\tlecture\t\\N\n50\t2019-10-16 08:52:16.041267\t2019-10-16 08:52:16.041267\tCode refactoring in the context of 'Piskel clone' task\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/refactoring.md\t\\N\tlecture\t\\N\n51\t2019-10-16 09:34:30.248464\t2019-10-16 09:34:30.248464\tUnit tests\t\\N\t\\N\tlecture\t\\N\n35\t2019-10-15 11:41:04.360314\t2019-11-04 08:03:24.3041\tJS Callbacks & Promises & async/await\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/promises-game-dev.md\t\\N\tlecture_self_study\t\\N\n33\t2019-10-15 11:32:41.327741\t2019-11-04 08:25:48.618847\tCode Jam \"Virtual Keyboard\" Retro. DOM FAQ\t\\N\t\\N\tlecture\t\\N\n34\t2019-10-15 11:38:40.662901\t2019-11-04 08:40:42.114517\tES6 Variables features. ESLint. Airbnb JavaScript Style Guide\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/es6.md\t\\N\tlecture_online\t\\N\n40\t2019-10-15 11:59:11.696371\t2019-11-13 10:39:39.24538\tModules in JS\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/js-modules.md\t\\N\tlecture_self_study\t\\N\n46\t2019-10-15 13:36:34.293902\t2019-11-04 08:47:16.223906\tPresentation. Grand Final\t\\N\t\tlecture_mixed\t\\N\n53\t2019-11-13 10:43:47.792601\t2019-11-13 10:43:47.792601\tRS School Meetup\thttps://community-z.com/events/rss2019q3-meetup1\t\\N\tmeetup\t\\N\n52\t2019-10-17 08:38:23.683982\t2019-11-13 14:29:26.085033\tTest: DOM, DOM Events\t\\N\tThis test is without score and deadline.\twarmup\t\\N\n37\t2019-10-15 11:51:25.290004\t2019-11-20 10:17:12.401905\tNetwork communication\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/http.md\t\\N\tlecture\t\\N\n54\t2019-11-13 14:27:35.085726\t2019-11-13 14:32:36.477144\tTest: http, https2, ajax\t\t\twarmup\t\\N\n55\t2019-11-13 14:59:57.236829\t2019-11-13 14:59:57.236829\tTest: JS basics\t\\N\t\\N\twarmup\t\\N\n56\t2019-11-19 13:01:09.061065\t2019-11-19 13:01:09.061065\tCodeJam \"Animation Player\". Intro\t\\N\t\\N\tlecture_online\t\\N\n57\t2019-11-20 10:45:11.804413\t2019-11-20 10:48:32.730569\tTask \"Fancy Weather\". Retro\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/fancy-weather.md\t\\N\tlecture_online\t\\N\n59\t2019-11-20 11:05:11.612965\t2019-11-20 11:05:11.612965\tFeedback on Mentors\thttps://app.rs.school/gratitude?course=rs-2019-q3\t\\N\tinfo\t\\N\n63\t2020-02-17 08:36:47.634937\t2020-02-17 08:36:47.634937\tAngular course intro. TypeScript & Angular basics\t\\N\t\\N\tlecture_online\t\\N\n64\t2020-02-17 08:37:21.305157\t2020-02-17 08:37:21.305157\tAngular. Components\t\\N\t\\N\tlecture_online\t\\N\n65\t2020-02-17 08:37:43.212378\t2020-02-17 08:37:43.212378\tAngular. Directives & Pipes\t\\N\t\\N\tlecture_online\t\\N\n66\t2020-02-17 08:38:23.367356\t2020-02-17 08:38:23.367356\tAngular. Task #1 review\t\\N\t\\N\tlecture_online\t\\N\n4\t2019-09-12 09:04:58.808458\t2020-03-23 10:47:18.744158\tHTML&CSS Basics\t\\N\t\\N\tlecture\t\\N\n67\t2020-02-17 08:39:33.33246\t2020-02-17 08:39:33.33246\tAngular. Modules & Services\t\\N\t\\N\tlecture_online\t\\N\n68\t2020-02-17 08:39:41.807016\t2020-02-17 08:39:41.807016\tAngular. Routing\t\\N\t\\N\tlecture_online\t\\N\n69\t2020-02-17 08:39:56.469453\t2020-02-17 08:39:56.469453\tAngular. Task #2 review\t\\N\t\\N\tlecture_online\t\\N\n70\t2020-02-17 08:40:12.090089\t2020-02-17 08:40:12.090089\tAngular. RxJS & Observables\t\\N\t\\N\tlecture_online\t\\N\n72\t2020-02-17 08:40:24.125896\t2020-02-17 08:40:24.125896\tAngular. HTTP\t\\N\t\\N\tlecture_online\t\\N\n73\t2020-02-17 08:40:37.099773\t2020-02-17 08:40:37.099773\tAngular. Task #3 review\t\\N\t\\N\tlecture_online\t\\N\n74\t2020-02-17 08:40:51.31546\t2020-02-17 08:40:51.31546\tAngular. Redux & NgRx\t\\N\t\\N\tlecture_online\t\\N\n75\t2020-02-17 08:42:30.658576\t2020-02-17 08:42:30.658576\tAngular. CodeJam \"Culture Portal\". Intro\t\\N\t\\N\tlecture_online\t\\N\n76\t2020-02-17 08:42:41.662791\t2020-02-17 08:42:41.662791\tAngular. Unit testing\t\\N\t\\N\tlecture_online\t\\N\n78\t2020-02-17 12:36:52.791254\t2020-02-17 12:36:52.791254\tGit for Android developers\thttps://www.youtube.com/watch?v=J1tDWhbf-Gs\t\\N\tlecture_self_study\t\\N\n79\t2020-02-17 12:39:28.042076\t2020-02-26 11:02:46.29792\tJava for Android developers\thttps://www.youtube.com/watch?v=XsbCDeCA9p0\tJava (syntax, base data types, Object class and methods, GC)\tlecture_online\t\\N\n124\t2020-02-27 20:45:01.821927\t2020-02-27 20:45:01.821927\tAngular. Task #4 review\t\\N\t\\N\tlecture_online\t\\N\n80\t2020-02-17 12:40:54.525405\t2020-03-11 11:45:16.464869\tKotlin for Android developers\thttps://www.youtube.com/watch?v=mbA8EQZSjTk\tKotlin (syntax, base data types, differencies form Java)\tlecture_online\t\\N\n81\t2020-02-17 12:42:37.911916\t2020-03-11 11:50:46.082372\tCollections for Android developers\thttps://www.youtube.com/watch?v=6HHLqP0_spk\tCollections(Array, Lists, Queue, Set, HashMap, TreeMap, ArrayMap, SparceArray, boxing, mutable and immutable)\tlecture_online\t\\N\n82\t2020-02-17 12:44:59.94523\t2020-03-11 11:55:59.980381\tAdvanced Java and Kotlin for Android developers\thttps://www.youtube.com/watch?v=mh6LV9aBypo\tGenerics. Static and Dynamic binding. Generics in Kotlin. (SOLID, Clean Code).\tlecture_online\t\\N\n83\t2020-02-17 12:49:12.081105\t2020-03-11 12:03:58.296963\tBase Android Components Overview\thttps://www.youtube.com/watch?v=KINkdbIfwdU\tApp Manifest (Data Backup, Permissions, App Components overview)\tlecture_online\t\\N\n125\t2020-03-19 09:18:54.244884\t2020-03-19 09:18:54.244884\t[iOS] Quiz\t\\N\t\\N\tinfo\t\\N\n126\t2020-06-08 16:27:06.413013\t2020-06-08 16:29:35.410611\t[iOS] Multithreading basics, NSOperation/GCD overview full functionality (part 1)\t\\N\tMultithreading\tlecture_mixed\t\\N\n127\t2020-06-08 16:30:18.594107\t2020-06-08 16:30:18.594107\t[iOS] Multithreading basics, NSOperation/GCD overview full functionality (part 2)\t\\N\tMultithreading\tlecture_mixed\t\\N\n128\t2020-06-08 16:35:16.780715\t2020-06-08 16:35:16.780715\t[iOS] App Sandbox and Bundle, NSUserDefaults, read/writing to file\t\\N\tApp Sandbox and Bundle, NSUserDefaults, read/writing to file\tlecture_mixed\t\\N\n129\t2020-06-08 16:35:53.342328\t2020-06-08 16:35:53.342328\t[iOS] Networking (CRUD, JSON, XML), NSURLSession\t\\N\tNetworking (CRUD, JSON, XML), NSURLSession\tlecture_mixed\t\\N\n130\t2020-06-08 16:36:22.196423\t2020-06-08 16:36:22.196423\t[iOS] Animations (UIView animation, CALayer animation ...)\t\\N\tAnimations (UIView animation, CALayer animation ...)\tlecture_mixed\t\\N\n131\t2020-06-08 16:36:47.014748\t2020-06-08 16:36:47.014748\t[iOS] Unit Tests (OCMock, XCTest)\t\\N\tUnit Tests (OCMock, XCTest)\tlecture_mixed\t\\N\n132\t2020-06-08 16:37:12.751094\t2020-06-08 16:37:12.751094\t[iOS] SQLLite\t\\N\tSQLLite\tlecture_mixed\t\\N\n133\t2020-06-08 16:37:35.982116\t2020-06-08 16:37:35.982116\t[iOS] Core Data\t\\N\tCore Data\tlecture_mixed\t\\N\n134\t2020-06-08 16:38:01.843965\t2020-06-08 16:38:01.843965\t[iOS] CocoaPods\t\\N\tCocoaPods\tlecture_mixed\t\\N\n136\t2020-06-08 16:40:17.139789\t2020-07-24 07:20:19.886061\t[iOS, Android] Patterns part2, (Adaptor, Bridge, Decorator, Facade, Proxy, MVP)\thttps://youtu.be/Dh1ktKpq9Fc\tAdaptor, Bridge, Decorator, Facade, Proxy, MVP\tlecture_mixed\t\\N\n137\t2020-06-08 16:41:04.611035\t2021-04-17 22:16:20.70863\t[iOS, Android] Patterns part1, (Factory Method, Abstact Factory,  Bulder, Singleton,  MVC)\thttps://www.youtube.com/watch?v=oMjzSNIbkg8\tFactory Method, Abstact Factory,  Bulder, Singleton,  MVC\tlecture_mixed\t\\N\n138\t2020-06-08 16:41:38.872838\t2020-08-04 19:40:39.79378\t[iOS, Android] Patterns part3, (Observer, Strategy, Command, State, MVVM)\thttps://youtu.be/dbdqeZ17E-4\tObserver, Strategy, Command, State, MVVM\tlecture_mixed\t\\N\n139\t2020-06-08 16:42:23.750115\t2020-07-30 14:20:12.261478\t[iOS, Android] Patterns part4, Inversion of Control (dependency injection, Service Locator), GRASP\thttps://www.youtube.com/watch?v=lKX_jw052Yk&feature=youtu.be\tdependency injection, Service Locator, GRASP\tlecture_mixed\t\\N\n140\t2020-06-08 18:45:58.708297\t2020-06-09 10:20:02.88197\t[Android] Storage Part 1 (FileStorage, FileProvider, External and Internal Storage, SharedPreferencies, PreferenceFragment)\thttps://www.youtube.com/watch?v=y9pRcpRb9aE\t\\N\tlecture_self_study\t\\N\n141\t2020-06-08 18:46:10.614889\t2020-06-08 18:46:10.614889\t[iOS] Swift, part 1 (Initialization, property, types, class)\t\\N\tSwift, part 1\tlecture_mixed\t\\N\n142\t2020-06-08 18:46:25.265151\t2020-06-11 11:57:49.773017\t[Android] Storage Part 2(SQLite, pain of Cursor)\thttps://www.youtube.com/watch?v=latY2xfh2OY\t\\N\tlecture_online\t\\N\n143\t2020-06-08 18:46:50.319888\t2020-06-08 18:46:50.319888\t[iOS] Swift, part 2 (Enums, Protocols, Extensions)\t\\N\tSwift, part 2\tlecture_mixed\t\\N\n144\t2020-06-08 18:47:48.716992\t2020-06-08 18:47:48.716992\t[iOS] Swift, part 3 (Collections, Closures)\t\\N\tSwift, part 3\tlecture_mixed\t\\N\n145\t2020-06-08 18:48:42.307624\t2020-06-08 18:48:42.307624\t[iOS] Swift,  part 4 (Generics)\t\\N\tSwift,  part 4\tlecture_mixed\t\\N\n146\t2020-06-08 18:48:59.453278\t2020-06-16 11:02:49.978649\t[Android] Storage Part 3(ORM: ORMLite, GreenDao)\thttps://www.youtube.com/watch?v=fcJvn5MpBoY&feature=youtu.be\t\\N\tlecture_online\t\\N\n147\t2020-06-08 18:49:18.177322\t2020-06-08 18:49:18.177322\t[iOS] Swift, part 5 (Error handling, ARC, Access levels)\t\\N\tSwift, part 5\tlecture_mixed\t\\N\n148\t2020-06-08 18:50:11.211062\t2020-06-08 18:50:11.211062\t[iOS] Swift, part 6 (UnitTests, UITests)\t\\N\tSwift, part 6\tlecture_mixed\t\\N\n149\t2020-06-08 18:51:07.115575\t2020-07-07 14:31:49.785689\t[Android] Demo: Creating settings screen with PreferenceFragment\thttps://www.youtube.com/watch?v=lcPO4sPUmQ0&feature=youtu.be\t\\N\tlecture_online\t\\N\n150\t2020-06-08 18:53:19.739061\t2020-06-23 14:43:24.657466\t[Android] Storage Part 4 (Realm, NoSQL, Firebase database, Firestore)\thttps://www.youtube.com/watch?v=RiQ0Fq9drpQ&feature=youtu.be\t\\N\tlecture_online\t\\N\n151\t2020-06-08 18:54:32.119471\t2020-06-25 14:43:07.646511\t[Android] Storage Part 5 (Room and LiveData overview)\thttps://www.youtube.com/watch?v=rSt4vlCr06k&feature=youtu.be\t\\N\tlecture_online\t\\N\n152\t2020-06-08 18:56:38.906763\t2020-06-30 14:01:23.917011\t[Android] Demo: Firestore\thttps://www.youtube.com/watch?v=Zu_GLyYD_Zk&feature=youtu.be\t\\N\tlecture_online\t\\N\n153\t2020-06-08 18:58:27.500847\t2020-07-02 09:17:56.488663\t[Android] Networking (CRUD, JSON, XML), HttpUrlConnection, OkHttp\thttps://www.youtube.com/watch?v=8MvM47n3inw&feature=youtu.be\t\\N\tlecture_online\t\\N\n154\t2020-06-08 18:59:56.539253\t2020-07-07 14:27:16.387969\t[Android] REST, Retrofit, Gson, Moshi, GraphQL overview\thttps://www.youtube.com/watch?v=7qI-W6qI8T4&feature=youtu.be\t\\N\tlecture_online\t\\N\n155\t2020-06-08 19:01:19.319504\t2020-07-09 10:55:59.44469\t[Android] Quality Assurance (Detekt, ktlint, AndroidLint, SonarQube, CI basics)\thttps://youtu.be/csWGsOK2xYk\t\\N\tlecture_online\t\\N\n156\t2020-06-08 19:04:43.156329\t2020-07-14 14:58:47.045131\t[Android] Demo: Working on the real project\thttps://www.youtube.com/watch?v=TTm_z64fWlk\t\\N\tlecture_online\t\\N\n157\t2020-06-08 19:12:19.91916\t2020-07-16 17:00:25.594532\t[Android] Build Configuration (Gradle, groovy vs kotlin, settings, BuildType, BuildFlavor, Plugins, buildSrc)\thttps://youtu.be/B4qoxeGSPOs\t\\N\tlecture_online\t\\N\n158\t2020-06-08 19:19:32.3009\t2020-08-04 20:20:43.827678\t[Android] DI (Dagger2, Koin)\thttps://youtu.be/aMwpHwLrxpE\t\\N\tlecture_online\t\\N\n159\t2020-06-08 19:20:29.24446\t2020-08-23 13:33:39.790374\t[Android] Clean Architecture, ViewModel and LiveData(MVVM by Google)\thttps://www.youtube.com/watch?v=v6xPnjZAL2U\t\\N\tlecture_online\t\\N\n160\t2020-06-08 19:21:44.853351\t2020-08-23 13:34:39.408732\t[Android] ReactiveX, RxJava, RxKotlin, Reaktive\thttps://www.youtube.com/watch?v=Q3e5R6KN1EM\t\\N\tlecture_online\t\\N\n161\t2020-06-08 19:23:04.841744\t2020-08-23 13:35:11.981109\t[Android] Kotlin Coroutines and Flow\thttps://www.youtube.com/watch?v=SLW2sm4YA_4\t\\N\tlecture_online\t\\N\n162\t2020-06-08 19:25:45.83293\t2020-09-22 08:18:37.263457\t[Android] Android Architecture Components(Lifecycle, Navigation, WorkManager, PagingLibrary, Preference)\thttps://www.youtube.com/watch?v=kShzWyBMjf4&feature=youtu.be&ab_channel=RollingScopesSchool\t\\N\tlecture_online\t\\N\n163\t2020-06-08 19:26:46.002742\t2020-09-22 08:51:58.883544\t[Android] Tests (Junit, Mockito, Mockk, Spek2, Espresso)\thttps://youtu.be/4LIgv91S8G8\t\\N\tlecture_online\t\\N\n164\t2020-07-21 16:55:08.461138\t2020-07-21 16:55:55.005421\t[iOS, Android] Working on the real project\t\\N\tCD/CI, Scrum, TDD....\tlecture_mixed\t\\N\n165\t2020-07-27 10:31:38.167381\t2021-07-10 22:05:56.133842\tAngular. HTTP\t\\N\t\\N\tworkshop\t\\N\n166\t2020-08-05 09:25:59.634529\t2020-08-05 09:25:59.634529\tAngular. Task #5 review\t\\N\t\\N\tlecture_online\t\\N\n167\t2021-01-18 20:30:53.435008\t2021-01-18 20:30:53.435008\tAngular. Final task \"RS Lang\". Intro\t\\N\t\\N\tlecture_online\t\\N\n168\t2021-04-17 21:16:46.854068\t2021-07-27 21:10:20.762053\t[iOS] Swift: Fundamentals, part1 (Classes, Structs, Init, Deinit)\thttps://youtu.be/bbDZ3vBjq-s\t\\N\tlecture_mixed\t\\N\n169\t2021-04-17 21:18:00.021065\t2021-07-27 21:10:33.318624\t[iOS] Swift: Fundamentals, part2 (Protocols, Extensions, Access control)\thttps://youtu.be/Zem7azTDTfA\t\\N\tlecture_mixed\t\\N\n170\t2021-04-17 21:19:09.995838\t2021-07-27 21:10:48.661699\t[iOS] Swift: Enum, Optionals, Properties\thttps://youtu.be/ecBhz5YITG4\t\\N\tlecture_mixed\t\\N\n171\t2021-04-17 21:20:01.309681\t2021-07-27 21:10:59.643765\t[iOS] Swift: Collections\thttps://youtu.be/N0HDxnj8zuo\t\\N\tlecture_mixed\t\\N\n172\t2021-04-17 21:21:04.644787\t2021-07-27 21:11:52.391729\t[iOS] Swift: Type casting, Nesting types, Opaque type\thttps://youtu.be/skD3iO-l6Lw\t\\N\tlecture_mixed\t\\N\n173\t2021-04-17 21:21:32.675193\t2021-07-27 21:11:34.326737\t[iOS] Swift: Closures\thttps://youtu.be/DqqrkbU6Csc\t\\N\tlecture_mixed\t\\N\n174\t2021-04-17 21:21:57.787204\t2021-07-27 21:12:05.272894\t[iOS] Swift: Generics\thttps://youtu.be/OkvvfNuhRrM\t\\N\tlecture_mixed\t\\N\n175\t2021-04-17 21:22:21.683772\t2021-07-27 21:12:16.2674\t[iOS] Swift: ARC, Error handling\thttps://youtu.be/I520sje9g7M\t\\N\tlecture_mixed\t\\N\n176\t2021-04-17 21:32:33.51823\t2021-07-27 21:18:02.98113\t[iOS] Stage 1\thttps://youtu.be/NjE4LVIcpQI\t\\N\tinfo\t\\N\n177\t2021-04-17 21:32:52.407919\t2021-04-17 21:32:52.407919\t[iOS] Stage 2\t\\N\t\\N\tinfo\t\\N\n178\t2021-04-17 21:33:08.809654\t2021-04-17 21:33:08.809654\t[iOS] Stage 3\t\\N\t\\N\tinfo\t\\N\n179\t2021-04-17 22:07:11.429289\t2021-04-17 22:07:11.429289\t[iOS] Unit Tests (ObjC: OCMock, XCTest, Swift: Quick, Nimble)\t\\N\t\\N\tlecture_mixed\t\\N\n180\t2021-04-17 22:11:36.745265\t2021-04-17 22:11:36.745265\t[iOS] CocoaPods, Swift Package Manager (SPM)\t\\N\t\\N\tlecture_mixed\t\\N\n181\t2021-04-17 22:19:11.535059\t2021-04-17 22:19:11.535059\t[iOS] Assessment period\t\\N\t\\N\tinfo\t\\N\n182\t2021-04-18 17:22:05.305628\t2021-04-18 17:22:05.305628\t[iOS] Final Task - Assessment\t\\N\t\\N\tinfo\t\\N\n183\t2021-04-18 17:24:53.506302\t2021-04-18 17:24:53.506302\t[iOS] Result (Summarize)\t\\N\t\\N\tinfo\t\\N\n184\t2021-05-24 07:20:56.730632\t2021-05-24 07:20:56.730632\tSoftware design principles. SOLID\thttps://www.youtube.com/rollingscopesschool\ta.        Single Responsibility Principle \\nb.        Open-Closed Principle\\nc.        Liskov Substitution Principle \\nd.        Interface Segregation Principle \\ne.        Dependency Inversion Principle\tOnline Lecture\t\\N\n185\t2021-06-22 11:41:18.759195\t2021-06-22 11:41:18.759195\tQ&A: Шахматы + English for kids\thttps://youtube.com\tОтветы на вопросы по новому заданию\tlecture_online\t\\N\n186\t2021-06-22 14:07:40.862461\t2021-06-22 14:07:40.862461\tВыдача сертификатов stage#2\thttps://docs.rs.school/#/rs-school-certificate\t\\N\tInfo\t\\N\n187\t2021-06-25 08:57:13.764023\t2021-07-23 10:09:43.479324\tЗнакомство с RS School и профессией \"JS/Front-end разработчик\"\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0/modules/js-fe-developer\t\\N\tOnline Lecture\t\\N\n188\t2021-06-25 11:09:37.325536\t2021-07-13 17:03:02.123734\tQ&A Stage#0\thttps://docs.google.com/spreadsheets/d/1QXlD5uknJLDjYmPcRhSqaKAw8VACwypvGOuXy-MFaYs/edit#gid=0\t\\N\tOnline Lecture\t\\N\n189\t2021-06-25 11:17:15.225806\t2021-06-25 11:17:15.225806\tNodeJS. Live Coding\t\\N\t\\N\tlecture_online\t\\N\n190\t2021-06-29 07:21:48.844085\t2021-06-29 07:22:21.456235\tChrome Dev Tools и VS Code\thttps://github.com/rolling-scopes-school/tasks/tree/roadmap/stage0/modules/basic-tools\t\\N\tOnline Lecture\t\\N\n191\t2021-06-30 12:27:36.94908\t2021-06-30 12:27:36.94908\tStage#0. Неделя #1\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-1\t\\N\tSelf-studying\t\\N\n192\t2021-06-30 12:43:17.150567\t2021-06-30 12:43:17.150567\tRefactoring Lecture\t\\N\t\\N\tlecture_online\t\\N\n193\t2021-07-01 14:03:58.0844\t2021-07-01 14:03:58.0844\tStage#0. Неделя #2\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-2\t\\N\tSelf-studying\t\\N\n194\t2021-07-01 14:04:43.442034\t2021-07-01 14:04:43.442034\tStage#0. Неделя #3\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-3\t\\N\tSelf-studying\t\\N\n195\t2021-07-01 14:05:29.684334\t2021-07-01 14:05:29.684334\tStage#0. Неделя #4\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-4\t\\N\tSelf-studying\t\\N\n196\t2021-07-01 14:06:12.526154\t2021-07-01 14:06:12.526154\tStage#0. Неделя #5\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-5\t\\N\tSelf-studying\t\\N\n197\t2021-07-01 14:08:08.564507\t2021-07-01 14:08:08.564507\tStage#0. Неделя #6\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-6\t\\N\tSelf-studying\t\\N\n198\t2021-07-01 14:08:56.268658\t2021-07-01 14:08:56.268658\tStage#0. Неделя #7\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-7\t\\N\tSelf-studying\t\\N\n199\t2021-07-01 14:10:04.277781\t2021-07-01 14:10:04.277781\tStage#0. Неделя #8\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-8\t\\N\tSelf-studying\t\\N\n200\t2021-07-01 14:11:13.939302\t2021-07-01 14:13:55.089393\tStage#0. Неделя #9\thttps://github.com/rolling-scopes-school/tasks/tree/master/stage0#%D0%BD%D0%B5%D0%B4%D0%B5%D0%BB%D1%8F-9\t\\N\tSelf-studying\t\\N\n201\t2021-07-05 20:57:07.500082\t2021-07-07 15:21:56.6395\tNode.js Basic\thttps://youtube.com\t\\N\tlecture_online\t\\N\n202\t2021-07-06 09:38:49.342292\t2021-07-08 06:13:01.627643\tCross-Check deadline: English for kids S1E1\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids.md\t\\N\tCross-Check deadline\t\\N\n204\t2021-07-13 02:28:26.44619\t2021-07-27 02:50:11.112532\tReact Stream. Components\thttps://docs.google.com/document/d/1WLWjBiVMjsVADf5FWFYfPObQOrLD1624h5etyafCfr8/edit?usp=sharing\tSpreadsheet for questions: https://docs.google.com/spreadsheets/d/1qSfNHkOLqK6XXliXJDbY5QL7c9reWYrsxNaTPZjgQ4o/edit?usp=sharing\tlecture_online\t\\N\n205\t2021-07-13 02:29:41.009407\t2021-07-27 03:37:03.415482\tReact Stream. Forms\thttps://docs.google.com/document/d/1C490mF-CzPkr2552nDcj3W3NJmrzXJKFBSs4C_Vg_cM/edit?usp=sharing\tSpreadsheet for questions: https://docs.google.com/spreadsheets/d/1wvdN5bmMcnXM_sc4l5NmOvrgM428fCf17PcwwH996Dg/edit?usp=sharing\tlecture_online\t\\N\n206\t2021-07-13 02:30:59.860857\t2021-07-13 02:30:59.860857\tReact Stream. API\t\\N\tSpreadsheet for questions: https://docs.google.com/spreadsheets/d/10WdCIZj6u2dLJm1Nn7UYt5rznLds42mxsjjJLJpYxvk/edit?usp=sharing\tlecture_online\t\\N\n207\t2021-07-13 02:32:06.692641\t2021-07-27 03:48:48.693306\tReact Stream. Redux\thttps://docs.google.com/document/d/11SOrFH5RSSmSaJia5XbeD02hJVwY5fsc1PJwEbOXg_A/edit?usp=sharing\tSpreadsheet for questions: https://docs.google.com/spreadsheets/d/1uxAgIrKso99fhi3svvIeWMTlyrLRByJhOTC0ttAMxeM/edit?usp=sharing\tlecture_online\t\\N\n208\t2021-07-13 02:33:12.930287\t2021-07-27 03:46:43.586183\tReact Stream. Routing\thttps://docs.google.com/document/d/1SrT0rl-YG0cMheXgHsI3H2u8hCKCImEYiFvQsnOw9Q8/edit?usp=sharing\tSpreadsheet for questions: https://docs.google.com/spreadsheets/d/14czN-v9qQMKfRGfwHHiFki0pA8kgRUKw3dd_7ZW8jyA/edit?usp=sharing\tlecture_online\t\\N\n209\t2021-07-13 02:34:19.795533\t2021-07-13 02:34:19.795533\tReact Stream. Testing\t\\N\tSpreadsheet for questions: https://docs.google.com/spreadsheets/d/1z5_B3-UA3R4-GtTm2hnqMEPBSDmkcZbd6sVsKLVzI5w/edit?usp=sharing\tlecture_online\t\\N\n210\t2021-07-13 02:42:32.368104\t2021-07-13 02:42:32.368104\tReact Streaming. SSR\t\\N\tQuestions: https://docs.google.com/spreadsheets/d/1z4B3WLStS0UME0ok-Prm2KUPc_fFVS34Q7dJALI3E64/edit?usp=sharing\tlecture_online\t\\N\n211\t2021-07-13 18:54:42.565201\t2021-07-16 13:18:08.778557\tGit for beginners\t\\N\tIntroduction to Git\tOnline Lecture\t\\N\n212\t2021-07-16 11:12:11.167369\t2021-07-16 11:12:44.404702\tCross-Check deadline: English for kids S1E2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids-admin-panel.md\t\\N\tCross-Check deadline\t\\N\n213\t2021-07-20 13:47:14.868153\t2021-07-20 13:49:26.368869\tCross-check deadline: Chess S1E2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/chess/codejam-chess-part-two.md\t\\N\tCross-Check deadline\t\\N\n135\t2022-03-27 12:11:38.539172\t2022-03-27 12:11:38.539172\t11\thttps://hello.com\t\\N\tOffline Lecture\t\\N\n203\t2022-03-27 12:12:46.314579\t2022-03-27 12:12:46.314579\t11\thttps://hello.com\t\\N\tOffline Lecture\t\\N\n\\.\n\n\n--\n-- Data for Name: feedback; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.feedback (id, \"createdDate\", \"updatedDate\", \"badgeId\", \"fromUserId\", \"toUserId\", \"courseId\", comment) FROM stdin;\n616\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t3493\t677\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n617\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t677\t587\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n618\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t2444\t2595\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n619\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t4428\t10130\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n620\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t606\t677\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n621\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t2595\t10130\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n622\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t2693\t11563\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n623\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t2480\t6776\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n624\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t606\t4428\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n625\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t1090\t4428\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n626\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t4428\t2480\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n627\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t2103\t587\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n628\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t1090\t2098\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n629\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t677\t10130\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n630\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t3961\t2098\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n631\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tExpert_help\t2480\t2480\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n632\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t606\t2612\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n633\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t2084\t587\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n634\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t1328\t677\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n635\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t2084\t4476\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n636\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t2549\t2595\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n637\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHero\t587\t2089\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n638\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t677\t11569\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n639\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t4428\t2098\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n640\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t2480\t7485\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n641\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t587\t2595\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n642\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t1090\t10031\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n643\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t6776\t677\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n644\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t7485\t10031\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n645\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t7485\t2444\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n646\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHero\t2595\t2084\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n647\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t10130\t2693\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n648\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHero\t11569\t11569\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n649\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHero\t2103\t10130\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n650\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tExpert_help\t3961\t2084\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n651\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t5481\t1090\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n652\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t3961\t677\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n653\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t5481\t2115\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n654\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t2089\t2693\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n655\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t2595\t1090\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n656\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t2612\t4428\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n657\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t2595\t11569\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n658\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t3961\t2693\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n659\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGood_job\t11569\t677\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n660\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t4749\t2693\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n661\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t2115\t2115\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n662\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t2444\t11563\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n663\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t2089\t2103\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n664\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t2084\t11569\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n665\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t587\t606\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n666\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t2480\t677\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n667\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t587\t2032\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n668\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t10031\t2084\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n669\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t606\t2612\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n670\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t11569\t2549\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n671\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t4749\t2098\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n672\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t2549\t11563\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n673\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t1090\t2480\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n674\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t7485\t677\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n675\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t7485\t2277\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n676\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t3961\t1090\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n677\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t7485\t1090\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n678\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t2480\t5481\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n679\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t3961\t10130\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n680\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t2595\t5481\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n681\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tExpert_help\t2277\t2612\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n682\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t11569\t2032\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n683\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t4749\t677\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n684\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tRS_activist\t1328\t2480\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n685\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t4428\t2612\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n686\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHero\t4749\t4476\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n687\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t2480\t10130\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n688\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t2115\t4428\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n689\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGood_job\t4749\t2103\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n690\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t2089\t2693\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n691\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGreat_speaker\t2693\t5481\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n692\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t4749\t677\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n693\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t2549\t606\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n694\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tExpert_help\t4476\t1090\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n695\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t2103\t11569\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n696\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t5481\t1090\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n697\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t4476\t2612\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n698\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGood_job\t4749\t2595\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n699\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t2115\t11563\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n700\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t2693\t677\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n701\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t11569\t2084\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n702\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJury_Team\t2444\t1090\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n703\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tGood_job\t1328\t606\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n704\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tTop_performer\t4749\t2549\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n705\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t10031\t3961\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n706\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t2693\t2098\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n707\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t2084\t2549\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n708\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tCongratulations\t1328\t4428\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n709\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t4428\t10130\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n710\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t2089\t4428\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n711\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tJob_Offer\t4476\t4428\t13\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n712\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tHelping_hand\t587\t7485\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n713\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tThank_you\t2103\t606\t11\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n714\t2023-08-26 14:16:06.484519\t2023-08-26 14:16:06.484519\tOutstanding_work\t2693\t3961\t23\tPariatur culpa exercitation occaecat consectetur nulla nulla Lorem pariatur ut esse.\n\\.\n\n\n--\n-- Data for Name: history; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.history (id, \"createdDate\", \"updatedDate\", event, \"entityId\", operation, update, previous) FROM stdin;\n1\t2023-01-31 20:05:46.630426\t2023-01-31 20:05:46.630426\tcourse_task\t821\tupdate\t{\"studentStartDate\":\"2021-02-28 10:00+00:00\",\"studentEndDate\":\"2021-03-09 23:59+00:00\",\"crossCheckEndDate\":\"2023-01-17 22:05+00:00\",\"taskId\":593,\"taskOwnerId\":2084,\"checker\":\"crossCheck\",\"scoreWeight\":0.2,\"maxScore\":100,\"type\":\"htmltask\",\"pairsCount\":4,\"submitText\":null,\"validations\":{},\"courseId\":23,\"id\":821}\t{\"id\":821,\"createdDate\":\"2021-02-28T09:07:38.664Z\",\"updatedDate\":\"2021-03-06T09:06:02.437Z\",\"taskId\":593,\"courseId\":23,\"studentStartDate\":\"2021-02-28T10:00:00.000Z\",\"studentEndDate\":\"2021-03-08T23:59:00.000Z\",\"crossCheckEndDate\":null,\"mentorStartDate\":null,\"mentorEndDate\":null,\"maxScore\":100,\"scoreWeight\":0.2,\"checker\":\"crossCheck\",\"taskOwnerId\":2084,\"pairsCount\":4,\"type\":\"htmltask\",\"disabled\":false,\"crossCheckStatus\":\"initial\",\"submitText\":null,\"validations\":null}\n2\t2023-01-31 20:06:50.33724\t2023-01-31 20:06:50.33724\tcourse_task\t725\tupdate\t{\"studentStartDate\":\"2021-02-28 23:59+00:00\",\"studentEndDate\":\"2021-03-17 23:59+00:00\",\"taskId\":473,\"taskOwnerId\":2084,\"checker\":\"mentor\",\"scoreWeight\":0.05,\"maxScore\":100,\"type\":\"jstask\",\"courseId\":23,\"id\":725}\t{\"id\":725,\"createdDate\":\"2020-02-19T15:18:06.945Z\",\"updatedDate\":\"2021-03-06T09:28:56.637Z\",\"taskId\":473,\"courseId\":23,\"studentStartDate\":\"2021-02-28T23:59:00.000Z\",\"studentEndDate\":\"2021-03-15T23:59:00.000Z\",\"crossCheckEndDate\":null,\"mentorStartDate\":null,\"mentorEndDate\":null,\"maxScore\":100,\"scoreWeight\":0.05,\"checker\":\"mentor\",\"taskOwnerId\":2084,\"pairsCount\":null,\"type\":\"jstask\",\"disabled\":false,\"crossCheckStatus\":\"initial\",\"submitText\":null,\"validations\":null}\n3\t2023-01-31 20:08:42.169821\t2023-01-31 20:08:42.169821\tcourse_task\t846\tupdate\t{\"studentStartDate\":\"2021-02-28 23:59+00:00\",\"studentEndDate\":\"2021-03-19 23:59+00:00\",\"taskId\":473,\"taskOwnerId\":2084,\"checker\":\"mentor\",\"scoreWeight\":0.05,\"maxScore\":100,\"type\":\"jstask\",\"courseId\":23,\"id\":846}\t{\"id\":846,\"createdDate\":\"2021-03-16T04:33:39.267Z\",\"updatedDate\":\"2021-03-22T19:29:25.192Z\",\"taskId\":625,\"courseId\":23,\"studentStartDate\":\"2021-03-16T04:32:00.000Z\",\"studentEndDate\":\"2021-03-23T01:59:00.000Z\",\"crossCheckEndDate\":null,\"mentorStartDate\":null,\"mentorEndDate\":null,\"maxScore\":50,\"scoreWeight\":1,\"checker\":\"crossCheck\",\"taskOwnerId\":2084,\"pairsCount\":4,\"type\":\"jstask\",\"disabled\":false,\"crossCheckStatus\":\"initial\",\"submitText\":null,\"validations\":null}\n4\t2023-01-31 20:09:55.297027\t2023-01-31 20:09:55.297027\tcourse_task\t766\tupdate\t{\"studentStartDate\":\"2020-12-25 21:59+00:00\",\"studentEndDate\":\"2021-03-10 23:59+00:00\",\"taskId\":595,\"taskOwnerId\":2084,\"checker\":\"auto-test\",\"scoreWeight\":0.1,\"maxScore\":100,\"type\":\"selfeducation\",\"courseId\":23,\"id\":766}\t{\"id\":766,\"createdDate\":\"2020-12-26T18:38:03.970Z\",\"updatedDate\":\"2021-03-06T09:02:07.383Z\",\"taskId\":595,\"courseId\":23,\"studentStartDate\":\"2020-12-25T21:59:00.000Z\",\"studentEndDate\":\"2021-03-08T23:59:00.000Z\",\"crossCheckEndDate\":null,\"mentorStartDate\":null,\"mentorEndDate\":null,\"maxScore\":100,\"scoreWeight\":0.1,\"checker\":\"auto-test\",\"taskOwnerId\":2084,\"pairsCount\":null,\"type\":\"selfeducation\",\"disabled\":false,\"crossCheckStatus\":\"initial\",\"submitText\":null,\"validations\":null}\n5\t2023-01-31 20:16:09.766741\t2023-01-31 20:16:09.766741\tcourse_task\t821\tupdate\t{\"studentStartDate\":\"2021-02-28T10:00:00Z\",\"studentEndDate\":\"2021-03-09T23:59:00Z\",\"crossCheckEndDate\":\"2023-01-18T23:59:00Z\",\"taskId\":593,\"taskOwnerId\":2084,\"checker\":\"crossCheck\",\"scoreWeight\":0.2,\"maxScore\":100,\"type\":\"htmltask\",\"pairsCount\":4,\"submitText\":null,\"validations\":{},\"courseId\":23,\"id\":821}\t{\"id\":821,\"createdDate\":\"2021-02-28T09:07:38.664Z\",\"updatedDate\":\"2023-01-31T20:05:46.644Z\",\"taskId\":593,\"courseId\":23,\"studentStartDate\":\"2021-02-28T10:00:00.000Z\",\"studentEndDate\":\"2021-03-09T23:59:00.000Z\",\"crossCheckEndDate\":\"2023-01-17T22:05:00.000Z\",\"mentorStartDate\":null,\"mentorEndDate\":null,\"maxScore\":100,\"scoreWeight\":0.2,\"checker\":\"crossCheck\",\"taskOwnerId\":2084,\"pairsCount\":4,\"type\":\"htmltask\",\"disabled\":false,\"crossCheckStatus\":\"initial\",\"submitText\":null,\"validations\":{}}\n6\t2024-06-19 18:49:00.884833\t2024-06-19 18:49:00.884833\tcourse_task\t432\tinsert\t{\"studentStartDate\":\"2024-06-19T00:00:00Z\",\"studentEndDate\":\"2027-07-16T23:59:59Z\",\"taskId\":499,\"taskOwnerId\":11569,\"checker\":\"mentor\",\"scoreWeight\":1,\"maxScore\":10,\"type\":\"interview\",\"courseId\":23,\"id\":432,\"createdDate\":\"2024-06-19T18:49:00.876Z\",\"updatedDate\":\"2024-06-19T18:49:00.876Z\",\"disabled\":false,\"crossCheckStatus\":\"initial\"}\t\\N\n\\.\n\n\n--\n-- Data for Name: interview_question; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.interview_question (id, \"createdDate\", \"updatedDate\", title, question) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: interview_question_categories_interview_question_category; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.interview_question_categories_interview_question_category (\"interviewQuestionId\", \"interviewQuestionCategoryId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: interview_question_category; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.interview_question_category (id, \"createdDate\", \"updatedDate\", name) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: login_state; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.login_state (id, \"createdDate\", data, \"userId\", expires) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: mentor; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.mentor (id, \"createdDate\", \"updatedDate\", \"maxStudentsLimit\", \"courseId\", \"userId\", \"studentsPreference\", \"isExpelled\") FROM stdin;\n1266\t2020-04-06 15:39:35.609875\t2020-04-06 15:39:35.609875\t\\N\t13\t2595\t\\N\tf\n1267\t2020-04-06 15:39:40.768722\t2020-04-06 15:39:40.768722\t\\N\t13\t2612\t\\N\tf\n1268\t2020-04-06 15:39:46.991811\t2020-04-06 15:39:46.991811\t\\N\t13\t2084\t\\N\tf\n1269\t2020-04-06 15:39:51.547456\t2020-04-06 15:39:51.547456\t\\N\t13\t2032\t\\N\tf\n1272\t2020-04-06 15:39:35.609875\t2020-04-06 15:39:35.609875\t\\N\t23\t2595\t\\N\tf\n1273\t2020-04-06 15:39:40.768722\t2020-04-06 15:39:40.768722\t\\N\t23\t2612\t\\N\tf\n1274\t2020-04-06 15:39:46.991811\t2020-04-06 15:39:46.991811\t\\N\t23\t2084\t\\N\tf\n1275\t2020-04-06 15:39:51.547456\t2020-04-06 15:39:51.547456\t\\N\t23\t2032\t\\N\tf\n1276\t2024-06-19 18:50:36.764741\t2024-06-19 18:50:36.764741\t2\t23\t11569\tany\tf\n\\.\n\n\n--\n-- Data for Name: mentor_registry; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.mentor_registry (id, \"userId\", \"preferedCourses\", \"maxStudentsLimit\", \"englishMentoring\", \"preferedStudentsLocation\", \"createdDate\", \"updatedDate\", \"technicalMentoring\", \"preselectedCourses\", canceled, \"languagesMentoring\", comment, \"receivedDate\", \"sendDate\") FROM stdin;\n290\t11569\t13\t2\tf\tany\t2024-06-19 18:50:08.844455\t2024-06-19 18:50:27.412364\tJavaScript,NodeJs\t23\tf\tAR\t\\N\t\\N\t2024-06-19 18:50:27.41\n\\.\n\n\n--\n-- Data for Name: migrations; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.migrations (id, \"timestamp\", name) FROM stdin;\n1\t1630340371992\tUserMigration1630340371992\n2\t1630341383942\tTaskResult1630341383942\n3\t1630342025950\tStudentMigration1630342025950\n4\t1630342266002\tUserMigration1630342266002\n5\t1630347897950\tStudentMigration1630347897950\n6\t1632333725126\tResumeMigration1632333725126\n7\t1635365797478\tUser1635365797478\n8\t1637591194886\tStageInterview1637591194886\n9\t1639418471577\tIndicies1639418471577\n10\t1638302439645\tCourseMigration1638302439645\n11\t1639427578702\tUpdate1639427578702\n12\t1639502600339\tStudent1639502600339\n13\t1642884123347\tResumeSelectCourses1642884123347\n14\t1643481312933\tTask1643481312933\n15\t1643550350939\tLoginState1643550350939\n16\t1643926895264\tNotifications1643926895264\n17\t1644695410918\tNotificationConnection1644695410918\n18\t1645364514538\tRepositoryEvent1645364514538\n19\t1645654601903\tOpportunitites1645654601903\n20\t1647175301446\tTaskSolutionConstraint1647175301446\n21\t1647550751147\tNotificationType1647550751147\n22\t1647885219936\tLoginStateUserId1647885219936\n23\t1647103154082\tCrossCheckScheduling1647103154082\n24\t1649505252996\tCourseLogo1649505252996\n25\t1649868994688\tCourseLogo1649868994688\n26\t1650652882300\tDiscordChannel1650652882300\n27\t1652870756742\tResume1652870756742\n28\t1656326258991\tHistory1656326258991\n29\t1661034658479\tFeedback1661034658479\n30\t1661087975938\tDiscipline1661087975938\n31\t1661106736439\tDisciplines1661106736439\n32\t1661107174477\tDisciplines1661107174477\n33\t1661616212488\tNotificationCategory1661616212488\n34\t1662275601017\tCourseTask1662275601017\n35\t1664183799115\tCourseEvent1664183799115\n36\t1666348642811\tTaskCriteria1666348642811\n37\t1666621080327\tTaskSolutionResult1666621080327\n38\t1671475396333\tTasks1671475396333\n39\t1672142743107\tTeamDistribution1672142743107\n40\t1672386450861\tTeamDistribution1672386450861\n41\t1673090827105\tTaskVerification1673090827105\n42\t1673692838338\tUser1673692838338\n43\t1674128274839\tTeam1674128274839\n44\t1674755854609\tResume1674755854609\n45\t1674377676805\tTeamDistributionStudent1674377676805\n46\t1675245424426\tUserGroup1675245424426\n47\t1675345245770\tCourse1675345245770\n48\t1676139987317\tUser1676139987317\n49\t1685197747051\tMentorRegistry1685197747051\n50\t1686657350908\tInterviewScore1686657350908\n51\t1687009744110\tPrompt1687009744110\n52\t1691520611773\tTemperature1691520611773\n53\t1691524327332\tTemperature1691524327332\n54\t1693930286280\tCourseUsersActivist1693930286280\n55\t1699808604000\tAddMinStudentPerMentorColumnToCourse1699808604000\n56\t1700391857109\tObfuscation1700391857109\n57\t1712137476312\tCourse1712137476312\n58\t1730926720293\tCourseTask1730926720293\n59\t1734874453585\tContributor1734874453585\n60\t1736458672717\tCourse1736458672717\n61\t1738250779923\tCoursePersonalMentoringDates1738250779923\n62\t1746467689328\tCourse1746467689328\n\\.\n\n\n--\n-- Data for Name: notification; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.notification (id, name, \"createdDate\", \"updatedDate\", type, enabled, \"parentId\") FROM stdin;\nmentorRegistrationApproval\tMentor registration approval\t2022-02-18 21:19:53.292291\t2022-02-18 21:19:53.292291\tmentor\tf\t\\N\ntaskGrade\tTask grade received\t2022-02-18 21:19:53.292291\t2022-02-18 21:19:53.292291\tstudent\tf\t\\N\n\\.\n\n\n--\n-- Data for Name: notification_channel; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.notification_channel (id, \"createdDate\", \"updatedDate\") FROM stdin;\nemail\t2022-02-18 21:19:53.292291\t2022-02-18 21:19:53.292291\ntelegram\t2022-02-18 21:19:53.292291\t2022-02-18 21:19:53.292291\ndiscord\t2023-01-26 18:24:37.509357\t2023-01-26 18:24:37.509357\n\\.\n\n\n--\n-- Data for Name: notification_channel_settings; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.notification_channel_settings (\"notificationId\", \"createdDate\", \"updatedDate\", \"channelId\", template) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: notification_user_connection; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.notification_user_connection (\"userId\", \"createdDate\", \"updatedDate\", \"channelId\", \"externalId\", enabled) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: notification_user_settings; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.notification_user_settings (\"notificationId\", \"createdDate\", \"updatedDate\", enabled, \"userId\", \"channelId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: private_feedback; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.private_feedback (id, comment, \"createdDate\", \"updatedDate\", \"courseId\", \"fromUserId\", \"toUserId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: profile_permissions; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.profile_permissions (id, \"createdDate\", \"updatedDate\", \"userId\", \"isProfileVisible\", \"isAboutVisible\", \"isEducationVisible\", \"isEnglishVisible\", \"isEmailVisible\", \"isTelegramVisible\", \"isSkypeVisible\", \"isPhoneVisible\", \"isContactsNotesVisible\", \"isLinkedInVisible\", \"isPublicFeedbackVisible\", \"isMentorStatsVisible\", \"isStudentStatsVisible\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: prompt; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.prompt (id, \"createdDate\", \"updatedDate\", type, text, temperature) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: registry; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.registry (id, type, status, \"createdDate\", \"updatedDate\", \"userId\", \"courseId\", attributes) FROM stdin;\n8953\tstudent\tapproved\t2020-04-06 15:15:02.782811\t2020-04-06 15:15:02.782811\t11563\t13\t{}\n8954\tstudent\tapproved\t2020-04-06 15:30:27.1162\t2020-04-06 15:30:27.1162\t677\t13\t{}\n8955\tstudent\tapproved\t2020-04-06 15:31:44.431228\t2020-04-06 15:31:44.431228\t1090\t13\t{}\n\\.\n\n\n--\n-- Data for Name: repository_event; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.repository_event (id, \"repositoryUrl\", action, \"githubId\", \"createdDate\", \"updatedDate\", \"userId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: resume; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.resume (id, \"githubId\", name, \"selfIntroLink\", \"startFrom\", \"fullTime\", expires, \"militaryService\", \"englishLevel\", \"avatarLink\", \"desiredPosition\", notes, phone, email, skype, telegram, linkedin, locations, \"githubUsername\", website, \"isHidden\", \"visibleCourses\", uuid, \"userId\", \"updatedDate\") FROM stdin;\n1\tvalerydluski\tv\t\\N\t2023-02-22\tf\t1678005529649\tserved\tA0\t\\N\tas\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\tv\t\\N\t\\N\tf\t{}\t4ca1a1ca-e98a-4ef2-81c2-f90e38605e8c\t11569\t2023-02-03 08:38:59.072447\n\\.\n\n\n--\n-- Data for Name: stage; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.stage (id, \"createdDate\", \"updatedDate\", name, \"courseId\", status, \"startDate\", \"endDate\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: stage_interview; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.stage_interview (id, \"createdDate\", \"updatedDate\", \"studentId\", \"mentorId\", \"stageId\", \"isCompleted\", decision, \"isGoodCandidate\", \"courseId\", \"courseTaskId\", \"isCanceled\", score) FROM stdin;\n10687\t2020-04-07 20:27:20.124459\t2020-04-07 20:27:20.124459\t14327\t1266\t\\N\tf\t\\N\t\\N\t13\t408\tf\t\\N\n10689\t2020-04-07 20:28:00.755084\t2020-04-07 21:07:08.374015\t14329\t1266\t\\N\tt\tnoButGoodCandidate\tt\t13\t408\tf\t\\N\n10688\t2020-04-07 20:27:41.249823\t2023-02-03 07:48:18.719106\t14329\t1266\t\\N\tf\t\\N\t\\N\t13\t408\tt\t\\N\n10690\t2024-06-19 18:51:41.305547\t2024-06-19 18:51:41.305547\t14337\t1276\t\\N\tf\t\\N\t\\N\t23\t719\tf\t\\N\n\\.\n\n\n--\n-- Data for Name: stage_interview_feedback; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.stage_interview_feedback (id, \"createdDate\", \"updatedDate\", \"stageInterviewId\", json, version) FROM stdin;\n1234\t2020-04-07 21:07:08.363918\t2020-04-07 21:07:08.363918\t10689\t{\"skills\":{\"htmlCss\":{\"level\":3},\"dataStructures\":{\"array\":3,\"stack\":4},\"common\":{\"binaryNumber\":4,\"sortingAndSearchAlgorithms\":3}},\"programmingTask\":{\"resolved\":1,\"codeWritingLevel\":3},\"english\":{\"levelStudentOpinion\":9,\"levelMentorOpinion\":8},\"resume\":{\"verdict\":\"noButGoodCandidate\",\"comment\":\"test\"}}\t\\N\n\\.\n\n\n--\n-- Data for Name: stage_interview_student; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.stage_interview_student (id, \"createdDate\", \"updatedDate\", \"studentId\", \"courseId\") FROM stdin;\n1091\t2020-04-07 21:16:20.362338\t2020-04-07 21:16:20.362338\t14329\t13\n\\.\n\n\n--\n-- Data for Name: student; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.student (id, \"createdDate\", \"updatedDate\", \"isExpelled\", \"expellingReason\", \"courseCompleted\", \"isTopPerformer\", \"preferedMentorGithubId\", \"readyFullTime\", \"courseId\", \"userId\", \"mentorId\", \"cvUrl\", \"hiredById\", \"hiredByName\", \"isFailed\", \"totalScore\", \"startDate\", \"endDate\", repository, \"totalScoreChangeDate\", \"repositoryLastActivityDate\", rank, \"crossCheckScore\", \"unassigningComment\", mentoring) FROM stdin;\n14327\t2020-04-06 15:15:02.77565\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t11563\t1266\t\\N\t\\N\t\\N\tf\t0\t2020-04-06 15:15:02.757+00\t\\N\t\\N\t\\N\t\\N\t2\t0\t\\N\tt\n14331\t2020-04-06 15:33:59.694437\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t2098\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t3\t0\t\\N\tt\n14332\t2020-04-06 15:34:04.8008\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t2103\t1267\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t4\t0\t\\N\tt\n14333\t2020-04-06 15:34:09.064514\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t2115\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t5\t0\t\\N\tt\n14335\t2020-04-06 15:34:19.221853\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t2480\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t6\t0\t\\N\tt\n14334\t2020-04-06 15:34:17.983101\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t2277\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t7\t0\t\\N\tt\n14336\t2020-04-06 15:39:07.779618\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t2549\t1266\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t8\t0\t\\N\tt\n14330\t2020-04-06 15:33:53.058912\t2021-07-28 21:28:00.086033\tf\t\\N\tf\tf\t\\N\t\\N\t13\t2089\t1266\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t9\t0\t\\N\tt\n14328\t2020-04-06 15:30:27.104695\t2021-07-28 21:28:00.086033\tt\ttest\tf\tf\t\\N\t\\N\t13\t677\t1268\t\\N\t\\N\t\\N\tf\t0\t2020-04-06 15:30:27.091+00\t2020-04-07 13:34:01.397+00\t\\N\t\\N\t\\N\t10\t0\t\\N\tt\n14340\t2020-04-06 15:33:53.058912\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t2089\t1266\t\\N\t\\N\t\\N\tf\t1585\t1970-01-01 00:00:00+00\t\\N\t\\N\t2021-07-28 21:28:00.124+00\t\\N\t1\t0\t\\N\tt\n14337\t2020-04-06 15:15:02.77565\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t11563\t1266\t\\N\t\\N\t\\N\tf\t620\t2020-04-06 15:15:02.757+00\t\\N\t\\N\t2021-07-28 21:28:00.123+00\t\\N\t2\t0\t\\N\tt\n14346\t2020-04-06 15:39:07.779618\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t2549\t1266\t\\N\t\\N\t\\N\tf\t560\t1970-01-01 00:00:00+00\t\\N\t\\N\t2021-07-28 21:28:00.124+00\t\\N\t3\t0\t\\N\tt\n14341\t2020-04-06 15:33:59.694437\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t2098\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t4\t0\t\\N\tt\n14342\t2020-04-06 15:34:04.8008\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t2103\t1267\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t5\t0\t\\N\tt\n14343\t2020-04-06 15:34:09.064514\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t2115\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t6\t0\t\\N\tt\n14339\t2020-04-06 15:31:44.421341\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t1090\t\\N\t\\N\t\\N\t\\N\tf\t0\t2020-04-06 15:31:44.388+00\t\\N\t\\N\t\\N\t\\N\t8\t0\t\\N\tt\n14344\t2020-04-06 15:34:17.983101\t2021-07-28 21:28:00.146524\tf\t\\N\tf\tf\t\\N\t\\N\t23\t2277\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t9\t0\t\\N\tt\n14338\t2020-04-06 15:30:27.104695\t2023-02-02 15:37:56.504153\tf\t\tf\tf\t\\N\t\\N\t23\t677\t1268\t\\N\t\\N\t\\N\tf\t0\t2020-04-06 15:30:27.091+00\t\\N\t\\N\t\\N\t\\N\t10\t0\t\\N\tt\n14345\t2020-04-06 15:34:19.221853\t2023-02-02 15:38:36.638022\tf\t\tf\tf\t\\N\t\\N\t23\t2480\t\\N\t\\N\t\\N\t\\N\tf\t0\t1970-01-01 00:00:00+00\t\\N\t\\N\t\\N\t\\N\t7\t0\t\\N\tt\n14329\t2020-04-06 15:31:44.421341\t2023-02-03 07:48:22.122732\tf\t\tf\tf\t\\N\t\\N\t13\t1090\t\\N\t\\N\t\\N\t\\N\tf\t32\t2020-04-06 15:31:44.388+00\t\\N\t\\N\t2021-07-28 21:28:00.058+00\t\\N\t1\t0\t\\N\tt\n\\.\n\n\n--\n-- Data for Name: student_feedback; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.student_feedback (id, created_date, updated_date, deleted_date, student_id, mentor_id, content, recommendation, english_level, author_id) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: student_team_distribution_team_distribution; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.student_team_distribution_team_distribution (\"studentId\", \"teamDistributionId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: student_teams_team; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.student_teams_team (\"studentId\", \"teamId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: task; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task (id, \"createdDate\", \"updatedDate\", name, \"descriptionUrl\", description, verification, \"githubPrRequired\", \"useJury\", \"allowStudentArtefacts\", \"githubRepoName\", \"sourceGithubRepoUrl\", type, tags, attributes, skills, \"disciplineId\", \"criteriaId\", \"deletedDate\") FROM stdin;\n441\t2019-10-16 15:05:31.176646\t2019-10-16 15:05:31.176646\tTechnical screening 2\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/technical-screening.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n413\t2019-08-29 10:57:34.732592\t2019-11-11 18:19:01.013044\tST JS Test\thttp://learn.javascript.ru/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\t\t{}\t\t\\N\t\\N\t\\N\n448\t2019-11-20 10:39:10.274681\t2019-11-20 10:39:10.274681\tFancy Weather\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/fancy-weather.md\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n445\t2019-11-13 07:46:32.194939\t2019-12-03 14:41:40.672641\tCode Jam \"Palette\"\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-2/codejam-palette/codejam-palette_en.md\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\tcodejam\tcodejam,canvas,dom\t{}\t\t\\N\t\\N\t\\N\n451\t2019-12-11 17:17:25.352869\t2019-12-11 17:17:25.352869\tAsync-extra\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tst\t{}\t\t\\N\t\\N\t\\N\n454\t2019-12-16 10:37:14.018926\t2019-12-16 10:37:14.018926\tTypical Arrays Problems\thttps://github.com/Shastel/typical-arrays-problems\t\\N\tauto\tf\tf\tf\ttypical-arrays-problems\thttps://github.com/Shastel/typical-arrays-problems\tjstask\tepam\t{}\t\t\\N\t\\N\t\\N\n457\t2019-12-16 10:38:57.10798\t2019-12-16 10:38:57.10798\tHuman Readable Number\thttps://github.com/Shastel/human-readable-number\t\\N\tauto\tf\tf\tf\thuman-readable-number\thttps://github.com/Shastel/human-readable-number\tjstask\tepam\t{}\t\t\\N\t\\N\t\\N\n460\t2019-12-20 08:53:52.921362\t2019-12-20 08:53:52.921362\tre:bind\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tst\t{}\t\t\\N\t\\N\t\\N\n417\t2019-09-17 07:09:54.066212\t2020-02-02 09:07:48.746248\tHTML/CSS Self Education\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-1/HTML-CSS-self-ru.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\thtmlcssacademy\tstage1\t{}\t\t\\N\t\\N\t\\N\n462\t2020-02-07 08:05:04.999374\t2020-02-07 08:05:04.999374\tSongbird\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/songbird.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n466\t2020-02-11 08:49:28.691804\t2020-02-11 08:49:28.691804\tios Test\thttps://test.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n468\t2020-02-17 08:27:20.358749\t2020-02-17 08:28:49.855244\tAngular. Intro\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/angular/intro.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n471\t2020-02-17 09:19:10.05115\t2020-02-17 09:19:10.05115\tAngular. RxJS & Observables. HTTP\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/angular/rxjs-observables-http.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n475\t2020-02-19 15:14:40.900394\t2020-02-19 15:22:20.919668\tTypical Arrays Problems\thttps://github.com/rolling-scopes-school/typical-arrays-problems/blob/master/README.md\t\\N\tauto\tf\tf\tf\ttypical-arrays-problems\thttps://github.com/rolling-scopes-school/typical-arrays-problems\tjstask\tstage1,algorithms\t{}\t\t\\N\t\\N\t\\N\n473\t2020-02-19 15:13:21.398993\t2020-02-19 15:22:34.391055\tHuman Readable Number\thttps://github.com/rolling-scopes-school/human-readable-number/blob/master/README.md\t\\N\tauto\tf\tf\tf\thuman-readable-number\thttps://github.com/rolling-scopes-school/human-readable-number\tjstask\tstage1,algorithms\t{}\t\t\\N\t\\N\t\\N\n478\t2020-02-26 06:55:13.604626\t2020-02-26 06:55:24.65169\tFAKE TEST IOS\thttp://example.com\t\\N\tauto\tf\tf\tf\ttest-solution\thttps://github.com/apalchys/test-solution\tobjctask\tfake\t{}\t\t\\N\t\\N\t\\N\n480\t2020-03-02 06:32:37.242366\t2020-03-02 06:32:49.611475\tReact Culture Portal\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codejam-culture-portal.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tportal,react\t{}\t\t\\N\t\\N\t\\N\n477\t2020-02-25 23:21:08.16798\t2020-03-12 17:34:18.306073\tFAKE TEST KOTLIN\thttp://example.com\t\\N\tauto\tf\tf\tf\tnadzeya\thttps://github.com/ziginsider/rs_task1\tkotlintask\tfake\t{}\t\t\\N\t\\N\t\\N\n483\t2020-03-15 15:29:20.69008\t2020-03-15 15:29:20.69008\tAngular test\thttps://github.com/rolling-scopes-school/tasks/tree/master/tasks\t\\N\tauto\tf\tf\tf\t\\N\t\\N\ttest\tangular,Angular\t{}\t\t\\N\t\\N\t\\N\n485\t2020-03-16 12:49:18.137702\t2020-03-16 12:49:18.137702\tSingolo. DOM & Responsive \thttps://github.com/rolling-scopes-school/tasks/tree/master/tasks/markups/level-2/singolo\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1\t{}\t\t\\N\t\\N\t\\N\n487\t2020-03-19 15:00:38.575898\t2020-03-19 15:04:07.496857\t[iOS] Quiz1\thttps://docs.google.com/forms/d/e/1FAIpQLSf4NwQRa2WbcjlcsDJI0kv62qJx0F0ltgapz0WczFrdBBSXug/viewform\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n416\t2019-09-10 08:14:33.753801\t2019-09-10 08:14:33.753801\tUZ Custom lodash tests\thttps://github.com/rolling-scopes-school/RS-Uzbekistan/wiki/10.-Custom-lodash-tests\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n95\t2019-04-26 14:55:46.480357\t2019-08-14 10:45:30.750037\tCJ \"CSS QD\"\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n442\t2019-10-27 12:08:46.726741\t2019-10-28 06:59:34.373416\tCode Jam \"Canvas\"\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-2/codejam-canvas/codejam-canvas.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\tstage2 ,canvas,codejam\t{}\t\t\\N\t\\N\t\\N\n443\t2019-10-28 07:46:31.518101\t2019-11-01 14:30:13.900706\tRepair Design Project. Difficulty Level 3\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/markups/level-3/repair-design-project/repair-design-project-en.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\tstage1\t{}\t\t\\N\t\\N\t\\N\n486\t2020-03-18 12:10:57.111813\t2020-03-20 09:12:43.838469\tAlgorithms Part 1\thttps://github.com/rolling-scopes-school/rs.android-stage1-task1\t\\N\tauto\tf\tf\tf\trs.android-stage1-task1\thttps://github.com/rolling-scopes-school/rs.android-stage1-task1\tkotlintask\tAndroid,Kotlin\t{}\t\t\\N\t\\N\t\\N\n446\t2019-11-13 08:16:07.288782\t2019-11-24 15:47:56.206248\tCode Jam \"Image API\"\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-2/codejam-image-api/codejam-image-api_ru.md\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\tcodejam\tcodejam,stage2 \t{}\t\t\\N\t\\N\t\\N\n449\t2019-11-27 15:58:51.613495\t2019-11-27 15:58:51.613495\tST Checkpoint 1\thttps://app.rs.school/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tinterview\t\t{}\t\t\\N\t\\N\t\\N\n402\t2019-08-14 10:35:12.012641\t2019-12-03 14:49:35.649926\tCode Jam \"Culture Portal\"\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/codejam-culture-portal.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodejam\tcodejam\t{}\t\t\\N\t\\N\t\\N\n452\t2019-12-16 09:39:38.046401\t2019-12-16 09:39:38.046401\tFancy-weather Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/fancy-weather.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n455\t2019-12-16 10:37:47.551919\t2019-12-16 10:37:47.551919\tReverse Int\thttps://github.com/Shastel/reverse-int\t\\N\tauto\tf\tf\tf\treverse-int\thttps://github.com/Shastel/reverse-int\tjstask\tepam\t{}\t\t\\N\t\\N\t\\N\n458\t2019-12-16 15:59:10.804471\t2019-12-16 15:59:10.804471\tST React App\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/6.-Things-APP\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tst\t{}\t\t\\N\t\\N\t\\N\n461\t2020-01-10 20:07:46.237318\t2020-01-10 20:07:46.237318\tAngular Workshop\thttps://angular.io/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n463\t2020-02-07 08:05:15.718038\t2020-02-07 08:05:15.718038\tSongbird\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/songbird.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n464\t2020-02-07 08:05:57.730605\t2020-02-07 08:05:57.730605\tCalculator\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/calculator.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n467\t2020-02-15 14:41:17.390262\t2020-02-16 08:44:46.403205\tBasic JS\thttps://github.com/AlreadyBored/basic-js\t\\N\tauto\tf\tf\tf\tbasic-js\thttps://github.com/AlreadyBored/basic-js\tjstask\tstage1,algorithms\t{}\t\t\\N\t\\N\t\\N\n469\t2020-02-17 08:28:38.434548\t2020-02-17 08:28:54.065591\tAngular. Components. Directives & Pipes\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/angular/components-directives-pipes.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n474\t2020-02-19 15:13:59.744793\t2020-02-19 15:22:27.177884\tReverse Int\thttps://github.com/rolling-scopes-school/reverse-int/blob/master/README.md\t\\N\tauto\tf\tf\tf\treverse-int\thttps://github.com/rolling-scopes-school/reverse-int\tjstask\tstage1,algorithms\t{}\t\t\\N\t\\N\t\\N\n472\t2020-02-19 15:12:35.267242\t2020-02-19 15:22:41.830318\tTowel Sort\thttps://github.com/rolling-scopes-school/towel-sort/blob/master/README.md\t\\N\tauto\tf\tf\tf\ttowel-sort\thttps://github.com/rolling-scopes-school/towel-sort\tjstask\tstage1,algorithms\t{}\t\t\\N\t\\N\t\\N\n476\t2020-02-21 10:24:38.588117\t2020-02-21 10:24:38.588117\tSingolo\thttps://github.com/rolling-scopes-school/tasks/tree/master/tasks/markups/level-2/singolo\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,html\t{}\t\t\\N\t\\N\t\\N\n479\t2020-03-02 06:25:15.661263\t2020-03-02 06:25:15.661263\tAngular Culture Portal\thttps://github.com/rolling-scopes-school/tasks/blob/angular-2020Q1/tasks/angular/culture-portal.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tangular,portal\t{}\t\t\\N\t\\N\t\\N\n481\t2020-03-02 11:56:29.196388\t2020-03-02 11:56:29.196388\tData grid\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/datagrid.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n396\t2019-08-06 09:43:51.676522\t2019-08-06 09:43:51.676522\tMatch Match Game\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q1/tasks/match-match-game.md\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n400\t2019-08-06 09:55:49.176631\t2019-08-06 09:55:49.176631\tReact Redux\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q1/tasks/react-match-match-game.md\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n86\t2019-04-26 14:55:46.436642\t2019-08-14 10:45:50.369308\tCJ \"DOM, DOM Events\"\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n398\t2019-08-06 09:52:41.754622\t2019-08-14 10:46:07.362506\tCJ \"Lodash Quick Draw\"\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n82\t2019-04-26 14:55:46.414479\t2019-04-26 14:55:46.414479\tHTML/CSS Test\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n85\t2019-04-26 14:55:46.431913\t2019-04-26 14:55:46.431913\tMarkup #1\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n88\t2019-04-26 14:55:46.446081\t2019-04-26 14:55:46.446081\tRS Activist\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n91\t2019-04-26 14:55:46.460834\t2019-04-26 14:55:46.460834\tMentor Dashboard\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n92\t2019-04-26 14:55:46.465569\t2019-04-26 14:55:46.465569\tCoreJS/Arrays Test\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n94\t2019-04-26 14:55:46.475554\t2019-04-26 14:55:46.475554\tGame\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n97\t2019-04-26 14:55:46.49026\t2019-04-26 14:55:46.49026\tDreamTeam\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n90\t2019-04-26 14:55:46.455449\t2019-04-26 14:55:46.45545\tCode Jam \"Scoreboard\"\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n117\t2019-04-30 13:51:17.676745\t2019-05-14 10:55:17.676745\tHexal\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/markup_d1_Hexal.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n221\t2019-05-17 13:01:38.633934\t2019-05-17 13:01:38.633934\thtmlCssBasics\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n224\t2019-05-17 13:01:38.650481\t2019-05-17 13:01:38.650481\tlayouts\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n222\t2019-05-17 13:01:38.639424\t2019-05-17 13:01:38.639424\tfloatExercise\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n223\t2019-05-17 13:01:38.644267\t2019-05-17 13:01:38.644267\tpositioning\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n225\t2019-05-17 13:01:38.655673\t2019-05-17 13:01:38.655673\tworkshop\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n226\t2019-05-17 13:01:38.660659\t2019-05-17 13:01:38.660659\tresponsive\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n227\t2019-05-17 13:01:38.666042\t2019-05-17 13:01:38.666042\tformsWidgets\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n228\t2019-05-17 13:01:38.671159\t2019-05-17 13:01:38.671159\tfinalTask\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n231\t2019-05-17 13:01:38.686221\t2019-05-17 13:01:38.686221\tdoublyLinkedList\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n232\t2019-05-17 13:01:38.695428\t2019-05-17 13:01:38.695428\tcustomJQuery\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n234\t2019-05-17 13:01:38.705612\t2019-05-17 13:01:38.705612\trealJquery\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n235\t2019-05-17 13:01:38.71084\t2019-05-17 13:01:38.71084\twsc\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n236\t2019-05-17 13:01:38.715941\t2019-05-17 13:01:38.715941\tnoNameOne\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n237\t2019-05-17 13:01:38.720957\t2019-05-17 13:01:38.720957\tnoNameTwo\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n238\t2019-05-17 13:02:30.13361\t2019-05-17 13:02:30.13361\tworkHonor\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n240\t2019-05-17 13:02:30.15818\t2019-05-17 13:02:30.15818\tcssQDTime\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n241\t2019-05-17 13:02:30.163081\t2019-05-17 13:02:30.163081\tuiLab\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n242\t2019-05-17 13:02:30.168177\t2019-05-17 13:02:30.168177\tflexbox\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n243\t2019-05-17 13:02:30.173271\t2019-05-17 13:02:30.173271\tadaptive\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n244\t2019-05-17 13:02:30.184497\t2019-05-17 13:02:30.184497\tcssTotal\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n245\t2019-05-17 13:02:30.190762\t2019-05-17 13:02:30.190762\tworkOnLessons\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n247\t2019-05-17 13:02:30.201713\t2019-05-17 13:02:30.201713\tfunctionMake\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n248\t2019-05-17 13:02:30.207184\t2019-05-17 13:02:30.207184\twsc\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n249\t2019-05-17 13:02:30.212126\t2019-05-17 13:02:30.212126\tgulp\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n250\t2019-05-17 13:02:30.217988\t2019-05-17 13:02:30.217988\thonoiTower\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n251\t2019-05-17 13:02:30.223044\t2019-05-17 13:02:30.223044\tanimation\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n252\t2019-05-17 13:02:30.2279\t2019-05-17 13:02:30.2279\tcustomJQuery\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n253\t2019-05-17 13:02:30.233767\t2019-05-17 13:02:30.233767\ttdd\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n89\t2019-04-26 14:55:46.450715\t2019-05-27 08:35:37.359351\tPresentation\t\\N\t\\N\tmanual\t\\N\tf\tt\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n96\t2019-04-26 14:55:46.485433\t2019-05-27 08:39:44.221825\tOffline Presentation\t\\N\t\\N\tmanual\t\\N\tt\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n351\t2019-06-05 11:51:12.229807\t2019-06-05 11:51:12.229807\tStage#2 Final Test\t\\N\t\\N\tauto\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n369\t2019-06-26 13:24:39.790098\t2019-06-26 13:24:39.790098\tyouTube\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n387\t2019-07-08 13:30:12.12725\t2019-07-08 13:30:12.12725\tPadawans\t\\N\t\\N\tauto\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n388\t2019-07-08 13:31:46.251832\t2019-07-08 13:31:46.251832\tUZ CV\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n389\t2019-07-08 13:32:18.083335\t2019-07-08 13:32:18.083335\tUZ Read me\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n390\t2019-07-10 12:56:29.975418\t2019-07-10 12:56:29.975418\tUZ Layout\thttps://github.com/rolling-scopes-school/RS-Uzbekistan/wiki/2.-Layout\tCreate web page, strictly according to:\\n\\nLambda restaurant layout\\n\\nBrowser support: Google Chrome, Mozilla Firefox, Microsoft Edge.\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n410\t2019-08-29 09:41:00.400898\t2019-08-29 10:08:08.993969\tST Chat\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/6.-Chat\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n407\t2019-08-29 09:32:17.606001\t2019-08-29 10:08:44.864627\tST Custom Lodash\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/3.-Custom-Lodash\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n230\t2019-05-17 13:01:38.681206\t2019-08-29 10:10:10.985834\tST JS Assignments\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/7.-JS-assignments\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n435\t2019-09-30 08:14:14.847165\t2019-10-15 12:40:10.75085\tHTML/CSS Test Advanced\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/html-css-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n408\t2019-08-29 09:34:32.473242\t2019-08-29 10:08:34.054101\tST Cyclic menu\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/4.-Cyclic-menu\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n405\t2019-08-29 09:16:23.185166\t2019-08-29 10:09:04.204396\tST Auto Complete\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/1.-Auto-Complete\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n411\t2019-08-29 10:11:56.69667\t2019-08-29 10:11:56.69667\tST Catalogue. P.1 React Client\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/FINAL:-Catalogue.-P.1-React-Client\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n414\t2019-08-29 10:57:50.108237\t2019-08-29 10:57:50.108237\tST JS Test 2\thttp://learn.javascript.ru/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n397\t2019-08-06 09:46:51.573349\t2019-08-06 09:46:51.573349\tCSS Recipes & Layouts\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q1/tasks/css-recipes-and-layouts.md\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n401\t2019-08-06 09:56:50.593508\t2019-08-06 09:56:50.593508\tGame Refactoring\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q1/tasks/game-refactoring.md\t\\N\tauto\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n229\t2019-05-17 13:01:38.676219\t2019-08-06 09:59:19.619433\tJS Test\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n122\t2019-04-30 14:11:11.94101\t2019-05-14 10:14:11.94101\tNeutron Mail\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/markup-d2-NeutronMail-en.md\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n87\t2019-04-26 14:55:46.441332\t2019-05-14 10:56:46.441332\tYouTube\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/youtube.md\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n404\t2019-08-29 08:12:24.073776\t2019-10-28 10:40:19.063008\tST Read me\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/0.-Readme\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n422\t2019-09-19 10:02:05.134479\t2019-11-01 14:31:29.943288\tJS: Multiply\thttps://github.com/Shastel/multiply\t\\N\tauto\tf\tf\tf\tmultiply\thttps://github.com/Shastel/multiply\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n484\t2020-03-15 23:11:23.55455\t2020-03-25 09:27:46.940288\tTechnical Screening\thttps://docs.rs.school/#/technical-screening\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tstage-interview\tinterview\t{}\t\t\\N\t\\N\t\\N\n428\t2019-09-20 09:56:26.502967\t2019-11-08 11:44:12.440623\tJS: JS-edu\thttps://github.com/davojta/js-edu\t\\N\tauto\tf\tf\tf\tjs-edu\thttps://github.com/davojta/js-edu\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n431\t2019-09-24 08:20:14.453176\t2019-11-08 11:44:50.366453\tJS: Unique \thttps://github.com/Shastel/unique\t\\N\tauto\tf\tf\tf\tunique\thttps://github.com/Shastel/unique\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n349\t2019-05-28 15:21:16.311993\t2019-11-19 09:35:38.995602\tCoreJS Interview \thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/interview-corejs.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tinterview\t\t{}\t\t\\N\t\\N\t\\N\n93\t2019-04-26 14:55:46.470595\t2019-11-19 09:53:57.574635\tWebSocket Challenge\thttps://github.com/rolling-scopes-school/lectures/blob/master/lectures/websocket-challenge.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodejam\t\t{}\t\t\\N\t\\N\t\\N\n350\t2019-06-03 06:50:19.575782\t2019-11-19 10:53:20.712051\tCodeJam \"Animation Player\"\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/piskel-animation-player.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodejam\t\t{}\t\t\\N\t\\N\t\\N\n352\t2019-06-21 07:22:11.052584\t2019-11-19 13:06:31.954741\tPiskel-clone\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/piskel-clone.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n129\t2019-05-13 11:45:12.64168\t2020-03-09 11:46:32.445946\tCodewars stage 2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars:stage2\tcodewars\t{}\t\t\\N\t\\N\t\\N\n220\t2019-05-17 13:01:38.627128\t2019-05-17 13:01:38.627128\tworkHonor\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n391\t2019-07-15 12:39:31.48174\t2019-07-15 12:39:31.48174\tUZ Autocomplete\thttps://github.com/rolling-scopes-school/RS-Uzbekistan/wiki/3.-Autocomplete\tThe task is to implement a custom createAutocomplete function\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n392\t2019-07-17 14:41:10.098861\t2019-07-17 14:41:10.098861\tUZ Codewars\thttps://github.com/rolling-scopes-school/RS-Uzbekistan/wiki/4.-Codewars\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n394\t2019-07-30 09:47:10.177586\t2019-07-30 09:47:10.177586\tUZ Javascript Classes & Inheritance\thttps://github.com/rolling-scopes-school/RS-Uzbekistan/wiki/5.-Javascript-Classes-&-Inheritance\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n395\t2019-07-31 12:59:19.767726\t2019-07-31 12:59:19.767726\tUZ Custom Lodash\thttps://github.com/rolling-scopes-school/RS-Uzbekistan/wiki/8.-Custom-Lodash\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n246\t2019-05-17 13:02:30.196693\t2019-08-06 09:59:24.394646\tJS Test\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n233\t2019-05-17 13:01:38.700498\t2019-08-06 11:08:43.462233\tCSS QD\t\\N\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n403\t2019-08-22 09:35:28.567592\t2019-08-22 09:35:28.567592\tUZ Cyclic menu\thttps://github.com/rolling-scopes-school/RS-Uzbekistan/wiki/9.-Cyclic-menu\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n406\t2019-08-29 09:21:54.045655\t2019-08-29 10:08:53.337095\tST Javascript Classes & Inheritance\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/2.-Javascript-Classes-&-Inheritance\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n412\t2019-08-29 10:12:27.740479\t2019-08-29 10:12:27.740479\tST Catalogue. P.2 Angular Admin Client\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/FINAL:-Catalogue.-P.2-Angular-Admin-Client\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n415\t2019-08-29 11:07:41.484385\t2019-08-29 11:07:41.484385\tST Bonus\thttps://github.com/rolling-scopes-school/docs/blob/master/rs-activist.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\t\t{}\t\t\\N\t\\N\t\\N\n434\t2019-09-30 08:09:29.61975\t2019-10-08 14:24:55.849506\tRS School Test\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rs-school-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n436\t2019-09-30 08:14:56.284783\t2019-10-08 07:05:43.425884\tGit Test #2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/git-test.md\\t\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\tstage1\t{}\t\t\\N\t\\N\t\\N\n433\t2019-09-30 08:05:43.034506\t2019-10-08 14:25:09.658362\tHTML/CSS Test\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/html-css-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n465\t2020-02-09 18:17:26.12848\t2020-02-09 18:17:26.12848\tCodewars stage 1\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars-stage-1.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars:stage1\tcodewars\t{}\t\t\\N\t\\N\t\\N\n432\t2019-09-30 08:03:38.411822\t2019-10-28 06:59:48.722431\tGit Test\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/git-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n418\t2019-09-17 07:20:20.07102\t2019-10-28 07:40:32.105112\tTheyalow. Difficulty Level 1\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/markups/level%201/theyalow/theyalow-en.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\t\\N\tstage1\t{}\t\t\\N\t\\N\t\\N\n439\t2019-10-13 13:50:38.385396\t2019-11-01 14:29:45.50486\tPriority Queue\thttps://github.com/rolling-scopes-school/priority-queue\t\\N\tauto\tf\tf\tf\tpriority-queue\thttps://github.com/rolling-scopes-school/priority-queue\tjstask\tstage1,algorithms\t{}\t\t\\N\t\\N\t\\N\n424\t2019-09-20 09:40:16.65468\t2019-11-01 14:31:12.362038\tJS: Expression Calculator\thttps://github.com/romacher/expression-calculator\t\\N\tauto\tf\tf\tf\texpression-calculator\thttps://github.com/romacher/expression-calculator\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n421\t2019-09-17 13:40:31.235798\t2019-11-01 14:31:18.390464\tJS: Brackets\thttps://github.com/Shastel/brackets\t\\N\tauto\tf\tf\tf\tbrackets\thttps://github.com/Shastel/brackets\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n423\t2019-09-19 10:02:37.126233\t2019-11-01 14:31:37.02801\tJS: Zeros\thttps://github.com/Shastel/zeros\t\\N\tauto\tf\tf\tf\tzeros\thttps://github.com/Shastel/zeros\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n393\t2019-07-26 13:14:49.106312\t2019-11-07 09:21:44.562843\tST JS assignments\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/3.-JS-assignments\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tst\t{}\t\t\\N\t\\N\t\\N\n425\t2019-09-20 09:42:22.766447\t2019-11-08 11:43:53.046921\tJS: Guessing-game\thttps://github.com/rolling-scopes-school/guessing-game\t\\N\tauto\tf\tf\tf\tguessing-game\thttps://github.com/rolling-scopes-school/guessing-game\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n426\t2019-09-20 09:54:01.865495\t2019-11-08 11:44:00.705846\tJS: Morse-decoder\thttps://github.com/romacher/morse-decoder\t\\N\tauto\tf\tf\tf\tmorse-decoder\thttps://github.com/romacher/morse-decoder\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n427\t2019-09-20 09:54:43.876086\t2019-11-08 11:44:06.756286\tJS: Finite-state-machine\thttps://github.com/rolling-scopes-school/finite-state-machine\t\\N\tauto\tf\tf\tf\tfinite-state-machine\thttps://github.com/rolling-scopes-school/finite-state-machine\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n429\t2019-09-22 09:55:22.942777\t2019-11-08 11:44:20.763439\tJS: Tic Tac Toe\thttps://github.com/rolling-scopes-school/tic-tac-toe\t\\N\tauto\tf\tf\tf\ttic-tac-toe\thttps://github.com/rolling-scopes-school/tic-tac-toe\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n430\t2019-09-22 09:56:18.079947\t2019-11-08 11:45:10.648593\tJS: Doubly Linked List\thttps://github.com/rolling-scopes-school/doubly-linked-list\t\\N\tauto\tf\tf\tf\tdoubly-linked-list\thttps://github.com/rolling-scopes-school/doubly-linked-list\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n409\t2019-08-29 09:37:01.324698\t2019-11-11 18:15:52.011347\tST Autocomplete UI\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/4.-Autocomplete-UI\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n447\t2019-11-18 07:47:39.508556\t2019-11-18 07:47:39.508556\ttest-task\thttps://github.com/mikhama/test-task\t\\N\tauto\tf\tf\tf\ttest-task\thttps://github.com/mikhama/test-task\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n399\t2019-08-06 09:54:06.658655\t2019-12-03 14:49:49.549586\tCode Jam \"Hacktrain\"\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q1/tasks/codejam-train.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodejam\t\t{}\t\t\\N\t\\N\t\\N\n440\t2019-10-15 07:50:32.749775\t2019-11-19 09:34:34.605432\tTechnical screening\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/technical-screening.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tinterview\tstage2 \t{}\t\t\\N\t\\N\t\\N\n450\t2019-12-03 14:52:19.396399\t2019-12-03 14:52:19.396399\tPortfolio\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-1/portfolio/portfolio-ru.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage2 ,html\t{}\t\t\\N\t\\N\t\\N\n83\t2019-04-26 14:55:46.421933\t2019-11-30 18:36:50.662322\tCoreJS\thttps://github.com/mikhama/core-js-101\t\\N\tauto\tt\tf\tf\tcore-js-101\thttps://github.com/mikhama/core-js-101\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n128\t2019-05-02 09:41:43.371377\t2019-12-03 14:42:15.453094\tCode Jam \"Palette\"\thttps://github.com/rolling-scopes-school/tasks/blob/2018-Q3/tasks/codejam-pallete.md\t\\N\tmanual\tt\tf\tf\t\\N\t\\N\tcodejam\tdeprecated\t{}\t\t\\N\t\\N\t\\N\n444\t2019-11-04 08:12:31.634176\t2020-03-31 10:17:18.546617\tVirtual Keyboard\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codejam-virtual-keyboard.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,js\t{}\t\t\\N\t\\N\t\\N\n453\t2019-12-16 10:34:47.548986\t2019-12-16 10:34:47.548986\tTemperature Converter\thttps://github.com/Shastel/temperature-converter\t\\N\tauto\tf\tf\tf\ttemperature-converter\thttps://github.com/Shastel/temperature-converter\tjstask\tepam\t{}\t\t\\N\t\\N\t\\N\n456\t2019-12-16 10:38:26.769964\t2019-12-16 10:38:26.769964\tTowel Sort\thttps://github.com/Shastel/towel-sort\t\\N\tauto\tf\tf\tf\ttowel-sort\thttps://github.com/Shastel/towel-sort\tjstask\tepam\t{}\t\t\\N\t\\N\t\\N\n459\t2019-12-18 14:22:47.842869\t2019-12-18 14:22:47.842869\tST TDD\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tst\t{}\t\t\\N\t\\N\t\\N\n84\t2019-04-26 14:55:46.426978\t2020-02-10 18:45:57.803066\tHTML, CSS & Git Basics\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codejam-cv.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcv:html\tstage1\t{}\t\t\\N\t\\N\t\\N\n437\t2019-10-06 11:20:27.617946\t2020-02-10 06:18:24.928919\tMarkdown & Git\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/git-markdown.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcv:markdown\tstage1\t{}\t\t\\N\t\\N\t\\N\n470\t2020-02-17 08:29:28.43587\t2020-02-17 08:29:28.43587\tAngular. Modules & Services. Routing\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/angular/modules-services-routing.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n438\t2019-10-13 13:34:49.201156\t2020-03-23 10:57:07.262729\tSudoku\thttps://github.com/rolling-scopes-school/sudoku\t\\N\tauto\tf\tf\tf\tsudoku\thttps://github.com/rolling-scopes-school/sudoku\tjstask\tstage1,algorithms\t{}\t\t\\N\t\\N\t\\N\n488\t2020-03-19 16:22:02.703098\t2020-03-24 16:19:06.071144\trs.ios.task2\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task2/blob/master/readme.md\t\\N\tauto\tf\tf\tf\trs.ios-stage1-task2\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task2/\tobjctask\tstage1\t{\"targets\":{\"project\":{\"folder\":\"RSSchool_T2\",\"xcodeproj\":\"RSSchool_T2.xcodeproj\"},\"tests\":{\"folder\":\"RSSchool_T2Tests\",\"classes\":[\"AbbreviationTests.m\",\"BlocksTest.m\",\"DatesTest.m\",\"FibonacciNumbersTests.m\",\"StringTransform.m\",\"TimeInWordsTests.m\"]}},\"folder\":\"RSSchool_T2\",\"details\":\"\",\"descriptions\":\"\"}\t\t\\N\t\\N\t\\N\n482\t2020-03-10 20:39:15.488061\t2020-03-24 16:20:39.287898\trs.ios.task1\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task1/\t\\N\tauto\tf\tf\tf\trs.ios-stage1-task1\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task1/\tobjctask\tstage1\t{\"targets\":{\"project\":{\"folder\":\"RSSchool_T1\",\"xcodeproj\":\"RSSchool_T1.xcodeproj\"},\"tests\":{\"folder\":\"RSSchool_T1Tests\",\"classes\":[\"BillCounterTests.m\",\"HighestPalindromeTests.m\",\"MiniMaxSumTests.m\",\"StringParseTests.m\",\"T1ArrayTests.m\"]}},\"folder\":\"RSSchool_T1\",\"details\":\"\",\"descriptions\":\"\"}\t\t\\N\t\\N\t\\N\n489\t2020-03-26 10:35:21.765085\t2020-03-26 10:35:21.765085\tCaesar cipher CLI tool\thttps://github.com/rolling-scopes-school/nodejs-course-template/blob/master/TASKS.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs\t{}\t\t\\N\t\\N\t\\N\n490\t2020-03-26 14:29:07.41166\t2020-03-26 14:29:07.41166\tHTML/Css(basic)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/css-recipes.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tPoland\t{}\t\t\\N\t\\N\t\\N\n492\t2020-03-31 09:33:53.140629\t2020-03-31 09:33:53.140629\tExpress REST service\thttps://github.com/rolling-scopes-school/nodejs-course-template/blob/master/TASKS.md#task-2-express-rest-service\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs\t{}\t\t\\N\t\\N\t\\N\n493\t2020-03-31 10:20:39.859981\t2020-03-31 10:20:39.859981\tVirtual Keyboard Cross-Check\thttps://rolling-scopes-school.github.io/checklist/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,js,cross-check\t{}\t\t\\N\t\\N\t\\N\n494\t2020-03-31 10:23:52.389221\t2020-03-31 10:23:52.389221\tGem Puzzle Cross-check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codejam-the-gem-puzzle.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,cross-check,js\t{}\t\t\\N\t\\N\t\\N\n495\t2020-04-01 08:43:01.126352\t2020-04-01 08:43:01.126352\t[Android] Quiz 1\thttps://docs.google.com/forms/d/e/1FAIpQLSdFHiOBHHDZpwztLq3rGYf7EzEQPw56I0HeYlqfg8BpB6leYg/viewform?usp=sf_link\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\t\t{}\t\t\\N\t\\N\t\\N\n491\t2020-03-30 09:57:08.558596\t2020-04-01 20:44:38.183195\trs.ios.task3.test\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task3/blob/master/readme.md\t\\N\tauto\tf\tf\tf\trs.ios-stage1-task3\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task3\tobjctask\tstage1\t{\"targets\":{\"project\":{\"folder\":\"RSSchool_T3\",\"xcodeproj\":\"RSSchool_T3.xcodeproj\"},\"tests\":{\"folder\":\"RSSchool_T3Tests\",\"classes\":[\"ArrayPrintTests.m\",\"FullBinaryTreesTests.m\"]},\"uiTests\":{\"folder\":\"RSSchool_T3UITests\",\"classes\":[\"DateMachineTests.m\"]}},\"testReplacement\":{\"link\":\"git@github.com:rolling-scopes-school/rs.ios-stage1-private-tests.git\",\"folder\":\"stage1-task3\",\"replacement\":[{\"folder\":\"RSSchool_T3Tests\",\"test\":\"ArrayPrintTests.m\"},{\"folder\":\"RSSchool_T3UITests\",\"test\":\"DateMachineTests.m\"}],\"verify\":[{\"folder\":\"RSSchool_T3Tests\",\"test\":\"FullBinaryTreesTests.m\"}]},\"folder\":\"RSSchool_T3\",\"details\":\"Task3\",\"descriptions\":\"Description task3\"}\t\t\\N\t\\N\t\\N\n496\t2020-04-02 17:01:12.759119\t2020-04-02 17:01:12.759119\tLayout(Restaurant)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/markup-1.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tPoland\t{}\t\t\\N\t\\N\t\\N\n497\t2020-04-02 18:49:24.244235\t2020-04-03 13:05:37.170103\trs.ios.task3\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task3.1/blob/master/README.md\t\\N\tauto\tf\tf\tf\trs.ios-stage1-task3.1\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task3.1\tobjctask\tstage1\t{\"targets\":{\"project\":{\"folder\":\"RSSchool_T3\",\"xcodeproj\":\"RSSchool_T3.xcodeproj\"},\"tests\":{\"folder\":\"RSSchool_T3Tests\",\"classes\":[\"T3_PolynomialTests.m\",\"T3_CombinatorTests.m\"]},\"uiTests\":{\"folder\":\"RSSchool_T3UITests\",\"classes\":[\"RS_Task3_UICheckerUITests.m\"]}},\"folder\":\"RSSchool_T3\",\"details\":\"Task3\",\"descriptions\":\"Description task3\"}\t\t\\N\t\\N\t\\N\n500\t2020-04-09 10:03:10.874771\t2020-04-09 10:03:10.874771\tEnglish for kids\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2\t{}\t\t\\N\t\\N\t\\N\n501\t2020-04-09 16:00:08.930182\t2021-06-22 09:18:19.384375\t[iOS] Quiz2\thttps://docs.google.com/forms/d/e/1FAIpQLSdLvcnvAofsQ1ETqDnwSjH3U2WQJgVvlG8pxVPV_ZfhBWDV9w/closedform\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n502\t2020-04-09 17:57:52.400972\t2020-04-09 17:57:52.400972\trs.ios.task4\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task4/blob/master/README.md\t\\N\tauto\tf\tf\tf\trs.ios-stage1-task4\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task4\tobjctask\tstage1\t{}\t\t\\N\t\\N\t\\N\n503\t2020-04-10 18:12:45.707666\t2021-06-06 20:26:13.523668\tLogging & Error Handling\thttps://github.com/rolling-scopes-school/basic-nodejs-2021Q2#task-5-logging--error-handling\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs\t{}\t\t\\N\t\\N\t\\N\n504\t2020-04-14 05:44:38.302281\t2020-04-14 05:44:38.302281\tDatabase MongoDB\thttps://github.com/rolling-scopes-school/nodejs-course-template/blob/master/TASKS.md#task-4-database-mongodb\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs\t{}\t\t\\N\t\\N\t\\N\n505\t2020-04-20 17:36:43.155586\t2021-06-27 20:09:25.924071\tAuthentication and JWT\thttps://github.com/rolling-scopes-school/basic-nodejs-2021Q2#task-8-authentification--jwt\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs\t{}\t\t\\N\t\\N\t\\N\n506\t2020-04-20 19:44:07.04595\t2021-06-25 16:57:29.2666\t[Android] Quiz 2\thttps://forms.gle/KLLFbKsKneosrwpV9\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n507\t2020-04-24 09:12:59.277372\t2020-06-02 11:36:07.441843\tSpeakIt\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/speakit.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,cross-check\t{}\t\t\\N\t\\N\t\\N\n508\t2020-04-27 06:51:46.900545\t2020-04-27 06:51:46.900545\tMovieSearch\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/movie-search.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2\t{}\t\t\\N\t\\N\t\\N\n509\t2020-04-27 06:52:41.255486\t2020-04-27 06:52:41.255486\tMovieSearch: Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/movie-search.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,cross-check\t{}\t\t\\N\t\\N\t\\N\n510\t2020-04-29 06:04:23.576262\t2021-06-02 06:56:53.49812\tJavascript Classes & Inheritance\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/javascript-classes-inheritance.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,Poland,rs-lt\t{}\t\t\\N\t\\N\t\\N\n511\t2020-04-30 16:13:15.587124\t2020-04-30 16:13:15.587124\trs.ios.task5\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task5/blob/master/README.md\t\\N\tauto\tf\tf\tf\trs.ios-stage1-task5\thttps://github.com/rolling-scopes-school/rs.ios-stage1-task5\tobjctask\tstage1\t{}\t\t\\N\t\\N\t\\N\n512\t2020-05-01 14:40:17.99012\t2021-07-02 09:12:24.068724\tAlgorithms Task 3\thttps://github.com/rolling-scopes-school/rs.android-2021-stage1-task3\t\\N\tauto\tf\tf\tf\trs.android-2021-stage1-task3\thttps://github.com/rolling-scopes-school/rs.android-2021-stage1-task3\tkotlintask\tstage1\t{}\t\t\\N\t\\N\t\\N\n513\t2020-05-03 19:35:27.599732\t2020-05-03 19:35:27.599732\tICanCodeJS\thttps://github.com/codenjoyme\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodejam\tstage2 ,codejam\t{}\t\t\\N\t\\N\t\\N\n514\t2020-05-05 17:07:38.151867\t2020-05-05 17:07:38.151867\tJS-assignments\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/js-assignments.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tPoland\t{}\t\t\\N\t\\N\t\\N\n515\t2020-05-11 14:15:10.391901\t2020-05-11 14:15:10.391901\t[iOS] Quiz 3\thttps://docs.google.com/forms/d/e/1FAIpQLSeb_To1WpYUWG_kfocuK5WfLLhL4MfXUn6AU0OVSEPt3ztXhw/viewform\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tobjctask\tstage1\t{}\t\t\\N\t\\N\t\\N\n516\t2020-05-13 13:39:03.279745\t2021-07-16 17:02:48.091094\t[Android] Quiz 3 Final\thttps://forms.gle/TTcLK8kLEWveR7BF9\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage1\t{}\t\t\\N\t\\N\t\\N\n517\t2020-05-14 18:49:07.427589\t2020-05-14 18:49:07.427589\tCyclic menu\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/cyclic-menu.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tPoland\t{}\t\t\\N\t\\N\t\\N\n518\t2020-05-19 12:57:16.890419\t2020-05-19 12:57:16.890419\tVirtual keyboard\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/virtual-keyboard/virtual-keyboard-en.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tPoland\t{}\t\t\\N\t\\N\t\\N\n519\t2020-05-28 20:05:20.202628\t2020-05-28 20:05:20.202628\tFancy-weather(en)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/fancy-weather(en).md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tPoland\t{}\t\t\\N\t\\N\t\\N\n520\t2020-06-02 11:28:16.858003\t2020-06-02 11:29:43.695887\tEnglish puzzle\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-puzzle.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,stage2\t{}\t\t\\N\t\\N\t\\N\n521\t2020-06-02 11:29:37.951145\t2020-06-02 11:29:52.45171\tEnglish puzzle: Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-puzzle.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,cross-check,stage2\t{}\t\t\\N\t\\N\t\\N\n522\t2020-06-07 17:14:36.355963\t2020-06-07 17:14:36.355963\tCV\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codejam-cv.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcv:html\tGeorgia\t{}\t\t\\N\t\\N\t\\N\n523\t2020-06-08 19:30:29.31376\t2020-06-08 19:30:29.31376\trs.ios.task6\thttps://github.com/rolling-scopes-school/rs.ios-stage2-task6/blob/master/README.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tobjctask\tstage2\t{}\t\t\\N\t\\N\t\\N\n524\t2020-06-08 19:31:03.111251\t2020-06-08 19:31:03.111251\trs.ios.task7\thttps://github.com/rolling-scopes-school/rs.ios-stage2-task7/blob/master/README.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tobjctask\tstage2\t{}\t\t\\N\t\\N\t\\N\n525\t2020-06-08 19:31:30.353779\t2020-06-08 19:31:30.353779\trs.ios.task8\thttps://github.com/rolling-scopes-school/rs.ios-stage2-task8/blob/master/README.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tobjctask\tstage2\t{}\t\t\\N\t\\N\t\\N\n526\t2020-06-08 19:55:04.118004\t2020-06-08 19:55:04.118004\t[iOS] Quiz 4\thttps://docs.google.com/forms/d/e/1FAIpQLSdc0z7shPfpCbcOlCyYggHqJqd01fiDYZCaif_kk7Azyt3ZxQ/viewform\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage2\t{}\t\t\\N\t\\N\t\\N\n527\t2020-06-08 19:56:23.355047\t2020-06-08 19:56:23.355047\t[iOS] Quiz 5\thttps://docs.google.com/forms/d/e/1FAIpQLScIUpMl0RSKJmve_4AID8owWgSUzAGWVZxPchfpvTRo-e1TZQ/viewform\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage2\t{}\t\t\\N\t\\N\t\\N\n528\t2020-06-09 12:05:43.593182\t2021-07-07 06:11:53.697552\tCustom lodash(unit tests)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/custom-lodash(unit%20%20tests).md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tPoland,rs-lt\t{}\t\t\\N\t\\N\t\\N\n529\t2020-06-14 18:51:48.51346\t2020-06-14 18:51:48.51346\tRS Lang. Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/rslang.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,cross-check,js\t{}\t\t\\N\t\\N\t\\N\n530\t2020-06-14 18:52:12.642677\t2020-06-14 18:52:12.642677\tRS Lang. Presentation\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/rslang.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,js\t{}\t\t\\N\t\\N\t\\N\n531\t2020-06-15 18:55:01.118769\t2020-06-15 18:55:01.118769\tFinal JS Test\thttps://google.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage2\t{}\t\t\\N\t\\N\t\\N\n532\t2020-06-18 11:57:24.090653\t2020-06-18 11:57:24.090653\t[Android] Task 4 Storage\thttps://github.com/rolling-scopes-school/rs.android.task.4\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tkotlintask\tstage2\t{}\t\t\\N\t\\N\t\\N\n533\t2020-07-02 17:22:29.052038\t2020-07-02 17:22:29.052038\tChat (React)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/chat.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tPoland,react\t{}\t\t\\N\t\\N\t\\N\n534\t2020-07-17 08:55:25.910527\t2020-07-17 08:55:25.910527\t[Android] Task 5\thttps://github.com/rolling-scopes-school/Android-2020-Task-5\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tkotlintask\tstage2\t{}\t\t\\N\t\\N\t\\N\n535\t2020-07-20 07:47:20.402571\t2020-07-20 07:47:48.182376\tAngular YouTube client: Cross-Check\thttps://rolling-scopes-school.github.io/checklist/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular,angular\t{}\t\t\\N\t\\N\t\\N\n536\t2020-07-22 08:08:14.64887\t2020-07-22 08:08:14.64887\tRS CloneWars\thttps://github.com/rolling-scopes-school/tasks\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage2\t{}\t\t\\N\t\\N\t\\N\n537\t2020-07-25 09:04:32.443128\t2020-07-25 09:16:46.759794\t[Android] Task 6 MVP\thttps://github.com/rolling-scopes-school/rs.android.task.6\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tkotlintask\tstage2\t{}\t\t\\N\t\\N\t\\N\n538\t2020-07-28 05:44:35.694818\t2020-07-28 06:09:53.982099\tCodewars Test\thttps://github.com/rolling-scopes/rsschool-app\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodewars\treact,codewars\t{}\t\t\\N\t\\N\t\\N\n539\t2020-08-02 20:57:21.752305\t2020-08-05 10:27:49.213083\tCodewars React\thttps://github.com/rolling-scopes-school/tasks/blob/f504966947a9f3e85a27f6401e7a6870f870f392/tasks/codewars-react.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodewars\treact,codewars\t{}\t\t\\N\t\\N\t\\N\n540\t2020-08-03 14:31:53.354433\t2020-08-03 14:31:53.354433\tInterview(React)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/interview-react.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tinterview\t\t{}\t\t\\N\t\\N\t\\N\n541\t2020-08-05 09:10:58.734646\t2020-08-05 09:10:58.734646\tAngular. NgRX\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/angular/NgRX.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular,angular\t{}\t\t\\N\t\\N\t\\N\n542\t2020-08-15 20:40:21.595491\t2020-08-15 20:41:37.149481\tSchedule\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/schedule.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact,js\t{}\t\t\\N\t\\N\t\\N\n543\t2020-08-15 20:42:00.436081\t2020-08-15 20:42:00.436081\tX Check App\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/xcheck/xcheck.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact,js\t{}\t\t\\N\t\\N\t\\N\n544\t2020-08-23 13:40:57.097441\t2020-08-23 13:40:57.097441\tMobile Hackathon\thttps://medium.com/mobilepeople/rolling-scopes-mobile-hackathon-results-9c96b4fb4211\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcodejam\t\t{}\t\t\\N\t\\N\t\\N\n545\t2020-08-27 04:13:37.333538\t2020-08-27 04:13:37.333538\tTask 1. Calculator\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-1-calculator-40\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n546\t2020-08-27 04:30:07.971139\t2020-10-06 14:37:51.758728\tCodewars Basic\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/codewars-basic.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n547\t2020-08-27 04:35:39.114632\t2020-08-27 04:35:39.114632\tSimple Singolo\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/simple-singolo.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\thtml\t{}\t\t\\N\t\\N\t\\N\n548\t2020-08-27 14:34:07.755403\t2020-08-27 14:34:07.755403\tHTML-basics\thttps://ru.code-basics.com/languages/html\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\thtml\t{}\t\t\\N\t\\N\t\\N\n549\t2020-08-27 14:34:39.873265\t2020-08-27 14:34:39.873265\tCSS-basics\thttps://ru.code-basics.com/languages/css\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\thtml\t{}\t\t\\N\t\\N\t\\N\n550\t2020-08-27 14:35:10.167076\t2020-08-27 14:35:10.167076\tJS-basics\thttps://ru.code-basics.com/languages/javascript\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n551\t2020-08-27 16:10:52.287849\t2020-08-27 16:10:52.287849\tTask 2. Dynamic Landing Page\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-2-dynamic-landing-page-30\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n552\t2020-08-27 16:11:28.541996\t2020-08-27 16:11:28.541996\tTask 3. Meditation App\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-3-meditation-app-20\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n553\t2020-08-27 16:11:57.491788\t2020-08-27 16:11:57.491788\tTask 4. Drum Kit\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-4-drum-kit-20\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n554\t2020-08-27 16:12:27.5845\t2020-08-27 16:12:27.5845\tTask 5. CSS Variables and JS\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-5-css-variables-and-js-20\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n593\t2020-12-19 12:47:59.940867\t2021-06-28 13:37:07.392607\tCV. Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/cv/html-css.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage0,html\t{}\t\t\\N\t\\N\t\\N\n596\t2020-12-26 18:32:46.338943\t2021-03-06 10:31:21.886056\tJS Test #0\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tstage0\t{}\t\t\\N\t\\N\t\\N\n555\t2020-08-27 16:12:54.861753\t2020-08-27 16:12:54.861753\tTask 6. Flex Panel Gallery\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-6-flex-panel-gallery-10\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n556\t2020-08-27 16:13:19.737287\t2020-08-27 16:13:19.737287\tTask 7. Fun with HTML5 Canvas\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-7-fun-with-html5-canvas-40\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n557\t2020-08-27 16:13:49.956984\t2020-08-27 16:13:49.956984\tTask 8. Custom Video Player\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-8-custom-video-player-20\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n558\t2020-08-27 16:14:13.433263\t2020-08-27 16:14:13.433263\tTask 9. Video Speed Controller\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-9-video-speed-controller-10\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n559\t2020-08-27 16:14:37.523502\t2020-08-27 16:14:37.523502\tTask 10. Whack-A-Mole\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-10-whack-a-mole-40\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n560\t2020-08-27 16:15:04.873511\t2020-08-27 16:15:04.873511\tTask 11. Virtual Keyboard\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-11-virtual-keyboard-40\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n561\t2020-08-27 16:15:27.500667\t2020-08-27 16:15:27.500667\tTask 12. Chat on socket.io\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/projects.md#task-12-chat-on-socketio-20\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n562\t2020-08-27 16:16:25.117143\t2020-10-05 17:32:25.450477\tCodewars Basic-1\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/codewars-basic-1.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n563\t2020-08-27 16:17:05.5464\t2020-10-05 17:24:40.436098\tCodewars Basic-2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/codewars-basic-2.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n564\t2020-08-27 16:17:46.058557\t2020-08-27 16:17:46.058557\traindrops\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/raindrops.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n565\t2020-08-27 16:18:08.763424\t2020-08-27 16:18:08.763424\tfancy-weather\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage-0/fancy-weather.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n566\t2020-08-27 16:35:27.649926\t2020-08-27 16:35:27.649926\tPortfolio\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/Portfolio.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs\t{}\t\t\\N\t\\N\t\\N\n567\t2020-09-05 12:46:35.283775\t2020-10-19 10:11:31.643018\tSelf HTML Basics\thttps://ru.code-basics.com/languages/html\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\thtml\t{}\t\t\\N\t\\N\t\\N\n568\t2020-09-07 19:16:43.975374\t2020-10-19 10:11:24.138441\tSelf CSS Basics\thttps://ru.code-basics.com/languages/css\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tcss\t{}\t\t\\N\t\\N\t\\N\n569\t2020-09-07 20:23:16.53491\t2020-10-19 10:11:13.239832\tSelf JS Basics\thttps://ru.code-basics.com/languages/javascript\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tjs\t{}\t\t\\N\t\\N\t\\N\n570\t2020-09-19 08:01:33.992409\t2020-09-19 08:01:33.992409\twebdev\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/markups/level-1/webdev/webdev-ru.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,html\t{}\t\t\\N\t\\N\t\\N\n571\t2020-09-21 11:21:05.630909\t2020-09-21 11:21:05.630909\tCalculator\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/ready-projects/calculator.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage1,js\t{}\t\t\\N\t\\N\t\\N\n572\t2020-09-21 16:03:35.625542\t2020-09-21 16:03:35.625542\tMomentum\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/ready-projects/momentum.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage1,js\t{}\t\t\\N\t\\N\t\\N\n573\t2020-09-21 16:04:10.12875\t2020-09-21 16:04:10.12875\tVirtual Keyboard\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/ready-projects/virtual-keyboard.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage1,js\t{}\t\t\\N\t\\N\t\\N\n574\t2020-09-22 08:55:51.123185\t2021-07-16 17:01:41.593901\tAndroid Final Quiz\thttps://forms.gle/TTcLK8kLEWveR7BF9\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage2 ,Android,Kotlin\t{}\t\t\\N\t\\N\t\\N\n575\t2020-09-26 12:58:24.834196\t2021-07-13 02:18:54.829974\tReact Team Task Presentation\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/schedule.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact,presentation\t{}\t\t\\N\t\\N\t\\N\n576\t2020-09-28 15:41:37.15626\t2020-09-28 15:45:51.670373\tShelter Cross-check\thttps://github.com/rolling-scopes-school/tasks/tree/master/tasks/markups/level-2/shelter\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,html\t{}\t\t\\N\t\\N\t\\N\n577\t2020-09-28 15:57:47.386043\t2020-09-28 15:57:47.386043\tGem Puzzle\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/gem-pazzle/codejam-the-gem-puzzle.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2\t{}\t\t\\N\t\\N\t\\N\n578\t2020-10-13 05:44:26.854548\t2020-10-13 05:44:26.854548\tAWS_task1\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task1-cloud-introduction/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws\t{}\t\t\\N\t\\N\t\\N\n579\t2020-10-19 08:18:56.59736\t2020-10-19 08:18:56.59736\tAWS_task2\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task2-serve-spa-aws/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws\t{}\t\t\\N\t\\N\t\\N\n580\t2020-10-26 11:34:32.421958\t2020-10-26 11:34:32.421958\tAWS-task3\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task3-product-api/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws ,cross-check\t{}\t\t\\N\t\\N\t\\N\n581\t2020-11-02 14:50:19.794867\t2020-11-02 14:50:19.794867\tAWS-task4\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task4-rds/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws ,cross-check,nodejs\t{}\t\t\\N\t\\N\t\\N\n582\t2020-11-12 16:52:45.903122\t2020-11-12 16:52:45.903122\tAWS_task5\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task5-import-to-s3/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws ,cross-check,js\t{}\t\t\\N\t\\N\t\\N\n583\t2020-11-16 12:01:36.081559\t2020-11-16 12:01:36.081559\tAWS-task6\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/tree/main/task6-sqs-sns\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws ,cross-check,js,nodejs\t{}\t\t\\N\t\\N\t\\N\n584\t2020-11-20 07:21:08.683763\t2020-11-20 07:21:08.683763\tRS Селекторы\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rs-css.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,stage2\t{}\t\t\\N\t\\N\t\\N\n585\t2020-11-20 07:26:46.82712\t2020-11-20 07:26:46.82712\tRS Селекторы:Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rs-css.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,stage2\t{}\t\t\\N\t\\N\t\\N\n586\t2020-11-24 09:22:01.197268\t2020-11-24 09:22:01.197268\tAWs_task7\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task7-lambda%2Bcognito-authorization/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws ,js,cross-check,nodejs\t{}\t\t\\N\t\\N\t\\N\n587\t2020-12-01 12:57:37.039959\t2020-12-01 12:57:37.039959\tAWS_task8\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task8-docker-elastic-beanstalk/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws ,cross-check,nodejs\t{}\t\t\\N\t\\N\t\\N\n588\t2020-12-08 20:21:00.816025\t2020-12-08 20:21:00.816025\tAWS_task9\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/task9-bff/task.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\taws ,cross-check,nodejs\t{}\t\t\\N\t\\N\t\\N\n589\t2020-12-11 12:19:08.377006\t2020-12-18 19:53:01.805815\tCOVID-19 Dashboard\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/covid-dashboard.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,stage2\t{}\t\t\\N\t\\N\t\\N\n590\t2020-12-11 12:20:12.955324\t2020-12-18 19:53:15.107973\tCOVID-19 Dashboard:Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/covid-dashboard.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,stage2\t{}\t\t\\N\t\\N\t\\N\n591\t2020-12-16 11:22:13.348836\t2020-12-16 11:22:13.348836\tAWS_feedback_build_plan\thttps://github.com/rolling-scopes-school/nodejs-aws-tasks/blob/main/feedback_and_possible_plan.me\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tjs,nodejs,aws\t{}\t\t\\N\t\\N\t\\N\n592\t2020-12-19 12:43:52.804419\t2021-05-19 02:54:47.307367\tCodewars #0\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tasks/codewars\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tjstask\tjs,codewars,stage0\t{}\t\t\\N\t\\N\t\\N\n594\t2020-12-19 12:49:31.14823\t2021-03-07 16:21:45.593427\tWildlife\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tasks/wildlife\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage0,html\t{}\t\t\\N\t\\N\t\\N\n595\t2020-12-26 18:31:32.147857\t2021-03-06 10:31:15.424715\tHTML/CSS Test #0\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tstage0\t{}\t\t\\N\t\\N\t\\N\n597\t2020-12-26 18:33:44.873478\t2021-06-30 16:38:02.096425\tRSS Test\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tstage0\t{}\t\t\\N\t\\N\t\\N\n598\t2021-01-14 16:07:51.521813\t2021-01-14 16:07:51.521813\tST Extra curry\thttps://observablehq.com/@shastel/functions-and-arguments\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tst\t{}\t\t\\N\t\\N\t\\N\n599\t2021-01-16 07:15:35.629304\t2021-01-16 07:15:35.629304\tRS Clone\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rsclone/rsclone.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2\t{}\t\t\\N\t\\N\t\\N\n600\t2021-01-18 20:37:27.531064\t2021-01-18 20:37:27.531064\tAngular. RS Lang\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/angular-new/angular-rslang.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n601\t2021-01-18 20:44:09.805032\t2021-04-07 09:35:44.904556\tAngular. RS Lang: Cross-Check\thttps://rs-lang-cross-check.netlify.app/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n602\t2021-01-21 12:47:14.940104\t2021-01-21 12:47:14.940104\tTest\thttps://github.com/yuliaHope/rsschool-api/tree/feature/S-9-implement-adding-task/client/src/components/Forms\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tkotlintask\t\t{}\t\t\\N\t\\N\t\\N\n603\t2021-01-21 17:00:47.237938\t2021-01-21 17:00:47.237938\t[EXTRA] Custom addEventListener\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/5.-%5BEXTRA%5D-Custom-addEventListener\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tST\t{}\t\t\\N\t\\N\t\\N\n604\t2021-01-22 13:36:10.256772\t2021-01-24 12:53:00.085111\tPandas data manipulations\thttps://github.com/rolling-scopes-school/ml-intro/blob/2021/1_data_manipulations/Pandas_data_manipulations.ipynb\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tipynb\tPandas,Python\t{}\t\t\\N\t\\N\t\\N\n605\t2021-02-01 14:35:31.761066\t2021-02-01 14:35:31.761066\t2 - Linear Regression and Visualization\thttps://github.com/rolling-scopes-school/ml-intro/blob/2021/2_linear_regression/seminar_and_homework.ipynb\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\tPandas,Python\t{}\t\t\\N\t\\N\t\\N\n606\t2021-02-01 16:45:01.840662\t2021-02-01 16:45:01.840662\tST Load\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/6.-Load\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tST\t{}\t\t\\N\t\\N\t\\N\n607\t2021-02-03 16:51:09.09653\t2021-02-03 16:51:09.09653\tThings 1\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/7.-Things-BE-v1\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tST\t{}\t\t\\N\t\\N\t\\N\n608\t2021-02-11 08:44:07.456369\t2021-02-11 08:44:07.456369\t3 - Overfitting and Regularization\thttps://github.com/rolling-scopes-school/ml-intro/tree/2021/3_overfitting_regularization\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\tPandas,Python\t{}\t\t\\N\t\\N\t\\N\n609\t2021-02-13 18:01:57.191651\t2021-02-13 18:01:57.191651\tRS Clone Presentation\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rsclone/rsclone.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2\t{}\t\t\\N\t\\N\t\\N\n610\t2021-02-15 10:58:06.20701\t2021-02-15 10:58:06.20701\t3- Quiz Overfitting and Regularization\thttps://docs.google.com/forms/d/e/1FAIpQLSe_QHNj_mHGQ3afxBLny2o3CeiE7kZbo41-Aco_gjbLq_J8_Q/viewform?usp=sf_link\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\t\t{}\t\t\\N\t\\N\t\\N\n611\t2021-02-15 17:32:15.694641\t2021-02-15 17:32:15.694641\t4 - Feature Engineering and Selection\thttps://github.com/rolling-scopes-school/ml-intro/blob/2021/4_feature_engineering_selection/feature_engineering_selection.ipynb\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\t\t{}\t\t\\N\t\\N\t\\N\n612\t2021-02-17 10:39:01.421981\t2021-02-17 10:39:01.421981\tReact Game\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-game.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n613\t2021-02-28 06:57:22.138546\t2021-02-28 11:51:23.17022\t5 - Classification Linear KNN (Part 1)\thttps://github.com/rolling-scopes-school/ml-intro/tree/2021/5_classification_linear_knn\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\tPandas,Python\t{}\t\t\\N\t\\N\t\\N\n614\t2021-03-01 10:33:03.991004\t2021-03-01 10:33:03.991004\t5 - Quiz Classification Linear KNN\thttps://docs.google.com/forms/d/e/1FAIpQLScJ3iEMm756uQq7JcNia9WMaUe6Dm1XkMjEHqKHrxgS6TLjpg/closedform\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\t\t{}\t\t\\N\t\\N\t\\N\n615\t2021-03-02 11:26:52.273548\t2021-03-02 11:26:52.273548\tНомер макета Online Zoo\thttps://rolling-scopes-school.github.io/roadmap/#/stage1/tasks/online-zoo\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n616\t2021-03-04 14:36:26.155447\t2021-03-04 14:36:26.155447\tTravel App\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/travel-app.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n617\t2021-03-05 11:09:19.040392\t2021-03-05 11:09:19.040392\t5 - Classification Linear KNN (Part 2)\thttps://github.com/rolling-scopes-school/ml-intro/blob/2021/5_classification_linear_knn/seminar.ipynb\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\tPandas,Python\t{}\t\t\\N\t\\N\t\\N\n618\t2021-03-05 11:17:02.022234\t2021-03-05 11:17:02.022234\t6 - Trees and Ensembles\thttps://github.com/rolling-scopes-school/ml-intro/blob/2021/6_trees%20and%20ensembles/rf_classifier.ipynb\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\tPython,Pandas\t{}\t\t\\N\t\\N\t\\N\n619\t2021-03-05 11:18:59.536474\t2021-03-05 11:18:59.536474\t6 - Quiz Trees and Ensembles\thttps://forms.gle/QppfozwckCZMoPhC8\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\t\t{}\t\t\\N\t\\N\t\\N\n620\t2021-03-05 16:59:00.627541\t2021-03-05 16:59:00.627541\tST Last checkpoint\thttps://docs.google.com/spreadsheets/d/19G_U4gPsuC6L2NjGoanGRGU2-cc6y6b1y8iZcDMF2fI/edit?usp=sharing\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tstage-interview\tST\t{}\t\t\\N\t\\N\t\\N\n621\t2021-03-06 16:09:14.287858\t2021-03-06 16:09:40.434646\t7 - Clustering and Dimensionality Reduction\thttps://github.com/rolling-scopes-school/ml-intro/blob/2021/7_clustering/clustering.ipynb\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\tPython,Pandas\t{}\t\t\\N\t\\N\t\\N\n622\t2021-03-06 16:13:05.067733\t2021-03-06 16:13:23.274674\t7 - Quiz Clustering and Dimensionality Reduction\thttps://forms.gle/bzBPEtnyuA347dJD7\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\ttest\t\t{}\t\t\\N\t\\N\t\\N\n623\t2021-03-11 11:04:08.681819\t2021-03-11 11:04:08.681819\t[Test] Virtual Piano\thttps://github.com/rolling-scopes-school/stage1/blob/main/tasks/virtual-piano.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\ttest\t{}\t\t\\N\t\\N\t\\N\n624\t2021-03-12 18:25:36.803679\t2021-03-12 18:25:36.803679\tMarkdown & Git (EN)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/git-markdown.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tcv:markdown\tstage0\t{}\t\t\\N\t\\N\t\\N\n625\t2021-03-16 04:32:02.049634\t2021-03-22 08:10:06.849863\tVirtual-piano\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/js-projects/virtual-piano\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n626\t2021-03-16 10:32:32.861577\t2021-03-16 11:47:39.420121\tGit test (EN)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/test-git\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt,stage0,test\t{}\t\t\\N\t\\N\t\\N\n627\t2021-03-19 15:38:13.638778\t2021-03-19 15:38:13.638778\tReact. RS Lang\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-rslang.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n628\t2021-03-19 15:39:27.503723\t2021-07-13 02:18:41.256448\tReact. Team Task\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/tba.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n629\t2021-03-25 06:58:19.94643\t2021-03-25 06:58:19.94643\t8 - Quiz model evaluation and selection\thttps://forms.gle/zTMLDLiFCMXijrJC9\t\\N\tauto\tf\tf\tf\t\\N\t\\N\ttest\t\t{}\t\t\\N\t\\N\t\\N\n630\t2021-03-29 09:18:15.128409\t2021-03-29 09:18:15.128409\tClean-code-s1e1\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/clean-code/clean-code-s1e1\t\\N\tmanual\tt\tf\tf\tclean-code-s1e1\t\\N\thtmltask\tstage1,html,clean-code\t{}\t\t\\N\t\\N\t\\N\n631\t2021-03-29 19:46:38.437531\t2021-04-18 16:08:11.754724\tonline-zoo-w-12-v-1\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-1\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n632\t2021-03-29 19:47:22.348097\t2021-04-18 16:08:03.65834\tonline-zoo-w-12-v-2\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-2\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n633\t2021-03-29 19:49:52.753381\t2021-04-18 16:07:55.984858\tonline-zoo-w-12-v-3\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-3\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n634\t2021-03-29 19:51:44.40457\t2021-04-18 16:07:43.996559\tonline-zoo-w-12-v-4\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-4\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n635\t2021-03-29 19:52:09.987412\t2021-04-18 16:07:33.660824\tonline-zoo-w-12-v-5\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-5\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n636\t2021-03-29 19:53:19.216383\t2021-04-18 16:07:24.592728\tonline-zoo-w-12-v-6\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-6\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n637\t2021-03-30 18:22:15.783273\t2021-07-13 07:27:31.217997\twebdev (EN)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/webdev-en.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,rs-lt,rs-ge\t{}\t\t\\N\t\\N\t\\N\n638\t2021-04-01 13:43:15.969162\t2021-04-01 13:43:15.969162\tFinal competition\thttps://www.kaggle.com/c/rss-top-performers-prediction\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tipynb\tPandas,Python\t{}\t\t\\N\t\\N\t\\N\n639\t2021-04-01 14:29:52.270971\t2021-04-01 14:37:09.388365\tST 2021\thttps://github.com/rkhaslarov/rs-school-short-track-2021\t\\N\tauto\tf\tf\tf\trs-school-short-track-2021\thttps://github.com/rkhaslarov/rs-school-short-track-2021\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n640\t2021-04-03 10:32:13.92427\t2021-07-13 07:34:45.184634\tHtml/Css test\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/self-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt,rs-ge\t{}\t\t\\N\t\\N\t\\N\n641\t2021-04-07 20:42:10.851958\t2021-04-22 12:21:36.749143\tClean-code: Test for generic principles\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tests/clean-code-generic-principles-test\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tclean-code,test,stage1\t{}\t\t\\N\t\\N\t\\N\n642\t2021-04-12 06:51:48.539525\t2021-04-12 07:43:02.439332\tSelf-Introduction\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/self-introduction/self-introduction\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tcross-check,self-presentation,stage1\t{}\t\t\\N\t\\N\t\\N\n643\t2021-04-13 08:16:49.145559\t2021-04-14 05:52:03.673776\tSemantic. CSS3 test\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/self-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt\t{}\t\t\\N\t\\N\t\\N\n644\t2021-04-13 08:36:33.949389\t2021-07-21 07:16:52.562083\tFlex / Grid test\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/self-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt,rs-ge\t{}\t\t\\N\t\\N\t\\N\n645\t2021-04-18 16:09:38.619468\t2021-04-18 16:09:38.619468\tonline-zoo-w-34-v-1\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-1\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n646\t2021-04-18 16:10:17.054588\t2021-04-18 16:10:17.054588\tonline-zoo-w-34-v-2\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-2\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n647\t2021-04-18 16:10:46.756453\t2021-04-18 16:10:46.756453\tonline-zoo-w-34-v-3\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-3\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n648\t2021-04-18 16:11:15.864407\t2021-04-18 16:11:15.864407\tonline-zoo-w-34-v-4\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-4\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n649\t2021-04-18 16:11:48.247653\t2021-04-18 16:11:48.247653\tonline-zoo-w-34-v-5\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-5\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n650\t2021-04-18 16:12:18.705378\t2021-04-18 16:12:18.705378\tonline-zoo-w-34-v-6\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-6\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n651\t2021-04-19 17:07:46.148707\t2021-07-22 08:30:46.745826\ttheyalow (LT)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/theyalow-en(LT).md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\trs-lt,rs-ge\t{}\t\t\\N\t\\N\t\\N\n652\t2021-04-20 07:22:34.80059\t2021-04-20 07:22:34.80059\tphoto-filter\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/js-projects/photo-filter\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage1\t{}\t\t\\N\t\\N\t\\N\n653\t2021-04-27 17:54:11.564999\t2021-05-04 07:36:17.722256\tJS Basics test\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/self-test.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt\t{}\t\t\\N\t\\N\t\\N\n654\t2021-05-02 14:17:32.626997\t2021-05-02 14:53:39.585606\tDebug in Node.js\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs,cross-check\t{}\t\t\\N\t\\N\t\\N\n655\t2021-05-02 14:18:45.971414\t2021-05-28 15:42:33.722491\tTypescript basics\thttps://github.com/rolling-scopes-school/basic-nodejs-2021Q2#task-4-typescript-basics\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs,typescript,cross-check\t{}\t\t\\N\t\\N\t\\N\n656\t2021-05-02 14:19:28.225416\t2021-06-06 20:25:53.6616\tDocker Basics\thttps://github.com/rolling-scopes-school/basic-nodejs-2021Q2#task-6-docker-basics\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs,docker,cross-check\t{}\t\t\\N\t\\N\t\\N\n657\t2021-05-02 14:20:16.880508\t2021-06-20 19:16:52.578078\tPostgreSQL + Typeorm\thttps://github.com/rolling-scopes-school/basic-nodejs-2021Q2#task-7-postgresql--typeorm\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs,cross-check\t{}\t\t\\N\t\\N\t\\N\n658\t2021-05-02 14:20:42.825012\t2021-06-27 20:09:12.145094\tNest.js\thttps://github.com/rolling-scopes-school/basic-nodejs-2021Q2#task-9-nestjs\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tnodejs,cross-check\t{}\t\t\\N\t\\N\t\\N\n659\t2021-05-03 13:53:09.236679\t2021-05-06 15:57:35.606074\trs.ios.objc.task1\thttps://github.com/rolling-scopes-school/rs.ios.stage-task1/blob/main/README.md\t\\N\tauto\tf\tf\tf\trs.ios.stage-task1\thttps://github.com/rolling-scopes-school/rs.ios.stage-task1\tobjctask\tstage1\t{}\t\t\\N\t\\N\t\\N\n660\t2021-05-03 13:54:27.780527\t2021-05-13 16:58:24.194514\trs.ios.objc.task2\thttps://github.com/rolling-scopes-school/rs.ios.stage-task2/blob/main/README.md\t\\N\tauto\tf\tf\tf\trs.ios.stage-task2\thttps://github.com/rolling-scopes-school/rs.ios.stage-task2\tobjctask\tstage1\t{}\t\t\\N\t\\N\t\\N\n661\t2021-05-03 13:55:39.123913\t2021-05-20 14:39:27.068753\trs.ios.objc.task3\thttps://github.com/rolling-scopes-school/rs.ios.stage-task3/blob/main/README.md\t\\N\tauto\tf\tf\tf\trs.ios.stage-task3\thttps://github.com/rolling-scopes-school/rs.ios.stage-task3\tobjctask\tstage1\t{}\t\t\\N\t\\N\t\\N\n662\t2021-05-03 16:10:49.681267\t2021-05-03 16:10:49.681267\ttest\thttp://www.google.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tstage-interview\ttest\t{}\t\t\\N\t\\N\t\\N\n663\t2021-05-04 08:37:07.553302\t2021-05-07 14:34:18.073106\tJS Functions test\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt\t{}\t\t\\N\t\\N\t\\N\n664\t2021-05-04 12:37:05.984112\t2021-05-04 12:38:54.504325\tonline-zoo-w-56-v-1\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-1\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n665\t2021-05-04 12:37:39.756077\t2021-05-04 12:39:05.223097\tonline-zoo-w-56-v-2\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-2\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n666\t2021-05-04 12:38:10.637801\t2021-05-04 12:39:17.704872\tonline-zoo-w-56-v-3\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-3\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n667\t2021-05-04 12:38:38.846279\t2021-05-04 12:39:32.750323\tonline-zoo-w-56-v-4\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-4\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n668\t2021-05-04 12:39:52.288354\t2021-05-04 12:39:52.288354\tonline-zoo-w-56-v-5\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-5\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n669\t2021-05-04 12:41:25.656806\t2021-05-04 12:41:25.656806\tonline-zoo-w-56-v-6\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/variant-6\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo\t{}\t\t\\N\t\\N\t\\N\n670\t2021-05-06 10:30:29.740685\t2021-05-06 10:30:29.740685\tJS Functions test part 2\thttps://example.com\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt\t{}\t\t\\N\t\\N\t\\N\n671\t2021-05-07 11:31:44.518467\t2021-05-07 11:31:44.518467\tCodewars #2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars2.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n672\t2021-05-13 08:22:31.158757\t2021-05-13 08:22:31.158757\tCalculator(LT)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/calculator(LT).md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\trs-lt\t{}\t\t\\N\t\\N\t\\N\n673\t2021-05-16 13:31:42.307849\t2021-05-16 13:31:42.307849\tST Deep Copy\thttps://github.com/rolling-scopes-school/RS-Short-Track/wiki/2.-Deep-copy\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tST\t{}\t\t\\N\t\\N\t\\N\n674\t2021-05-19 06:11:22.765584\t2021-05-19 06:11:22.765584\tInterview(LT)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/stage1-interview(LT).md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tinterview\trs-lt,stage1,interview\t{}\t\t\\N\t\\N\t\\N\n675\t2021-05-19 16:14:25.053477\t2021-05-19 16:14:25.053477\tST Checkpoint 1\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n676\t2021-05-20 05:51:02.732991\t2021-05-20 05:51:02.732991\tDOM API\thttps://example.com\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt\t{}\t\t\\N\t\\N\t\\N\n677\t2021-05-20 12:01:17.704883\t2021-05-20 12:01:17.704883\tonline-zoo\thttps://rolling-scopes-school.github.io/stage0/#/stage1/tasks/online-zoo/online-zoo\t\\N\tauto\tf\tf\tf\t\\N\t\\N\thtmltask\tstage1,online zoo,html,css,js\t{}\t\t\\N\t\\N\t\\N\n678\t2021-05-21 13:36:43.887646\t2021-05-21 13:36:43.887646\tAndroid 2021 - Practice 1 - Randomizer\thttps://github.com/rolling-scopes-school/rsschool2021-Android-task-randomizer\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tkotlintask\tAndroid,Kotlin,stage1\t{}\t\t\\N\t\\N\t\\N\n679\t2021-05-26 14:01:27.071863\t2021-05-26 14:01:27.071863\tMatch-Match Game\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/match-match-game.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,TypeScript\t{}\t\t\\N\t\\N\t\\N\n680\t2021-05-26 14:30:24.634918\t2021-05-26 14:30:24.634918\tAsync Race\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/async-race.md\t\\N\tmanual\t\\N\tf\tf\t\\N\t\\N\tJS task\t\t{}\t\t\\N\t\\N\t\\N\n681\t2021-06-03 15:43:16.636933\t2021-06-10 17:00:13.376693\trs.ios.swift.task4\thttps://github.com/rolling-scopes-school/rs.ios.stage-task4/blob/main/README.md\t\\N\tauto\tf\tf\tf\trs.ios.stage-task4\thttps://github.com/rolling-scopes-school/rs.ios.stage-task4\tobjctask\tstage2\t{}\t\t\\N\t\\N\t\\N\n682\t2021-06-03 17:00:30.151954\t2021-06-11 08:11:45.792399\tInheritance Test (LT)\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt\t{}\t\t\\N\t\\N\t\\N\n683\t2021-06-08 15:08:20.85744\t2021-06-08 15:08:20.85744\tST Checkpoint 2\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n684\t2021-06-09 13:27:53.269173\t2021-06-09 13:27:53.269173\tAndroid 2021 - Practice 2 - Quiz\thttps://github.com/rolling-scopes-school/rsschool2021-Android-task-quiz\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tkotlintask\tstage1,Android,Kotlin,cross-check\t{}\t\t\\N\t\\N\t\\N\n685\t2021-06-10 08:08:46.270232\t2021-06-10 08:09:09.092384\tST CRP course\thttps://www.udacity.com/course/website-performance-optimization--ud884\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tst\t{}\t\t\\N\t\\N\t\\N\n686\t2021-06-10 16:51:59.094555\t2021-06-10 17:05:58.337933\trs.ios.swift.task5\thttps://github.com/rolling-scopes-school/rs.ios.stage-task5/blob/main/README.md\t\\N\tauto\tf\tf\tf\trs.ios.stage-task5\thttps://github.com/rolling-scopes-school/rs.ios.stage-task5\tobjctask\tstage2\t{}\t\t\\N\t\\N\t\\N\n687\t2021-06-10 18:57:54.547085\t2021-06-10 18:57:54.547085\tAsync test\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tselfeducation\trs-lt\t{}\t\t\\N\t\\N\t\\N\n688\t2021-06-11 08:03:40.16882\t2021-06-11 08:03:40.16882\tAsync Race. Cross-Check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/async-race.md#cross-check\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,cross-check\t{}\t\t\\N\t\\N\t\\N\n689\t2021-06-17 16:48:43.28106\t2021-06-17 16:48:43.28106\trs.ios.swift.task6\thttps://github.com/rolling-scopes-school/rs.ios.stage-task6/blob/main/README.md\t\\N\tauto\tf\tf\tf\trs.ios.stage-task6\thttps://github.com/rolling-scopes-school/rs.ios.stage-task6\tobjctask\tstage2\t{}\t\t\\N\t\\N\t\\N\n690\t2021-06-20 16:40:22.899085\t2021-06-22 14:18:21.578778\tEnglish for kids S1E1. Cross-check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,cross-check,stage2\t{}\t\t\\N\t\\N\t\\N\n691\t2021-06-20 16:43:38.061004\t2021-06-22 14:17:41.169677\tChess S1E1. Cross-check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/chess/codejam-chess-part-one.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,cross-check,stage2\t{}\t\t\\N\t\\N\t\\N\n692\t2021-06-21 13:42:46.349301\t2021-06-22 14:15:10.391564\tEnglish for kids S1E1\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,stage2\t{}\t\t\\N\t\\N\t\\N\n693\t2021-06-21 13:43:09.688432\t2021-06-22 14:17:13.581254\tChess S1E1\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/chess/codejam-chess-part-one.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,stage2\t{}\t\t\\N\t\\N\t\\N\n694\t2021-06-22 14:19:43.29645\t2021-06-22 14:19:43.29645\tEnglish for kids S1E2. Cross-check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids-admin-panel.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage2 ,TypeScript,cross-check\t{}\t\t\\N\t\\N\t\\N\n695\t2021-06-22 14:20:29.274537\t2021-06-24 18:43:34.398904\tChess S1E2. Cross-check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/chess/codejam-chess-part-two.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,stage2 ,cross-check\t{}\t\t\\N\t\\N\t\\N\n696\t2021-06-22 14:22:03.421406\t2021-06-22 14:22:03.421406\tEnglish for kids S1E2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids-admin-panel.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,stage2\t{}\t\t\\N\t\\N\t\\N\n697\t2021-06-23 09:50:14.00401\t2021-07-25 05:31:50.361822\tChess S1E2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/chess/codejam-chess-part-two.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,stage2\t{}\t\t\\N\t\\N\t\\N\n698\t2021-06-23 09:51:49.128203\t2021-06-23 09:51:49.128203\tEnglish for kids S1E2. Cross-check\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids-admin-panel.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tTypeScript,stage2 ,cross-check\t{}\t\t\\N\t\\N\t\\N\n699\t2021-06-29 14:04:45.230899\t2021-06-29 14:04:45.230899\trs.ios.crosscheck.task7\thttps://github.com/rolling-scopes-school/rs.ios.stage-task7\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tobjctask\tstage3\t{}\t\t\\N\t\\N\t\\N\n700\t2021-07-02 16:38:03.731078\t2021-07-02 16:38:03.731078\tCodewars Data Types\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars/data-types.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n701\t2021-07-02 16:47:46.456174\t2021-07-02 16:47:46.456174\tCodewars Functions\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars/functions.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n702\t2021-07-02 16:52:48.911494\t2021-07-02 16:52:48.911494\tCodewars Objects & Arrays\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars/objects-arrays.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n703\t2021-07-02 16:58:40.753701\t2021-07-02 16:58:40.753701\tCodewars Algorithms-1\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars/algorithms-1.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n704\t2021-07-02 17:04:13.971816\t2021-07-03 12:37:16.362491\tCodewars Algorithms-2\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/codewars/algorithms-2.md\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tcodewars\tcodewars\t{}\t\t\\N\t\\N\t\\N\n705\t2021-07-04 19:19:47.54283\t2021-07-04 19:19:47.54283\tAndroid 2021 - Practice 3 - Pomodoro\thttps://github.com/rolling-scopes-school/RSShool2021-Android-task-Pomodoro\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tkotlintask\tAndroid,Kotlin,stage1,cross-check\t{}\t\t\\N\t\\N\t\\N\n706\t2021-07-05 08:13:38.447765\t2021-07-05 08:13:38.447765\t[ST] Checkpoint 3\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n707\t2021-07-05 12:36:27.332959\t2021-07-06 07:12:39.982334\tTest HTML Basics [RU]\thttps://ru.code-basics.com/languages/html\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\thtml\t{}\t\t\\N\t\\N\t\\N\n708\t2021-07-06 21:03:43.537339\t2021-07-06 21:03:43.537339\tHTML Quiz\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\thtml\t{}\t\t\\N\t\\N\t\\N\n709\t2021-07-06 21:04:36.087632\t2021-07-06 21:04:36.087632\tCSS Quiz\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tcss\t{}\t\t\\N\t\\N\t\\N\n710\t2021-07-06 21:05:14.935484\t2021-07-06 21:05:14.935484\tJS Quiz\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tjs\t{}\t\t\\N\t\\N\t\\N\n711\t2021-07-06 21:05:58.610129\t2021-07-06 21:06:08.839616\tReactJs Quiz\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\treact\t{}\t\t\\N\t\\N\t\\N\n712\t2021-07-09 09:50:48.828546\t2021-07-09 09:50:48.828546\tAngular Shop\thttps://github.com/rolling-scopes-school/tasks\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n713\t2021-07-09 09:53:06.989516\t2021-07-09 09:53:06.989516\tAngular Shop. Cross-check\thttps://rs-lang-cross-check.netlify.app/\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tAngular\t{}\t\t\\N\t\\N\t\\N\n714\t2021-07-12 16:39:31.049741\t2021-07-12 16:39:31.049741\t[ST] Final checkpoint\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n715\t2021-07-13 01:56:27.706355\t2021-07-21 02:05:29.234568\tReact. Components\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-components.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact,js\t{}\t\t\\N\t\\N\t\\N\n716\t2021-07-13 01:59:06.630799\t2021-07-13 01:59:06.630799\tReact. Forms\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-forms.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n717\t2021-07-13 01:59:57.193405\t2021-07-13 01:59:57.193405\tReact. Redux\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-redux.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n718\t2021-07-13 02:00:40.399879\t2021-07-13 02:00:40.399879\tReact. Routing\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-routing.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n719\t2021-07-13 02:01:22.630194\t2021-07-13 02:01:22.630194\tReact. API\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-api.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n720\t2021-07-13 02:02:18.901305\t2021-07-13 02:02:18.901305\tReact. Testing\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-testing.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n721\t2021-07-13 02:03:39.899612\t2021-07-13 02:03:39.899612\tReact. SSR*\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/react/react-ssr.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\treact\t{}\t\t\\N\t\\N\t\\N\n722\t2021-07-13 11:51:08.864691\t2021-07-13 11:51:08.864691\tGit Quiz\thttps://rolling-scopes-school.github.io/stage0/#/stage0/tests/index\t\\N\tauto\tf\tf\tf\t\\N\t\\N\tselfeducation\tgit\t{}\t\t\\N\t\\N\t\\N\n723\t2021-07-15 07:00:13.193068\t2021-07-15 07:00:13.193068\tEnglish for kids( EN)\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/rslang/english-for-kids-translated.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\trs-lt,rs-ge,stage2\t{}\t\t\\N\t\\N\t\\N\n724\t2021-07-15 07:18:55.566964\t2021-07-15 13:08:12.925631\tTest CSS Basics [RU]\thttps://ru.code-basics.com/languages/css\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tselfeducation\tstage0\t{}\t\t\\N\t\\N\t\\N\n725\t2021-07-15 16:07:15.120253\t2021-07-15 16:07:15.120253\trs.ios.crosscheck.task8\thttps://github.com/rolling-scopes-school/rs.ios.stage-task8\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tobjctask\tstage3\t{}\t\t\\N\t\\N\t\\N\n726\t2021-07-20 03:39:54.174636\t2021-07-20 03:39:54.174636\tMuseum\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/museum/museum.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\thtmltask\tstage0,cross-check\t{}\t\t\\N\t\\N\t\\N\n727\t2021-07-20 07:20:25.761953\t2021-07-20 17:18:06.275459\tTest Algorithms & Data structures\thttps://www.youtube.com/playlist?list=PLP-a1IHLCS7PqDf08LFIYCiTYY1CtoAkt\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tselfeducation\tstage0,algorithms\t{}\t\t\\N\t\\N\t\\N\n728\t2021-07-22 07:59:30.138616\t2021-07-22 07:59:30.138616\t[UZ] RS-lang Backend\thttps://example.com\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\t\t{}\t\t\\N\t\\N\t\\N\n729\t2021-07-22 17:07:21.458164\t2021-07-22 17:39:29.902012\tDrum Kit\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/js30/js30-1.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage0,js\t{}\t\t\\N\t\\N\t\\N\n730\t2021-07-22 17:08:05.196206\t2021-07-22 17:08:05.196206\tJS Clock\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/js30/js30-2.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage0,js\t{}\t\t\\N\t\\N\t\\N\n731\t2021-07-22 17:08:44.272934\t2021-07-22 17:08:44.272934\tVertical Slider\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/js30/js30-3.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage0,js\t{}\t\t\\N\t\\N\t\\N\n732\t2021-07-22 17:09:31.573179\t2021-07-22 17:35:00.094133\tVideo Speed Controller\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/js30/js30-4.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage0,js\t{}\t\t\\N\t\\N\t\\N\n733\t2021-07-22 17:10:07.813794\t2021-07-22 17:10:07.813794\tPhotofilter\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/js30/js30-5.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage0,js\t{}\t\t\\N\t\\N\t\\N\n734\t2021-07-22 17:10:39.403863\t2021-07-22 17:41:32.343542\tWhack-A-Mole\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/js30/js30-6.md\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tjstask\tstage0,js\t{}\t\t\\N\t\\N\t\\N\n735\t2021-07-26 04:57:34.397304\t2021-07-27 07:39:38.807563\tTest JS Basics [RU]\thttps://ru.code-basics.com/languages/javascript\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tselfeducation\tstage0\t{}\t\t\\N\t\\N\t\\N\n736\t2021-07-27 20:49:41.263593\t2021-07-27 20:49:41.263593\trs.ios.crosscheck.task9\thttps://github.com/rolling-scopes-school/rs.ios.stage-task9\t\\N\tmanual\tf\tf\tf\t\\N\t\\N\tobjctask\tstage3\t{}\t\t\\N\t\\N\t\\N\n498\t2022-03-27 11:50:14.892444\t2022-03-27 11:50:14.892444\ttest\thttps://example.com\t\t\\N\t\\N\tf\tf\t\\N\t\\N\tKotlin task\t\t{}\t\t\\N\t\\N\t\\N\n499\t2024-06-19 18:48:13.436646\t2024-06-19 18:48:13.553689\tcore-js-2-interview\thttps://github.com/rolling-scopes-school/tasks/blob/master/tasks/interview-corejs.md\t\t\\N\tf\tf\tf\t\t\tinterview\tinterview\t{\"template\":\"corejs2\"}\t\t1\t499\t\\N\n\\.\n\n\n--\n-- Data for Name: task_artefact; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_artefact (id, \"createdDate\", \"updatedDate\", \"courseTaskId\", \"studentId\", \"videoUrl\", \"presentationUrl\", comment) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: task_checker; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_checker (id, \"createdDate\", \"updatedDate\", \"courseTaskId\", \"studentId\", \"mentorId\") FROM stdin;\n4149\t2024-06-19 18:50:57.699696\t2024-06-19 18:50:57.699696\t432\t14338\t1276\n\\.\n\n\n--\n-- Data for Name: task_criteria; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_criteria (\"taskId\", \"createdDate\", \"updatedDate\", criteria) FROM stdin;\n499\t2024-06-19 18:48:13.538796\t2024-06-19 18:48:13.538796\t[]\n\\.\n\n\n--\n-- Data for Name: task_interview_result; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_interview_result (id, \"createdDate\", \"updatedDate\", \"courseTaskId\", \"studentId\", \"mentorId\", \"formAnswers\", score, comment) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: task_interview_student; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_interview_student (id, \"createdDate\", \"updatedDate\", \"studentId\", \"courseId\", \"courseTaskId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: task_result; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_result (id, \"createdDate\", \"updatedDate\", \"githubPrUrl\", \"githubRepoUrl\", score, comment, \"studentId\", \"courseTaskId\", \"historicalScores\", \"juryScores\", \"lastCheckerId\") FROM stdin;\n78642\t2021-07-28 21:18:25.107083\t2021-07-28 21:18:25.107083\t\\N\t\\N\t500\tVery good task solution! I like it!\t14337\t979\t[{\"authorId\":2595,\"score\":500,\"dateTime\":1627507105089,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78643\t2021-07-28 21:18:35.624298\t2021-07-28 21:18:35.624298\t\\N\t\\N\t700\tVery good task solution! I like it!\t14340\t979\t[{\"authorId\":2595,\"score\":700,\"dateTime\":1627507115622,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78644\t2021-07-28 21:18:45.007131\t2021-07-28 21:18:45.007131\t\\N\t\\N\t45\tVery good task solution! I like it!\t14346\t929\t[{\"authorId\":2595,\"score\":45,\"dateTime\":1627507124998,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78645\t2021-07-28 21:18:57.747085\t2021-07-28 21:18:57.747085\t\\N\t\\N\t120\tVery good task solution! I like it!\t14337\t945\t[{\"authorId\":2595,\"score\":120,\"dateTime\":1627507137729,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78646\t2021-07-28 21:19:25.513612\t2021-07-28 21:19:25.513612\t\\N\t\\N\t355\tVery good task solution! I like it!\t14340\t981\t[{\"authorId\":2595,\"score\":355,\"dateTime\":1627507165497,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78647\t2021-07-28 21:19:35.132131\t2021-07-28 21:19:35.132131\t\\N\t\\N\t360\tVery good task solution! I like it!\t14340\t977\t[{\"authorId\":2595,\"score\":360,\"dateTime\":1627507175130,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78648\t2021-07-28 21:19:42.924362\t2021-07-28 21:19:42.924362\t\\N\t\\N\t160\tVery good task solution! I like it!\t14340\t928\t[{\"authorId\":2595,\"score\":160,\"dateTime\":1627507182916,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78649\t2021-07-28 21:19:58.344963\t2021-07-28 21:19:58.344963\t\\N\t\\N\t160\tVery good task solution! I like it!\t14346\t928\t[{\"authorId\":2595,\"score\":160,\"dateTime\":1627507198326,\"comment\":\"Very good task solution! I like it!\"}]\t[]\t2595\n78650\t2021-07-28 21:21:53.845892\t2021-07-28 21:21:53.845892\t\\N\t\\N\t100\tVery good task. I like it! Keep going!\t14340\t864\t[{\"authorId\":2595,\"score\":100,\"dateTime\":1627507313823,\"comment\":\"Very good task. I like it! Keep going!\"}]\t[]\t2595\n78651\t2021-07-28 21:22:01.000726\t2021-07-28 21:22:01.000726\t\\N\t\\N\t355\tVery good task. I like it! Keep going!\t14346\t981\t[{\"authorId\":2595,\"score\":355,\"dateTime\":1627507320974,\"comment\":\"Very good task. I like it! Keep going!\"}]\t[]\t2595\n\\.\n\n\n--\n-- Data for Name: task_solution; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_solution (id, \"createdDate\", \"updatedDate\", \"courseTaskId\", \"studentId\", url, review, comments) FROM stdin;\n3330\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14327\thttps://example.com\t[]\t[]\n3331\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14328\thttps://example.com\t[]\t[]\n3332\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14329\thttps://example.com\t[]\t[]\n3333\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14330\thttps://example.com\t[]\t[]\n3334\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14331\thttps://example.com\t[]\t[]\n3335\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14332\thttps://example.com\t[]\t[]\n3336\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14333\thttps://example.com\t[]\t[]\n3337\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14334\thttps://example.com\t[]\t[]\n3338\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14335\thttps://example.com\t[]\t[]\n3339\t2020-09-24 18:55:43.0769\t2020-09-24 18:55:43.0769\t386\t14336\thttps://example.com\t[]\t[]\n\\.\n\n\n--\n-- Data for Name: task_solution_checker; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_solution_checker (id, \"createdDate\", \"updatedDate\", \"courseTaskId\", \"taskSolutionId\", \"studentId\", \"checkerId\") FROM stdin;\n11568\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3336\t14333\t14332\n11569\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3337\t14334\t14327\n11570\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3338\t14335\t14334\n11571\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3332\t14329\t14335\n11572\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3335\t14332\t14336\n11573\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3334\t14331\t14330\n11574\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3339\t14336\t14329\n11575\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3331\t14328\t14331\n11576\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3333\t14330\t14333\n11577\t2020-09-24 18:55:51.350257\t2020-09-24 18:55:51.350257\t386\t3330\t14327\t14328\n\\.\n\n\n--\n-- Data for Name: task_solution_result; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_solution_result (id, \"createdDate\", \"updatedDate\", \"courseTaskId\", \"studentId\", \"checkerId\", score, \"historicalScores\", comment, anonymous, review, messages) FROM stdin;\n10812\t2020-09-24 18:57:05.786416\t2020-09-24 18:57:05.786416\t386\t14334\t14327\t50\t[{\"score\":50,\"comment\":\"50 points.\\\\n\\\\n+10 - blah-blah-blah\\\\n+20 - blah-blah-blah\\\\n+30 - blah-blah-blah\",\"anonymous\":false,\"authorId\":11563,\"dateTime\":1600973825778}]\t50 points.\\n\\n+10 - blah-blah-blah\\n+20 - blah-blah-blah\\n+30 - blah-blah-blah\tf\t[]\t[]\n\\.\n\n\n--\n-- Data for Name: task_verification; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.task_verification (id, \"createdDate\", \"updatedDate\", \"studentId\", \"courseTaskId\", details, status, score, metadata, answers) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: team; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.team (id, name, description, \"chatLink\", password, \"teamDistributionId\", \"teamLeadId\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: team_distribution; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.team_distribution (id, \"createdDate\", \"updatedDate\", \"courseId\", \"startDate\", \"endDate\", name, description, \"minTotalScore\", \"descriptionUrl\", \"minTeamSize\", \"maxTeamSize\", \"strictTeamSize\", \"strictTeamSizeMode\") FROM stdin;\n\\.\n\n\n--\n-- Data for Name: team_distribution_student; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.team_distribution_student (id, \"createdDate\", \"updatedDate\", \"studentId\", \"courseId\", \"teamDistributionId\", distributed, active) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: typeorm_metadata; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.typeorm_metadata (type, database, schema, \"table\", name, value) FROM stdin;\n\\.\n\n\n--\n-- Data for Name: user; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.\"user\" (id, \"githubId\", \"firstName\", \"lastName\", \"createdDate\", \"updatedDate\", \"firstNameNative\", \"lastNameNative\", \"tshirtSize\", \"tshirtFashion\", \"dateOfBirth\", \"locationName\", \"locationId\", \"educationHistory\", \"employmentHistory\", \"contactsEpamEmail\", \"contactsPhone\", \"contactsEmail\", \"externalAccounts\", \"epamApplicantId\", activist, \"englishLevel\", \"lastActivityTime\", \"isActive\", \"primaryEmail\", \"contactsTelegram\", \"contactsSkype\", \"contactsNotes\", \"aboutMyself\", \"contactsLinkedIn\", \"profilePermissionsId\", \"countryName\", \"cityName\", \"opportunitiesConsent\", \"cvLink\", \"militaryService\", discord, \"providerUserId\", provider, \"contactsWhatsApp\", languages, obfuscated, contributor_id) FROM stdin;\n11563\tapalchys\t\t\t2020-04-06 15:12:34.19737\t2020-04-06 15:15:02.729722\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\t[]\t[]\t\\N\t\\N\t\\N\t[]\t\\N\tf\t\\N\t1586185954173\tt\ttest@example.com\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t11563\tgithub\t\\N\t{}\tf\t\\N\n2693\tviktoriyavorozhun\t\\N\t\\N\t2019-04-24 13:42:45.500139\t2019-10-18 08:07:58.858658\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2693\tgithub\t\\N\t{}\tf\t\\N\n2098\tyauhenkavalchuk\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-11-12 11:22:33.350237\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567594678450\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2098\tgithub\t\\N\t{}\tf\t\\N\n2103\tshastel\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2020-03-28 19:57:33.715031\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1566996696787\tf\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2103\tgithub\t\\N\t{}\tf\t\\N\n5481\talreadybored\t\\N\t\\N\t2019-09-09 17:27:41.909149\t2020-03-22 14:10:37.252351\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tf\ta1\t1568050061907\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t5481\tgithub\t\\N\t{}\tf\t\\N\n2115\trootthelure\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-06-10 14:20:21.551616\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2115\tgithub\t\\N\t{}\tf\t\\N\n2480\tpavelrazuvalau\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-11-05 16:52:28.602784\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567072599465\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2480\tgithub\t\\N\t{}\tf\t\\N\n2612\tdmitryromaniuk\t\\N\t\\N\t2019-04-24 13:42:44.206396\t2019-12-26 08:27:30.060107\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2612\tgithub\t\\N\t{}\tf\t\\N\n10031\tartem-bagritsevich\t\\N\t\\N\t2020-02-11 08:38:35.202688\t2020-03-05 11:50:05.118784\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tf\ta1\t1581410315197\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t10031\tgithub\t\\N\t{}\tf\t\\N\n2032\tmikhama\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2020-02-24 09:36:43.272628\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567578141812\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2032\tgithub\t\\N\t{}\tf\t\\N\n1328\tdavojta\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-09-07 04:28:42.419938\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567830522415\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t1328\tgithub\t\\N\t{}\tf\t\\N\n3961\tsergeyshalyapin\t\\N\t\\N\t2019-05-15 14:49:46.402468\t2020-02-12 08:17:55.231843\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t3961\tgithub\t\\N\t{}\tf\t\\N\n4476\tabramenal\t\\N\t\\N\t2019-09-02 12:28:32.979516\t2020-03-01 21:13:30.351302\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tf\ta1\t1567427312977\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t4476\tgithub\t\\N\t{}\tf\t\\N\n10130\tsixtyxi\t\\N\t\\N\t2020-02-13 11:35:19.12045\t2020-02-13 11:35:19.12045\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tf\ta1\t1581593719117\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t10130\tgithub\t\\N\t{}\tf\t\\N\n7485\trootical\t\\N\t\\N\t2019-12-19 12:07:57.161662\t2020-03-05 18:51:41.896803\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tf\ta1\t1576757277159\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t7485\tgithub\t\\N\t{}\tf\t\\N\n606\tirinainina\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-08-28 17:19:48.460791\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567012788456\tf\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t606\tgithub\t\\N\t{}\tf\t\\N\n2595\tanik188\t\\N\t\\N\t2019-04-24 13:42:43.967659\t2020-03-06 15:43:33.384469\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tt\ta1\t1567423260809\tf\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2595\tgithub\t\\N\t{}\tf\t\\N\n6776\tksenia-mahilnaya\t\\N\t\\N\t2019-09-17 11:16:55.976071\t2019-09-17 12:19:51.740451\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tf\ta1\t1568719015974\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t6776\tgithub\t\\N\t{}\tf\t\\N\n1090\tpulya10c\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-09-13 10:21:35.108464\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567492440483\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t1090\tgithub\t\\N\t{}\tf\t\\N\n4428\tegngron\t\\N\t\\N\t2019-08-06 12:06:24.920343\t2019-08-06 12:06:24.920343\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t4428\tgithub\t\\N\t{}\tf\t\\N\n4749\tstudentluffi\t\\N\t\\N\t2019-09-09 10:09:09.275849\t2019-09-09 10:09:28.91177\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\tf\ta1\t1568023749273\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t4749\tgithub\t\\N\t{}\tf\t\\N\n587\tsijioth\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-06-10 14:20:03.059291\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t587\tgithub\t\\N\t{}\tf\t\\N\n2084\tdzmitry-varabei\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-09-05 10:13:27.273815\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567678407268\tf\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2084\tgithub\t\\N\t{}\tf\t\\N\n2444\ttoshabely\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-08-22 11:56:20.531337\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2444\tgithub\t\\N\t{}\tf\t\\N\n2277\tanv21\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2020-01-18 11:47:48.686227\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1567683807154\tf\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2277\tgithub\t\\N\t{}\tf\t\\N\n3493\thumanamburu\t\\N\t\\N\t2019-04-25 06:42:53.208093\t2019-09-24 11:22:04.181665\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t0\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t3493\tgithub\t\\N\t{}\tf\t\\N\n2549\tkvtofan\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-09-24 14:56:49.229102\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1563521151921\tf\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2549\tgithub\t\\N\t{}\tf\t\\N\n2089\tyuliahope\t\\N\t\\N\t2019-04-17 11:41:21.396686\t2019-08-29 11:15:32.412097\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1566418583423\tt\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t2089\tgithub\t\\N\t{}\tf\t\\N\n677\tamoebiusss\tTest 1\tLast Name\t2019-04-17 11:41:21.396686\t2020-04-06 15:30:27.059612\t\\N\t\\N\tm\t\\N\t\\N\tMinsk\t12158\t[]\t[]\thello@epam.com\t+375297777777\thello@example.com\t[]\t\\N\t\\N\ta1\t1568012639853\tf\tprimary@example.com\tpavel_durov\t\\N\tdo not call me\ti am a bad guy\t\\N\t\\N\tBelarus\tMinsk\tf\t\\N\t\\N\t\\N\t677\tgithub\t\\N\t{}\tf\t\\N\n11569\tvalerydluski\tname\tlast name\t2020-04-06 15:12:34.19737\t2024-06-19 18:50:08.847974\t\\N\t\\N\t\\N\t\\N\t\\N\tMinsk\t12158\t[]\t[]\texample_mail@epam.com\t+325155534711\thello@example.com\t[]\t\\N\tf\tb2\t1586185954173\tt\ttest@example.com\t\\N\t\\N\t\\N\t\\N\t\\N\t\\N\tBelarus\tMinsk\tt\t\\N\t\\N\t\\N\t12341\tgithub\t\\N\t{AR}\tf\t\\N\n\\.\n\n\n--\n-- Data for Name: user_group; Type: TABLE DATA; Schema: public; Owner: rs_master\n--\n\nCOPY public.user_group (id, \"createdDate\", \"updatedDate\", name, users, roles) FROM stdin;\n1\t2023-02-02 14:53:58.798684\t2023-02-02 14:53:58.798684\tdementors\t{2098,2103,5481}\t{dementor}\n\\.\n\n\n--\n-- Name: alert_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.alert_id_seq', 1, false);\n\n\n--\n-- Name: certificate_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.certificate_id_seq', 590, true);\n\n\n--\n-- Name: consent_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.consent_id_seq', 1, false);\n\n\n--\n-- Name: contributor_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.contributor_id_seq', 1, false);\n\n\n--\n-- Name: course_event_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.course_event_id_seq', 133, true);\n\n\n--\n-- Name: course_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.course_id_seq', 25, true);\n\n\n--\n-- Name: course_manager_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.course_manager_id_seq', 30, true);\n\n\n--\n-- Name: course_task_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.course_task_id_seq', 432, true);\n\n\n--\n-- Name: course_user_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.course_user_id_seq', 125, true);\n\n\n--\n-- Name: cv_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.cv_id_seq', 1, false);\n\n\n--\n-- Name: discipline_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.discipline_id_seq', 4, true);\n\n\n--\n-- Name: discord_server_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.discord_server_id_seq', 2, true);\n\n\n--\n-- Name: event_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.event_id_seq', 224, true);\n\n\n--\n-- Name: feedback_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.feedback_id_seq', 615, true);\n\n\n--\n-- Name: history_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.history_id_seq', 6, true);\n\n\n--\n-- Name: interview_question_category_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.interview_question_category_id_seq', 1, false);\n\n\n--\n-- Name: interview_question_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.interview_question_id_seq', 1, false);\n\n\n--\n-- Name: mentor_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.mentor_id_seq', 1276, true);\n\n\n--\n-- Name: mentor_registry_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.mentor_registry_id_seq', 290, true);\n\n\n--\n-- Name: migrations_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.migrations_id_seq', 62, true);\n\n\n--\n-- Name: private_feedback_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.private_feedback_id_seq', 65, true);\n\n\n--\n-- Name: profile_permissions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.profile_permissions_id_seq', 115, true);\n\n\n--\n-- Name: prompt_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.prompt_id_seq', 1, false);\n\n\n--\n-- Name: registry_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.registry_id_seq', 8955, true);\n\n\n--\n-- Name: repository_event_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.repository_event_id_seq', 1, false);\n\n\n--\n-- Name: resume_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.resume_id_seq', 1, true);\n\n\n--\n-- Name: stage_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.stage_id_seq', 30, true);\n\n\n--\n-- Name: stage_interview_feedback_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.stage_interview_feedback_id_seq', 1234, true);\n\n\n--\n-- Name: stage_interview_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.stage_interview_id_seq', 10690, true);\n\n\n--\n-- Name: stage_interview_student_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.stage_interview_student_id_seq', 1091, true);\n\n\n--\n-- Name: student_feedback_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.student_feedback_id_seq', 136, true);\n\n\n--\n-- Name: student_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.student_id_seq', 14346, true);\n\n\n--\n-- Name: task_artefact_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_artefact_id_seq', 226, true);\n\n\n--\n-- Name: task_checker_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_checker_id_seq', 4149, true);\n\n\n--\n-- Name: task_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_id_seq', 499, true);\n\n\n--\n-- Name: task_interview_result_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_interview_result_id_seq', 627, true);\n\n\n--\n-- Name: task_interview_student_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_interview_student_id_seq', 1, false);\n\n\n--\n-- Name: task_result_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_result_id_seq', 78651, true);\n\n\n--\n-- Name: task_solution_checker_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_solution_checker_id_seq', 11577, true);\n\n\n--\n-- Name: task_solution_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_solution_id_seq', 3339, true);\n\n\n--\n-- Name: task_solution_result_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_solution_result_id_seq', 10812, true);\n\n\n--\n-- Name: task_verification_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.task_verification_id_seq', 55459, true);\n\n\n--\n-- Name: team_distribution_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.team_distribution_id_seq', 1, false);\n\n\n--\n-- Name: team_distribution_student_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.team_distribution_student_id_seq', 1, false);\n\n\n--\n-- Name: team_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.team_id_seq', 1, false);\n\n\n--\n-- Name: user_group_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.user_group_id_seq', 1, true);\n\n\n--\n-- Name: user_id_seq; Type: SEQUENCE SET; Schema: public; Owner: rs_master\n--\n\nSELECT pg_catalog.setval('public.user_id_seq', 11569, true);\n\n\n--\n-- Name: interview_question_category PK_023f8ae4bea4330f21df438399c; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question_category\n    ADD CONSTRAINT \"PK_023f8ae4bea4330f21df438399c\" PRIMARY KEY (id);\n\n\n--\n-- Name: interview_question_categories_interview_question_category PK_0557624b272acc8d39463763be1; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question_categories_interview_question_category\n    ADD CONSTRAINT \"PK_0557624b272acc8d39463763be1\" PRIMARY KEY (\"interviewQuestionId\", \"interviewQuestionCategoryId\");\n\n\n--\n-- Name: stage_interview PK_06a48c907e0091d4082cfb003aa; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview\n    ADD CONSTRAINT \"PK_06a48c907e0091d4082cfb003aa\" PRIMARY KEY (id);\n\n\n--\n-- Name: discipline PK_139512aefbb11a5b2fa92696828; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.discipline\n    ADD CONSTRAINT \"PK_139512aefbb11a5b2fa92696828\" PRIMARY KEY (id);\n\n\n--\n-- Name: private_feedback PK_14f0f39ae69058ce456dbd0d77f; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.private_feedback\n    ADD CONSTRAINT \"PK_14f0f39ae69058ce456dbd0d77f\" PRIMARY KEY (id);\n\n\n--\n-- Name: registry PK_2eca29d55a9556d854416df8ce5; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.registry\n    ADD CONSTRAINT \"PK_2eca29d55a9556d854416df8ce5\" PRIMARY KEY (id);\n\n\n--\n-- Name: event PK_30c2f3bbaf6d34a55f8ae6e4614; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.event\n    ADD CONSTRAINT \"PK_30c2f3bbaf6d34a55f8ae6e4614\" PRIMARY KEY (id);\n\n\n--\n-- Name: mentor_registry PK_3673050147cd9bc5c73d27512e3; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor_registry\n    ADD CONSTRAINT \"PK_3673050147cd9bc5c73d27512e3\" PRIMARY KEY (id);\n\n\n--\n-- Name: user_group PK_3c29fba6fe013ec8724378ce7c9; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.user_group\n    ADD CONSTRAINT \"PK_3c29fba6fe013ec8724378ce7c9\" PRIMARY KEY (id);\n\n\n--\n-- Name: student PK_3d8016e1cb58429474a3c041904; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student\n    ADD CONSTRAINT \"PK_3d8016e1cb58429474a3c041904\" PRIMARY KEY (id);\n\n\n--\n-- Name: team_distribution PK_432a4b1c8bfacae59140f6fcaf8; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution\n    ADD CONSTRAINT \"PK_432a4b1c8bfacae59140f6fcaf8\" PRIMARY KEY (id);\n\n\n--\n-- Name: stage_interview_student PK_43beb2b1cc5778fd367897b92e8; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_student\n    ADD CONSTRAINT \"PK_43beb2b1cc5778fd367897b92e8\" PRIMARY KEY (id);\n\n\n--\n-- Name: task_artefact PK_43bf3d6d2510e22aac59085f0e0; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_artefact\n    ADD CONSTRAINT \"PK_43bf3d6d2510e22aac59085f0e0\" PRIMARY KEY (id);\n\n\n--\n-- Name: cv PK_4ddf7891daf83c3506efa503bb8; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.cv\n    ADD CONSTRAINT \"PK_4ddf7891daf83c3506efa503bb8\" PRIMARY KEY (id);\n\n\n--\n-- Name: task_verification PK_5080be855b9d24b3d8e93ff425b; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_verification\n    ADD CONSTRAINT \"PK_5080be855b9d24b3d8e93ff425b\" PRIMARY KEY (id);\n\n\n--\n-- Name: notification_channel PK_50b36f3daa5dd86f7e707740b23; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_channel\n    ADD CONSTRAINT \"PK_50b36f3daa5dd86f7e707740b23\" PRIMARY KEY (id);\n\n\n--\n-- Name: task_interview_result PK_549c326d1e4b1c5b42eb915fa2f; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_result\n    ADD CONSTRAINT \"PK_549c326d1e4b1c5b42eb915fa2f\" PRIMARY KEY (id);\n\n\n--\n-- Name: course_event PK_55e3af1e9fa10f21fc27fdc0852; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_event\n    ADD CONSTRAINT \"PK_55e3af1e9fa10f21fc27fdc0852\" PRIMARY KEY (id);\n\n\n--\n-- Name: student_teams_team PK_61c7be2163ef7fd885c6d6c8afc; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_teams_team\n    ADD CONSTRAINT \"PK_61c7be2163ef7fd885c6d6c8afc\" PRIMARY KEY (\"studentId\", \"teamId\");\n\n\n--\n-- Name: task_result PK_623dd43986d67c74bad752b37a5; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_result\n    ADD CONSTRAINT \"PK_623dd43986d67c74bad752b37a5\" PRIMARY KEY (id);\n\n\n--\n-- Name: student_feedback PK_62d4a9be66752e38bd228a78223; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_feedback\n    ADD CONSTRAINT \"PK_62d4a9be66752e38bd228a78223\" PRIMARY KEY (id);\n\n\n--\n-- Name: profile_permissions PK_63cefd76c1a42679af47a57eeba; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.profile_permissions\n    ADD CONSTRAINT \"PK_63cefd76c1a42679af47a57eeba\" PRIMARY KEY (id);\n\n\n--\n-- Name: notification_channel_settings PK_6464daee0ff1cf581129618bc8c; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_channel_settings\n    ADD CONSTRAINT \"PK_6464daee0ff1cf581129618bc8c\" PRIMARY KEY (\"notificationId\", \"channelId\");\n\n\n--\n-- Name: task_solution_result PK_676aad5c32840e4c5d04a61300e; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_result\n    ADD CONSTRAINT \"PK_676aad5c32840e4c5d04a61300e\" PRIMARY KEY (id);\n\n\n--\n-- Name: notification_user_settings PK_679cad5ff478ef93af7221fd98f; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_settings\n    ADD CONSTRAINT \"PK_679cad5ff478ef93af7221fd98f\" PRIMARY KEY (\"notificationId\", \"userId\", \"channelId\");\n\n\n--\n-- Name: task_criteria PK_6de018b8a8dbe8845ffe811ad20; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_criteria\n    ADD CONSTRAINT \"PK_6de018b8a8dbe8845ffe811ad20\" PRIMARY KEY (\"taskId\");\n\n\n--\n-- Name: notification PK_705b6c7cdf9b2c2ff7ac7872cb7; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification\n    ADD CONSTRAINT \"PK_705b6c7cdf9b2c2ff7ac7872cb7\" PRIMARY KEY (id);\n\n\n--\n-- Name: login_state PK_73bea2737e9230e18dc8dc1e7f2; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.login_state\n    ADD CONSTRAINT \"PK_73bea2737e9230e18dc8dc1e7f2\" PRIMARY KEY (id);\n\n\n--\n-- Name: task_solution PK_77bdef09a7686521e5bbc8247a9; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution\n    ADD CONSTRAINT \"PK_77bdef09a7686521e5bbc8247a9\" PRIMARY KEY (id);\n\n\n--\n-- Name: stage_interview_feedback PK_7cafd89ce6a6a3789de3912df21; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_feedback\n    ADD CONSTRAINT \"PK_7cafd89ce6a6a3789de3912df21\" PRIMARY KEY (id);\n\n\n--\n-- Name: resume PK_7ff05ea7599e13fac01ac812e48; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.resume\n    ADD CONSTRAINT \"PK_7ff05ea7599e13fac01ac812e48\" PRIMARY KEY (id);\n\n\n--\n-- Name: contributor PK_816afef005b8100becacdeb6e58; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.contributor\n    ADD CONSTRAINT \"PK_816afef005b8100becacdeb6e58\" PRIMARY KEY (id);\n\n\n--\n-- Name: feedback PK_8389f9e087a57689cd5be8b2b13; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.feedback\n    ADD CONSTRAINT \"PK_8389f9e087a57689cd5be8b2b13\" PRIMARY KEY (id);\n\n\n--\n-- Name: repository_event PK_861ff064ff09ee2e5bbae703649; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.repository_event\n    ADD CONSTRAINT \"PK_861ff064ff09ee2e5bbae703649\" PRIMARY KEY (id);\n\n\n--\n-- Name: interview_question PK_87eb879ef299ec607aa30e9bd39; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question\n    ADD CONSTRAINT \"PK_87eb879ef299ec607aa30e9bd39\" PRIMARY KEY (id);\n\n\n--\n-- Name: team_distribution_student PK_8a75ed7468b867aef47a7320188; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution_student\n    ADD CONSTRAINT \"PK_8a75ed7468b867aef47a7320188\" PRIMARY KEY (id);\n\n\n--\n-- Name: migrations PK_8c82d7f526340ab734260ea46be; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.migrations\n    ADD CONSTRAINT \"PK_8c82d7f526340ab734260ea46be\" PRIMARY KEY (id);\n\n\n--\n-- Name: certificate PK_8daddfc65f59e341c2bbc9c9e43; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.certificate\n    ADD CONSTRAINT \"PK_8daddfc65f59e341c2bbc9c9e43\" PRIMARY KEY (id);\n\n\n--\n-- Name: consent PK_9115e8d6b082d4fc46d56134d29; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.consent\n    ADD CONSTRAINT \"PK_9115e8d6b082d4fc46d56134d29\" PRIMARY KEY (id);\n\n\n--\n-- Name: history PK_9384942edf4804b38ca0ee51416; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.history\n    ADD CONSTRAINT \"PK_9384942edf4804b38ca0ee51416\" PRIMARY KEY (id);\n\n\n--\n-- Name: task_checker PK_999186887e14614c7cdf73b176e; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_checker\n    ADD CONSTRAINT \"PK_999186887e14614c7cdf73b176e\" PRIMARY KEY (id);\n\n\n--\n-- Name: mentor PK_9fcebd0a40237e9b6defcbd9d74; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor\n    ADD CONSTRAINT \"PK_9fcebd0a40237e9b6defcbd9d74\" PRIMARY KEY (id);\n\n\n--\n-- Name: discord_server PK_a4db655f3e40126e5eed1769c90; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.discord_server\n    ADD CONSTRAINT \"PK_a4db655f3e40126e5eed1769c90\" PRIMARY KEY (id);\n\n\n--\n-- Name: course_task PK_aba6301a06559588941ae21b70c; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_task\n    ADD CONSTRAINT \"PK_aba6301a06559588941ae21b70c\" PRIMARY KEY (id);\n\n\n--\n-- Name: alert PK_ad91cad659a3536465d564a4b2f; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.alert\n    ADD CONSTRAINT \"PK_ad91cad659a3536465d564a4b2f\" PRIMARY KEY (id);\n\n\n--\n-- Name: course_manager PK_b344e2b90017167035afd591a76; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_manager\n    ADD CONSTRAINT \"PK_b344e2b90017167035afd591a76\" PRIMARY KEY (id);\n\n\n--\n-- Name: course_user PK_bb2c8374d6f04bf9301895d1b33; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_user\n    ADD CONSTRAINT \"PK_bb2c8374d6f04bf9301895d1b33\" PRIMARY KEY (id);\n\n\n--\n-- Name: task_solution_checker PK_bc32b5c4e5fb9602786de86594f; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_checker\n    ADD CONSTRAINT \"PK_bc32b5c4e5fb9602786de86594f\" PRIMARY KEY (id);\n\n\n--\n-- Name: course PK_bf95180dd756fd204fb01ce4916; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course\n    ADD CONSTRAINT \"PK_bf95180dd756fd204fb01ce4916\" PRIMARY KEY (id);\n\n\n--\n-- Name: stage PK_c54d11b3c24a188262844af1612; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage\n    ADD CONSTRAINT \"PK_c54d11b3c24a188262844af1612\" PRIMARY KEY (id);\n\n\n--\n-- Name: user PK_cace4a159ff9f2512dd42373760; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\"\n    ADD CONSTRAINT \"PK_cace4a159ff9f2512dd42373760\" PRIMARY KEY (id);\n\n\n--\n-- Name: student_team_distribution_team_distribution PK_cd9ddb2e8a915b54f5ab2612bc2; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_team_distribution_team_distribution\n    ADD CONSTRAINT \"PK_cd9ddb2e8a915b54f5ab2612bc2\" PRIMARY KEY (\"studentId\", \"teamDistributionId\");\n\n\n--\n-- Name: prompt PK_d8e3aa07a95560a445ad50fb931; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.prompt\n    ADD CONSTRAINT \"PK_d8e3aa07a95560a445ad50fb931\" PRIMARY KEY (id);\n\n\n--\n-- Name: task_interview_student PK_e01dbf882c881571c02d3e59bf2; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_student\n    ADD CONSTRAINT \"PK_e01dbf882c881571c02d3e59bf2\" PRIMARY KEY (id);\n\n\n--\n-- Name: notification_user_connection PK_e7ab7a5154b15417e5ee0e31a3b; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_connection\n    ADD CONSTRAINT \"PK_e7ab7a5154b15417e5ee0e31a3b\" PRIMARY KEY (\"userId\", \"channelId\");\n\n\n--\n-- Name: team PK_f57d8293406df4af348402e4b74; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team\n    ADD CONSTRAINT \"PK_f57d8293406df4af348402e4b74\" PRIMARY KEY (id);\n\n\n--\n-- Name: task PK_fb213f79ee45060ba925ecd576e; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task\n    ADD CONSTRAINT \"PK_fb213f79ee45060ba925ecd576e\" PRIMARY KEY (id);\n\n\n--\n-- Name: contributor REL_16ce6a76e8a6808b976a61db48; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.contributor\n    ADD CONSTRAINT \"REL_16ce6a76e8a6808b976a61db48\" UNIQUE (user_id);\n\n\n--\n-- Name: task_solution UQ_098e2d5fb54138c4a090b2de0e5; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution\n    ADD CONSTRAINT \"UQ_098e2d5fb54138c4a090b2de0e5\" UNIQUE (\"courseTaskId\", \"studentId\");\n\n\n--\n-- Name: user UQ_0d84cc6a830f0e4ebbfcd6381dd; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\"\n    ADD CONSTRAINT \"UQ_0d84cc6a830f0e4ebbfcd6381dd\" UNIQUE (\"githubId\");\n\n\n--\n-- Name: stage_interview_student UQ_16e069fec7420cb8c9bce692360; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_student\n    ADD CONSTRAINT \"UQ_16e069fec7420cb8c9bce692360\" UNIQUE (\"studentId\", \"courseId\");\n\n\n--\n-- Name: profile_permissions UQ_28231d1cb8ceafd42ae9ed45db9; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.profile_permissions\n    ADD CONSTRAINT \"UQ_28231d1cb8ceafd42ae9ed45db9\" UNIQUE (\"userId\");\n\n\n--\n-- Name: course UQ_30d559218724a6d6e0cc4f26b0e; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course\n    ADD CONSTRAINT \"UQ_30d559218724a6d6e0cc4f26b0e\" UNIQUE (name);\n\n\n--\n-- Name: interview_question_category UQ_40c79f8d86c0b762b849c8c0781; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question_category\n    ADD CONSTRAINT \"UQ_40c79f8d86c0b762b849c8c0781\" UNIQUE (name);\n\n\n--\n-- Name: mentor_registry UQ_469871166ea5d53d181d63bba4d; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor_registry\n    ADD CONSTRAINT \"UQ_469871166ea5d53d181d63bba4d\" UNIQUE (\"userId\");\n\n\n--\n-- Name: student UQ_5b59e5fa1772006c44bacf10d4e; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student\n    ADD CONSTRAINT \"UQ_5b59e5fa1772006c44bacf10d4e\" UNIQUE (\"courseId\", \"userId\");\n\n\n--\n-- Name: task_result UQ_7d9b9262cf5403990b21b6b5cd7; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_result\n    ADD CONSTRAINT \"UQ_7d9b9262cf5403990b21b6b5cd7\" UNIQUE (\"courseTaskId\", \"studentId\");\n\n\n--\n-- Name: mentor UQ_86a8c9674f84523385ff741bfc2; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor\n    ADD CONSTRAINT \"UQ_86a8c9674f84523385ff741bfc2\" UNIQUE (\"courseId\", \"userId\");\n\n\n--\n-- Name: course UQ_8a167196d86062fa6abf6f0d546; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course\n    ADD CONSTRAINT \"UQ_8a167196d86062fa6abf6f0d546\" UNIQUE (alias);\n\n\n--\n-- Name: prompt UQ_8b52c9f9bf5ffaba2f772c65456; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.prompt\n    ADD CONSTRAINT \"UQ_8b52c9f9bf5ffaba2f772c65456\" UNIQUE (type);\n\n\n--\n-- Name: task UQ_91f8c79680ddb1486f56128a9d6; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task\n    ADD CONSTRAINT \"UQ_91f8c79680ddb1486f56128a9d6\" UNIQUE (\"criteriaId\");\n\n\n--\n-- Name: task_interview_student UQ_9b70aaee77ce73e847688838e7e; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_student\n    ADD CONSTRAINT \"UQ_9b70aaee77ce73e847688838e7e\" UNIQUE (\"studentId\", \"courseId\", \"courseTaskId\");\n\n\n--\n-- Name: team_distribution_student UQ_a1c39af9e449474f6495b634cd5; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution_student\n    ADD CONSTRAINT \"UQ_a1c39af9e449474f6495b634cd5\" UNIQUE (\"studentId\", \"courseId\", \"teamDistributionId\");\n\n\n--\n-- Name: certificate UQ_a5b1acee8501273d8c777df4bc1; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.certificate\n    ADD CONSTRAINT \"UQ_a5b1acee8501273d8c777df4bc1\" UNIQUE (\"studentId\");\n\n\n--\n-- Name: consent UQ_a85c68db612327cf60a0d0e7b4a; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.consent\n    ADD CONSTRAINT \"UQ_a85c68db612327cf60a0d0e7b4a\" UNIQUE (\"channelValue\");\n\n\n--\n-- Name: user UQ_aadfefeabbf834e1bb67c9fec0a; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\"\n    ADD CONSTRAINT \"UQ_aadfefeabbf834e1bb67c9fec0a\" UNIQUE (contributor_id);\n\n\n--\n-- Name: user UQ_afa885683cae0bb53ae1c81bce5; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\"\n    ADD CONSTRAINT \"UQ_afa885683cae0bb53ae1c81bce5\" UNIQUE (\"profilePermissionsId\");\n\n\n--\n-- Name: user UQ_bbaf6a936b2124dc6448ba3448f; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\"\n    ADD CONSTRAINT \"UQ_bbaf6a936b2124dc6448ba3448f\" UNIQUE (\"providerUserId\", provider);\n\n\n--\n-- Name: notification_user_connection UQ_c1665f13b0eb372fcb8d48ccf6a; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_connection\n    ADD CONSTRAINT \"UQ_c1665f13b0eb372fcb8d48ccf6a\" UNIQUE (\"userId\", \"channelId\", \"externalId\");\n\n\n--\n-- Name: task_solution_result UQ_cd11c253afeee499efe93f3e184; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_result\n    ADD CONSTRAINT \"UQ_cd11c253afeee499efe93f3e184\" UNIQUE (\"courseTaskId\", \"studentId\", \"checkerId\");\n\n\n--\n-- Name: cv UQ_f21b478fe949f06e4e64d728318; Type: CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.cv\n    ADD CONSTRAINT \"UQ_f21b478fe949f06e4e64d728318\" UNIQUE (\"githubId\");\n\n\n--\n-- Name: IDX_062e03d78da22a7bd9becbfaaa; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_062e03d78da22a7bd9becbfaaa\" ON public.course_user USING btree (\"userId\");\n\n\n--\n-- Name: IDX_06facda60b88268da22c37ddec; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_06facda60b88268da22c37ddec\" ON public.login_state USING btree (\"createdDate\");\n\n\n--\n-- Name: IDX_076f71901ba479a51b2deaacd5; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_076f71901ba479a51b2deaacd5\" ON public.repository_event USING btree (\"repositoryUrl\");\n\n\n--\n-- Name: IDX_07a7e2f79cde1c82b5be2f4716; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_07a7e2f79cde1c82b5be2f4716\" ON public.notification USING btree (enabled);\n\n\n--\n-- Name: IDX_0b3c9d5127523db43a8c4997f5; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_0b3c9d5127523db43a8c4997f5\" ON public.interview_question_categories_interview_question_category USING btree (\"interviewQuestionId\");\n\n\n--\n-- Name: IDX_0d29e2a35a0c87dc9377411f43; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_0d29e2a35a0c87dc9377411f43\" ON public.student USING btree (\"mentorId\");\n\n\n--\n-- Name: IDX_0d84cc6a830f0e4ebbfcd6381d; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE UNIQUE INDEX \"IDX_0d84cc6a830f0e4ebbfcd6381d\" ON public.\"user\" USING btree (\"githubId\");\n\n\n--\n-- Name: IDX_115efaf0e1569ebe8a201f000e; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_115efaf0e1569ebe8a201f000e\" ON public.task_solution_checker USING btree (\"taskSolutionId\");\n\n\n--\n-- Name: IDX_12380a77f5769e0b608b4c5ece; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_12380a77f5769e0b608b4c5ece\" ON public.task_solution_checker USING btree (\"courseTaskId\");\n\n\n--\n-- Name: IDX_16ce6a76e8a6808b976a61db48; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_16ce6a76e8a6808b976a61db48\" ON public.contributor USING btree (user_id);\n\n\n--\n-- Name: IDX_1a6e36b16de159653a4fd2f432; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_1a6e36b16de159653a4fd2f432\" ON public.course_task USING btree (\"courseId\");\n\n\n--\n-- Name: IDX_1c6a31a1098e0c472c4196f85d; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_1c6a31a1098e0c472c4196f85d\" ON public.course USING btree (\"discordServerId\");\n\n\n--\n-- Name: IDX_277a1b8395fd2896391b01b761; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_277a1b8395fd2896391b01b761\" ON public.interview_question_categories_interview_question_category USING btree (\"interviewQuestionCategoryId\");\n\n\n--\n-- Name: IDX_2e2c071fde8ee3f26724de7e67; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_2e2c071fde8ee3f26724de7e67\" ON public.notification_channel_settings USING btree (\"channelId\");\n\n\n--\n-- Name: IDX_2e4ed1c8264a48ffe7f8547401; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_2e4ed1c8264a48ffe7f8547401\" ON public.stage_interview USING btree (\"studentId\");\n\n\n--\n-- Name: IDX_33cc2ea503287d1e19e696c028; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_33cc2ea503287d1e19e696c028\" ON public.task_interview_result USING btree (\"courseTaskId\");\n\n\n--\n-- Name: IDX_33f33cc8ef29d805a97ff4628b; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_33f33cc8ef29d805a97ff4628b\" ON public.notification USING btree (type);\n\n\n--\n-- Name: IDX_3cf45a981cf54c2b3e10f677c9; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_3cf45a981cf54c2b3e10f677c9\" ON public.course_task USING btree (\"taskId\");\n\n\n--\n-- Name: IDX_46ecfda37a00bdb0eb9853805e; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_46ecfda37a00bdb0eb9853805e\" ON public.student_teams_team USING btree (\"teamId\");\n\n\n--\n-- Name: IDX_4f512b65d2481c2fd737680f79; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_4f512b65d2481c2fd737680f79\" ON public.task_interview_result USING btree (\"mentorId\");\n\n\n--\n-- Name: IDX_50802da9f1d09f275d964dd491; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_50802da9f1d09f275d964dd491\" ON public.notification USING btree (name);\n\n\n--\n-- Name: IDX_5565a1f41896ecd29591b239ef; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_5565a1f41896ecd29591b239ef\" ON public.task_result USING btree (\"studentId\");\n\n\n--\n-- Name: IDX_5d15876da767ed2eef032144ca; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_5d15876da767ed2eef032144ca\" ON public.student_team_distribution_team_distribution USING btree (\"studentId\");\n\n\n--\n-- Name: IDX_5fbd9182fe89b2417f288c61f9; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_5fbd9182fe89b2417f288c61f9\" ON public.student_teams_team USING btree (\"studentId\");\n\n\n--\n-- Name: IDX_600ad506d38c98395590e76ea1; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_600ad506d38c98395590e76ea1\" ON public.student_feedback USING btree (student_id);\n\n\n--\n-- Name: IDX_6543e24d4d8714017acd1a1b39; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_6543e24d4d8714017acd1a1b39\" ON public.resume USING btree (\"userId\");\n\n\n--\n-- Name: IDX_70824fef35e6038e459e58e035; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_70824fef35e6038e459e58e035\" ON public.course_user USING btree (\"courseId\");\n\n\n--\n-- Name: IDX_773a8c01eb6d281590cdbcaabd; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_773a8c01eb6d281590cdbcaabd\" ON public.notification_channel_settings USING btree (\"notificationId\");\n\n\n--\n-- Name: IDX_79279baf9c5c6e3fb9baabbb5b; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_79279baf9c5c6e3fb9baabbb5b\" ON public.team USING btree (\"teamDistributionId\");\n\n\n--\n-- Name: IDX_79b102f1b191c731920e2ea486; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_79b102f1b191c731920e2ea486\" ON public.login_state USING btree (\"userId\");\n\n\n--\n-- Name: IDX_80735bc019562abb4e7099340e; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_80735bc019562abb4e7099340e\" ON public.history USING btree (event);\n\n\n--\n-- Name: IDX_85a40b3dcc11dcfdfb836b7ff3; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_85a40b3dcc11dcfdfb836b7ff3\" ON public.task_solution_checker USING btree (\"checkerId\");\n\n\n--\n-- Name: IDX_87736b09d69bacdc6bc272e023; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_87736b09d69bacdc6bc272e023\" ON public.course_task USING btree (\"taskOwnerId\");\n\n\n--\n-- Name: IDX_87c5a426accd8659ac76e8d3fb; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_87c5a426accd8659ac76e8d3fb\" ON public.course_task USING btree (disabled);\n\n\n--\n-- Name: IDX_8a167196d86062fa6abf6f0d54; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_8a167196d86062fa6abf6f0d54\" ON public.course USING btree (alias);\n\n\n--\n-- Name: IDX_951e2b89c3a2b4554516409cfb; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_951e2b89c3a2b4554516409cfb\" ON public.team_distribution USING btree (\"courseId\");\n\n\n--\n-- Name: IDX_955719ac67b6cb47bf005b200e; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_955719ac67b6cb47bf005b200e\" ON public.repository_event USING btree (\"githubId\");\n\n\n--\n-- Name: IDX_9d0edea65b297ba0d7d8064d05; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_9d0edea65b297ba0d7d8064d05\" ON public.task_interview_result USING btree (\"studentId\");\n\n\n--\n-- Name: IDX_a29d066e554ba135f0d9408c1b; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_a29d066e554ba135f0d9408c1b\" ON public.student USING btree (\"courseId\");\n\n\n--\n-- Name: IDX_a619e6e10deb16bf6519d204cf; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_a619e6e10deb16bf6519d204cf\" ON public.history USING btree (\"updatedDate\");\n\n\n--\n-- Name: IDX_a745cd57c268bf3728acbcfccb; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_a745cd57c268bf3728acbcfccb\" ON public.notification_user_settings USING btree (\"channelId\");\n\n\n--\n-- Name: IDX_a939c4402f9eb96a7c2b9b5663; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_a939c4402f9eb96a7c2b9b5663\" ON public.student_team_distribution_team_distribution USING btree (\"teamDistributionId\");\n\n\n--\n-- Name: IDX_adba43a9054da3ee83e6531d7d; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_adba43a9054da3ee83e6531d7d\" ON public.student_feedback USING btree (mentor_id);\n\n\n--\n-- Name: IDX_b35463776b4a11a3df3c30d920; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_b35463776b4a11a3df3c30d920\" ON public.student USING btree (\"userId\");\n\n\n--\n-- Name: IDX_b74f71762142b09ea10a288166; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_b74f71762142b09ea10a288166\" ON public.task_solution_result USING btree (\"studentId\");\n\n\n--\n-- Name: IDX_bdb2f3421163e324b337395909; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_bdb2f3421163e324b337395909\" ON public.task_solution_result USING btree (\"courseTaskId\");\n\n\n--\n-- Name: IDX_d0a655e0bd36811dc5e74a1b64; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_d0a655e0bd36811dc5e74a1b64\" ON public.task_verification USING btree (\"updatedDate\");\n\n\n--\n-- Name: IDX_d0b6bedfc9eb1243b01facefe1; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_d0b6bedfc9eb1243b01facefe1\" ON public.notification_user_settings USING btree (\"notificationId\", \"userId\");\n\n\n--\n-- Name: IDX_d2236f176c9281802d3ff00d3f; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_d2236f176c9281802d3ff00d3f\" ON public.login_state USING btree (expires);\n\n\n--\n-- Name: IDX_d223b6ab8859d668ab080c3628; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_d223b6ab8859d668ab080c3628\" ON public.\"user\" USING btree (\"providerUserId\");\n\n\n--\n-- Name: IDX_d8959fe22a43ff7773b3640992; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_d8959fe22a43ff7773b3640992\" ON public.task_verification USING btree (\"studentId\");\n\n\n--\n-- Name: IDX_dae85baef040e0c3eaf1794ff6; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_dae85baef040e0c3eaf1794ff6\" ON public.task_verification USING btree (\"courseTaskId\");\n\n\n--\n-- Name: IDX_db66372bf51271337293b341bf; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_db66372bf51271337293b341bf\" ON public.stage_interview USING btree (\"mentorId\");\n\n\n--\n-- Name: IDX_de17ec9312951a05365d5d4d25; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_de17ec9312951a05365d5d4d25\" ON public.course_task USING btree (checker);\n\n\n--\n-- Name: IDX_e0c522b2cdf095ad5c5f51c0ae; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_e0c522b2cdf095ad5c5f51c0ae\" ON public.task_result USING btree (\"courseTaskId\");\n\n\n--\n-- Name: IDX_e848fe0c47f23605364a5f163f; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_e848fe0c47f23605364a5f163f\" ON public.student USING btree (\"isFailed\");\n\n\n--\n-- Name: IDX_e8aaf4d079a719ade8ebc1397e; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_e8aaf4d079a719ade8ebc1397e\" ON public.task_solution_result USING btree (\"checkerId\");\n\n\n--\n-- Name: IDX_ee6434baa5d6a66edf5c8fa122; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_ee6434baa5d6a66edf5c8fa122\" ON public.resume USING btree (\"githubId\");\n\n\n--\n-- Name: IDX_f277c5f942b6421c4e02e4b959; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_f277c5f942b6421c4e02e4b959\" ON public.student USING btree (\"isExpelled\");\n\n\n--\n-- Name: IDX_f3dfd194e3463dc94600921378; Type: INDEX; Schema: public; Owner: rs_master\n--\n\nCREATE INDEX \"IDX_f3dfd194e3463dc94600921378\" ON public.mentor USING btree (\"courseId\");\n\n\n--\n-- Name: task_solution FK_04a0e8cec45008def71698916ae; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution\n    ADD CONSTRAINT \"FK_04a0e8cec45008def71698916ae\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: course_user FK_062e03d78da22a7bd9becbfaaac; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_user\n    ADD CONSTRAINT \"FK_062e03d78da22a7bd9becbfaaac\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: interview_question_categories_interview_question_category FK_0b3c9d5127523db43a8c4997f59; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question_categories_interview_question_category\n    ADD CONSTRAINT \"FK_0b3c9d5127523db43a8c4997f59\" FOREIGN KEY (\"interviewQuestionId\") REFERENCES public.interview_question(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: student FK_0d29e2a35a0c87dc9377411f432; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student\n    ADD CONSTRAINT \"FK_0d29e2a35a0c87dc9377411f432\" FOREIGN KEY (\"mentorId\") REFERENCES public.mentor(id);\n\n\n--\n-- Name: task_result FK_0d531a05b39c159334a1724e1b0; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_result\n    ADD CONSTRAINT \"FK_0d531a05b39c159334a1724e1b0\" FOREIGN KEY (\"lastCheckerId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: task_solution_checker FK_115efaf0e1569ebe8a201f000e2; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_checker\n    ADD CONSTRAINT \"FK_115efaf0e1569ebe8a201f000e2\" FOREIGN KEY (\"taskSolutionId\") REFERENCES public.task_solution(id);\n\n\n--\n-- Name: private_feedback FK_1448716050d6c839a198a199ddb; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.private_feedback\n    ADD CONSTRAINT \"FK_1448716050d6c839a198a199ddb\" FOREIGN KEY (\"fromUserId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: stage FK_16bd843ee63aeb303b35e288960; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage\n    ADD CONSTRAINT \"FK_16bd843ee63aeb303b35e288960\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: contributor FK_16ce6a76e8a6808b976a61db487; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.contributor\n    ADD CONSTRAINT \"FK_16ce6a76e8a6808b976a61db487\" FOREIGN KEY (user_id) REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: course_event FK_18edb72a122ff56bddcaec6055c; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_event\n    ADD CONSTRAINT \"FK_18edb72a122ff56bddcaec6055c\" FOREIGN KEY (\"organizerId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: course_task FK_1a6e36b16de159653a4fd2f4323; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_task\n    ADD CONSTRAINT \"FK_1a6e36b16de159653a4fd2f4323\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: course FK_1c6a31a1098e0c472c4196f85d8; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course\n    ADD CONSTRAINT \"FK_1c6a31a1098e0c472c4196f85d8\" FOREIGN KEY (\"discordServerId\") REFERENCES public.discord_server(id);\n\n\n--\n-- Name: registry FK_2449b2493e4b436fda3c21ba5df; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.registry\n    ADD CONSTRAINT \"FK_2449b2493e4b436fda3c21ba5df\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: interview_question_categories_interview_question_category FK_277a1b8395fd2896391b01b7612; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.interview_question_categories_interview_question_category\n    ADD CONSTRAINT \"FK_277a1b8395fd2896391b01b7612\" FOREIGN KEY (\"interviewQuestionCategoryId\") REFERENCES public.interview_question_category(id);\n\n\n--\n-- Name: feedback FK_2b4d98c492a3965505cf57e2e8a; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.feedback\n    ADD CONSTRAINT \"FK_2b4d98c492a3965505cf57e2e8a\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: notification_channel_settings FK_2e2c071fde8ee3f26724de7e678; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_channel_settings\n    ADD CONSTRAINT \"FK_2e2c071fde8ee3f26724de7e678\" FOREIGN KEY (\"channelId\") REFERENCES public.notification_channel(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: stage_interview FK_2e4ed1c8264a48ffe7f85474018; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview\n    ADD CONSTRAINT \"FK_2e4ed1c8264a48ffe7f85474018\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: task_interview_result FK_33cc2ea503287d1e19e696c0280; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_result\n    ADD CONSTRAINT \"FK_33cc2ea503287d1e19e696c0280\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: course_task FK_3cf45a981cf54c2b3e10f677c95; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_task\n    ADD CONSTRAINT \"FK_3cf45a981cf54c2b3e10f677c95\" FOREIGN KEY (\"taskId\") REFERENCES public.task(id);\n\n\n--\n-- Name: private_feedback FK_43900d7df69f46dd5c7a44d0c80; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.private_feedback\n    ADD CONSTRAINT \"FK_43900d7df69f46dd5c7a44d0c80\" FOREIGN KEY (\"toUserId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: mentor_registry FK_469871166ea5d53d181d63bba4d; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor_registry\n    ADD CONSTRAINT \"FK_469871166ea5d53d181d63bba4d\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: student_teams_team FK_46ecfda37a00bdb0eb9853805e3; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_teams_team\n    ADD CONSTRAINT \"FK_46ecfda37a00bdb0eb9853805e3\" FOREIGN KEY (\"teamId\") REFERENCES public.team(id);\n\n\n--\n-- Name: task_interview_result FK_4f512b65d2481c2fd737680f791; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_result\n    ADD CONSTRAINT \"FK_4f512b65d2481c2fd737680f791\" FOREIGN KEY (\"mentorId\") REFERENCES public.mentor(id);\n\n\n--\n-- Name: course_manager FK_51727e0e86522ee68c1d7ab556f; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_manager\n    ADD CONSTRAINT \"FK_51727e0e86522ee68c1d7ab556f\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: task_checker FK_520bd8f9d4ae3b18430899c4490; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_checker\n    ADD CONSTRAINT \"FK_520bd8f9d4ae3b18430899c4490\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: team_distribution_student FK_552eb86c51b2449e2665ad7be0f; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution_student\n    ADD CONSTRAINT \"FK_552eb86c51b2449e2665ad7be0f\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: task_result FK_5565a1f41896ecd29591b239ef5; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_result\n    ADD CONSTRAINT \"FK_5565a1f41896ecd29591b239ef5\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: task_checker FK_5a95946eb2c610d54379689312d; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_checker\n    ADD CONSTRAINT \"FK_5a95946eb2c610d54379689312d\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: course_event FK_5aa0fd2863ab6cc52828525649c; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_event\n    ADD CONSTRAINT \"FK_5aa0fd2863ab6cc52828525649c\" FOREIGN KEY (\"eventId\") REFERENCES public.event(id);\n\n\n--\n-- Name: team_distribution_student FK_5b0eb057a06b5fafb89edefd358; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution_student\n    ADD CONSTRAINT \"FK_5b0eb057a06b5fafb89edefd358\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: student_team_distribution_team_distribution FK_5d15876da767ed2eef032144caf; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_team_distribution_team_distribution\n    ADD CONSTRAINT \"FK_5d15876da767ed2eef032144caf\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: student_teams_team FK_5fbd9182fe89b2417f288c61f9c; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_teams_team\n    ADD CONSTRAINT \"FK_5fbd9182fe89b2417f288c61f9c\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: student_feedback FK_600ad506d38c98395590e76ea1f; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_feedback\n    ADD CONSTRAINT \"FK_600ad506d38c98395590e76ea1f\" FOREIGN KEY (student_id) REFERENCES public.student(id);\n\n\n--\n-- Name: stage_interview FK_61a1f43cc337dcfd0a267e6f3bc; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview\n    ADD CONSTRAINT \"FK_61a1f43cc337dcfd0a267e6f3bc\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: stage_interview_student FK_61d2e056326504ec484b8ed59e7; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_student\n    ADD CONSTRAINT \"FK_61d2e056326504ec484b8ed59e7\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: resume FK_6543e24d4d8714017acd1a1b392; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.resume\n    ADD CONSTRAINT \"FK_6543e24d4d8714017acd1a1b392\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: notification_user_connection FK_686acb0bbf9634ef2497e87582f; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_connection\n    ADD CONSTRAINT \"FK_686acb0bbf9634ef2497e87582f\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: course_user FK_70824fef35e6038e459e58e0358; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_user\n    ADD CONSTRAINT \"FK_70824fef35e6038e459e58e0358\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: notification_channel_settings FK_773a8c01eb6d281590cdbcaabdf; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_channel_settings\n    ADD CONSTRAINT \"FK_773a8c01eb6d281590cdbcaabdf\" FOREIGN KEY (\"notificationId\") REFERENCES public.notification(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: team FK_79279baf9c5c6e3fb9baabbb5bd; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team\n    ADD CONSTRAINT \"FK_79279baf9c5c6e3fb9baabbb5bd\" FOREIGN KEY (\"teamDistributionId\") REFERENCES public.team_distribution(id);\n\n\n--\n-- Name: stage_interview_feedback FK_7b7d891769e42df16686873c3c6; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_feedback\n    ADD CONSTRAINT \"FK_7b7d891769e42df16686873c3c6\" FOREIGN KEY (\"stageInterviewId\") REFERENCES public.stage_interview(id);\n\n\n--\n-- Name: course FK_7dc67e5ff23f9a74b7cb129a088; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course\n    ADD CONSTRAINT \"FK_7dc67e5ff23f9a74b7cb129a088\" FOREIGN KEY (\"disciplineId\") REFERENCES public.discipline(id) ON DELETE SET NULL;\n\n\n--\n-- Name: private_feedback FK_7f6ab332685af8fa4239d8e04e5; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.private_feedback\n    ADD CONSTRAINT \"FK_7f6ab332685af8fa4239d8e04e5\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: task_solution_checker FK_85a40b3dcc11dcfdfb836b7ff3e; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_checker\n    ADD CONSTRAINT \"FK_85a40b3dcc11dcfdfb836b7ff3e\" FOREIGN KEY (\"checkerId\") REFERENCES public.student(id);\n\n\n--\n-- Name: event FK_868c8f954dd31217a7e0981b1d2; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.event\n    ADD CONSTRAINT \"FK_868c8f954dd31217a7e0981b1d2\" FOREIGN KEY (\"disciplineId\") REFERENCES public.discipline(id) ON DELETE SET NULL;\n\n\n--\n-- Name: notification_user_settings FK_8704ffbe765e552c633f5c96588; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_settings\n    ADD CONSTRAINT \"FK_8704ffbe765e552c633f5c96588\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: course_task FK_87736b09d69bacdc6bc272e0239; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_task\n    ADD CONSTRAINT \"FK_87736b09d69bacdc6bc272e0239\" FOREIGN KEY (\"taskOwnerId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: notification_user_connection FK_8cefc11aa24ba4e51162685196d; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_connection\n    ADD CONSTRAINT \"FK_8cefc11aa24ba4e51162685196d\" FOREIGN KEY (\"channelId\") REFERENCES public.notification_channel(id) ON UPDATE CASCADE;\n\n\n--\n-- Name: task FK_91f8c79680ddb1486f56128a9d6; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task\n    ADD CONSTRAINT \"FK_91f8c79680ddb1486f56128a9d6\" FOREIGN KEY (\"criteriaId\") REFERENCES public.task_criteria(\"taskId\") ON DELETE CASCADE;\n\n\n--\n-- Name: team_distribution_student FK_92af6f1f2345cb39398cea4748a; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution_student\n    ADD CONSTRAINT \"FK_92af6f1f2345cb39398cea4748a\" FOREIGN KEY (\"teamDistributionId\") REFERENCES public.team_distribution(id);\n\n\n--\n-- Name: team_distribution FK_951e2b89c3a2b4554516409cfbd; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.team_distribution\n    ADD CONSTRAINT \"FK_951e2b89c3a2b4554516409cfbd\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: task_interview_result FK_9d0edea65b297ba0d7d8064d05a; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_result\n    ADD CONSTRAINT \"FK_9d0edea65b297ba0d7d8064d05a\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: task FK_9e32af93bbf4f4dcf66387b3073; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task\n    ADD CONSTRAINT \"FK_9e32af93bbf4f4dcf66387b3073\" FOREIGN KEY (\"disciplineId\") REFERENCES public.discipline(id) ON DELETE SET NULL;\n\n\n--\n-- Name: registry FK_a19cc98b348420faa739dfd4240; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.registry\n    ADD CONSTRAINT \"FK_a19cc98b348420faa739dfd4240\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: student FK_a29d066e554ba135f0d9408c1b3; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student\n    ADD CONSTRAINT \"FK_a29d066e554ba135f0d9408c1b3\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: certificate FK_a5b1acee8501273d8c777df4bc1; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.certificate\n    ADD CONSTRAINT \"FK_a5b1acee8501273d8c777df4bc1\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: notification_user_settings FK_a745cd57c268bf3728acbcfccb1; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_settings\n    ADD CONSTRAINT \"FK_a745cd57c268bf3728acbcfccb1\" FOREIGN KEY (\"channelId\") REFERENCES public.notification_channel(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: student_team_distribution_team_distribution FK_a939c4402f9eb96a7c2b9b56634; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_team_distribution_team_distribution\n    ADD CONSTRAINT \"FK_a939c4402f9eb96a7c2b9b56634\" FOREIGN KEY (\"teamDistributionId\") REFERENCES public.team_distribution(id);\n\n\n--\n-- Name: user FK_aadfefeabbf834e1bb67c9fec0a; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\"\n    ADD CONSTRAINT \"FK_aadfefeabbf834e1bb67c9fec0a\" FOREIGN KEY (contributor_id) REFERENCES public.contributor(id);\n\n\n--\n-- Name: student_feedback FK_adba43a9054da3ee83e6531d7da; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_feedback\n    ADD CONSTRAINT \"FK_adba43a9054da3ee83e6531d7da\" FOREIGN KEY (mentor_id) REFERENCES public.mentor(id);\n\n\n--\n-- Name: user FK_afa885683cae0bb53ae1c81bce5; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.\"user\"\n    ADD CONSTRAINT \"FK_afa885683cae0bb53ae1c81bce5\" FOREIGN KEY (\"profilePermissionsId\") REFERENCES public.profile_permissions(id);\n\n\n--\n-- Name: student FK_b35463776b4a11a3df3c30d920a; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student\n    ADD CONSTRAINT \"FK_b35463776b4a11a3df3c30d920a\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: notification FK_b7386b61afc53e6b82251e41b5c; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification\n    ADD CONSTRAINT \"FK_b7386b61afc53e6b82251e41b5c\" FOREIGN KEY (\"parentId\") REFERENCES public.notification(id);\n\n\n--\n-- Name: task_solution_result FK_b74f71762142b09ea10a2881669; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_result\n    ADD CONSTRAINT \"FK_b74f71762142b09ea10a2881669\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: task_solution_result FK_bdb2f3421163e324b337395909e; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_result\n    ADD CONSTRAINT \"FK_bdb2f3421163e324b337395909e\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: feedback FK_bfea5673b7379b1adfa2036da3f; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.feedback\n    ADD CONSTRAINT \"FK_bfea5673b7379b1adfa2036da3f\" FOREIGN KEY (\"fromUserId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: task_checker FK_c8594a64515d69f4dae0da90006; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_checker\n    ADD CONSTRAINT \"FK_c8594a64515d69f4dae0da90006\" FOREIGN KEY (\"mentorId\") REFERENCES public.mentor(id);\n\n\n--\n-- Name: notification_user_settings FK_d58ed9fef5ec0b2875892cda12f; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.notification_user_settings\n    ADD CONSTRAINT \"FK_d58ed9fef5ec0b2875892cda12f\" FOREIGN KEY (\"notificationId\") REFERENCES public.notification(id) ON UPDATE CASCADE ON DELETE CASCADE;\n\n\n--\n-- Name: task_artefact FK_d79f770bf46cd7659b6e5dda1c1; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_artefact\n    ADD CONSTRAINT \"FK_d79f770bf46cd7659b6e5dda1c1\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: task_verification FK_d8959fe22a43ff7773b36409924; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_verification\n    ADD CONSTRAINT \"FK_d8959fe22a43ff7773b36409924\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: course_manager FK_d937cb10a6bf6cc8574046bb716; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_manager\n    ADD CONSTRAINT \"FK_d937cb10a6bf6cc8574046bb716\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: task_interview_student FK_da5613e78890f0093805a441c92; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_student\n    ADD CONSTRAINT \"FK_da5613e78890f0093805a441c92\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: task_verification FK_dae85baef040e0c3eaf1794ff6d; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_verification\n    ADD CONSTRAINT \"FK_dae85baef040e0c3eaf1794ff6d\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: stage_interview FK_db66372bf51271337293b341bf4; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview\n    ADD CONSTRAINT \"FK_db66372bf51271337293b341bf4\" FOREIGN KEY (\"mentorId\") REFERENCES public.mentor(id);\n\n\n--\n-- Name: task_interview_student FK_dc9248f22e6b30f63e7afa4f218; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_student\n    ADD CONSTRAINT \"FK_dc9248f22e6b30f63e7afa4f218\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: mentor FK_df4bfe54f243bd089ea8fb66ed0; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor\n    ADD CONSTRAINT \"FK_df4bfe54f243bd089ea8fb66ed0\" FOREIGN KEY (\"userId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: task_result FK_e0c522b2cdf095ad5c5f51c0ae0; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_result\n    ADD CONSTRAINT \"FK_e0c522b2cdf095ad5c5f51c0ae0\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: task_solution FK_e2487265adac81bea6f085d2fa0; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution\n    ADD CONSTRAINT \"FK_e2487265adac81bea6f085d2fa0\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: stage_interview_student FK_e59f3cbfd1cf52fddf905fc8dea; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview_student\n    ADD CONSTRAINT \"FK_e59f3cbfd1cf52fddf905fc8dea\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: task_artefact FK_e683ee274bcf6363c043a29f535; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_artefact\n    ADD CONSTRAINT \"FK_e683ee274bcf6363c043a29f535\" FOREIGN KEY (\"courseTaskId\") REFERENCES public.course_task(id);\n\n\n--\n-- Name: task_solution_result FK_e8aaf4d079a719ade8ebc1397ef; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_result\n    ADD CONSTRAINT \"FK_e8aaf4d079a719ade8ebc1397ef\" FOREIGN KEY (\"checkerId\") REFERENCES public.student(id);\n\n\n--\n-- Name: task_solution_checker FK_ee4c145a114a9ada3ec1be0f936; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_solution_checker\n    ADD CONSTRAINT \"FK_ee4c145a114a9ada3ec1be0f936\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: stage_interview FK_f08ecdf6dd22870ac34cbacff51; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.stage_interview\n    ADD CONSTRAINT \"FK_f08ecdf6dd22870ac34cbacff51\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: student_feedback FK_f133ab9aba2bb7c28da9a93351d; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.student_feedback\n    ADD CONSTRAINT \"FK_f133ab9aba2bb7c28da9a93351d\" FOREIGN KEY (author_id) REFERENCES public.\"user\"(id);\n\n\n--\n-- Name: task_interview_student FK_f348c327bf727d9de3acd7b4b49; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.task_interview_student\n    ADD CONSTRAINT \"FK_f348c327bf727d9de3acd7b4b49\" FOREIGN KEY (\"studentId\") REFERENCES public.student(id);\n\n\n--\n-- Name: mentor FK_f3dfd194e3463dc946009213782; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.mentor\n    ADD CONSTRAINT \"FK_f3dfd194e3463dc946009213782\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: course_task FK_f45fe9bce062ecb8f59edf079e8; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_task\n    ADD CONSTRAINT \"FK_f45fe9bce062ecb8f59edf079e8\" FOREIGN KEY (\"teamDistributionId\") REFERENCES public.team_distribution(id);\n\n\n--\n-- Name: course_event FK_f736d0c55020fc4e5eb28634316; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.course_event\n    ADD CONSTRAINT \"FK_f736d0c55020fc4e5eb28634316\" FOREIGN KEY (\"courseId\") REFERENCES public.course(id);\n\n\n--\n-- Name: feedback FK_fefc350f416e262e904dcf6b35e; Type: FK CONSTRAINT; Schema: public; Owner: rs_master\n--\n\nALTER TABLE ONLY public.feedback\n    ADD CONSTRAINT \"FK_fefc350f416e262e904dcf6b35e\" FOREIGN KEY (\"toUserId\") REFERENCES public.\"user\"(id);\n\n\n--\n-- PostgreSQL database dump complete\n--\n\n"
  },
  {
    "path": "setup/cdk/App.ts",
    "content": "import * as cdk from 'aws-cdk-lib';\nimport { RsSchoolAppStack } from './Stack';\n\nconst app = new cdk.App();\n\nconst feature = app.node.tryGetContext('feature') ?? 'master';\nconst deployId = app.node.tryGetContext('deployId') ?? new Date().getTime().toString();\n\nnew RsSchoolAppStack(app, `rsschool-app-${feature}`, {\n  env: { account: '511361162520', region: 'eu-central-1' },\n  feature,\n  deployId,\n  certificateArn: 'arn:aws:acm:us-east-1:511361162520:certificate/07e01035-1bb4-430c-8b82-625565f66bdb',\n});\n"
  },
  {
    "path": "setup/cdk/DockerFunctionConstruct.ts",
    "content": "import * as cdk from 'aws-cdk-lib';\nimport * as apiv2 from 'aws-cdk-lib/aws-apigatewayv2';\nimport { HttpLambdaIntegration } from 'aws-cdk-lib/aws-apigatewayv2-integrations';\nimport { IRepository } from 'aws-cdk-lib/aws-ecr';\nimport * as iam from 'aws-cdk-lib/aws-iam';\nimport * as lambda from 'aws-cdk-lib/aws-lambda';\nimport * as logs from 'aws-cdk-lib/aws-logs';\nimport * as custom from 'aws-cdk-lib/custom-resources';\nimport { Construct } from 'constructs';\n\nexport type DockerFunctionProps = cdk.StackProps & {\n  feature: string;\n  deployId: string;\n  variables?: Record<string, string>;\n  repository: IRepository;\n  httpApi: apiv2.HttpApi;\n  basePath?: string;\n  memorySize?: number;\n};\n\nexport class DockerFunction extends Construct {\n  public domainName: string;\n\n  private readonly defaultBasePath = '/{proxy+}';\n\n  constructor(scope: Construct, id: string, props: DockerFunctionProps) {\n    super(scope, id);\n    const { feature, variables, httpApi, basePath, deployId, memorySize } = props;\n\n    const tag = feature;\n    const logGroup = new logs.LogGroup(this, 'LogGroup', {\n      retention: logs.RetentionDays.TWO_WEEKS,\n      removalPolicy: cdk.RemovalPolicy.DESTROY,\n    });\n    const dockerImageFunction = new lambda.DockerImageFunction(this, 'DockerImageFunction', {\n      description: tag,\n      code: lambda.DockerImageCode.fromEcr(props.repository, { tagOrDigest: tag }),\n      memorySize: memorySize ?? 1024,\n      timeout: cdk.Duration.seconds(30),\n      environment: variables,\n      logGroup,\n    });\n    const integration = new HttpLambdaIntegration('LambdaIntegration', dockerImageFunction, {\n      payloadFormatVersion: apiv2.PayloadFormatVersion.VERSION_2_0,\n    });\n    httpApi.addRoutes({ path: basePath ?? this.defaultBasePath, integration });\n    const [domainName] = httpApi.url?.replace('https://', '').split('/') ?? [];\n    this.domainName = domainName ?? '';\n\n    const dockerImageUpdater = new custom.AwsCustomResource(this, 'DockerImageUpdater', {\n      installLatestAwsSdk: false,\n      onCreate: {\n        physicalResourceId: custom.PhysicalResourceId.of(feature),\n        service: 'Lambda',\n        action: 'updateFunctionCode',\n        parameters: {\n          FunctionName: dockerImageFunction.functionName,\n          ImageUri: props.repository.repositoryUriForTag(tag),\n        },\n      },\n      onUpdate: {\n        service: 'Lambda',\n        action: 'updateFunctionCode',\n        parameters: {\n          FunctionName: dockerImageFunction.functionName,\n          ImageUri: props.repository.repositoryUriForTag(tag),\n        },\n        physicalResourceId: custom.PhysicalResourceId.of(deployId),\n      },\n      policy: custom.AwsCustomResourcePolicy.fromStatements([\n        new iam.PolicyStatement({\n          actions: ['lambda:UpdateFunctionCode'],\n          effect: iam.Effect.ALLOW,\n          resources: ['*'],\n        }),\n      ]),\n    });\n    dockerImageUpdater.node.addDependency(dockerImageFunction);\n  }\n}\n"
  },
  {
    "path": "setup/cdk/Stack.ts",
    "content": "import * as cdk from 'aws-cdk-lib';\nimport * as apiv2 from 'aws-cdk-lib/aws-apigatewayv2';\nimport { Repository } from 'aws-cdk-lib/aws-ecr';\nimport * as route53 from 'aws-cdk-lib/aws-route53';\nimport * as alias from 'aws-cdk-lib/aws-route53-targets';\n\nimport { CfnOutput } from 'aws-cdk-lib';\nimport {\n  Distribution,\n  OriginRequestPolicy,\n  OriginRequestQueryStringBehavior,\n  OriginRequestHeaderBehavior,\n  OriginRequestCookieBehavior,\n  OriginProtocolPolicy,\n  AllowedMethods,\n  CachePolicy,\n  CacheHeaderBehavior,\n  CacheQueryStringBehavior,\n  CacheCookieBehavior,\n} from 'aws-cdk-lib/aws-cloudfront';\nimport { Construct } from 'constructs';\nimport { DockerFunction } from './DockerFunctionConstruct';\nimport * as origins from 'aws-cdk-lib/aws-cloudfront-origins';\nimport * as acm from 'aws-cdk-lib/aws-certificatemanager';\n\ntype Props = cdk.StackProps & {\n  feature: string;\n  deployId: string;\n  certificateArn: string;\n};\n\nexport class RsSchoolAppStack extends cdk.Stack {\n  fqdn: string;\n  url: string;\n\n  constructor(scope: Construct, id: string, props: Props) {\n    super(scope, id, props);\n\n    const { feature, certificateArn, deployId } = props;\n\n    this.fqdn = `${feature}.app.rs.school`;\n    this.url = `https://${this.fqdn}`;\n\n    const httpApi = new apiv2.HttpApi(this, 'HttpApi', {\n      apiName: feature,\n    });\n\n    const defaultProps = { feature, deployId, httpApi, memorySize: 4096 };\n\n    const nextApp = new DockerFunction(this, 'Next', {\n      ...defaultProps,\n      repository: Repository.fromRepositoryName(this, 'UiRepository', 'rsschool-ui'),\n      variables: {\n        NODE_ENV: 'production',\n        RS_HOST: this.url,\n      },\n    });\n\n    const serverApi = new DockerFunction(this, 'ServerApi', {\n      ...defaultProps,\n      basePath: '/api/{proxy+}',\n      variables: {\n        NODE_ENV: 'development',\n      },\n      repository: Repository.fromRepositoryName(this, 'ServerRepository', 'rsschool-server'),\n    });\n\n    const nestjsApi = new DockerFunction(this, 'NestjsApi', {\n      ...defaultProps,\n      basePath: '/api/v2/{proxy+}',\n      variables: {\n        NODE_ENV: 'development',\n      },\n      repository: Repository.fromRepositoryName(this, 'NestjsRepository', 'rsschool-nestjs'),\n    });\n\n    const authorizationCachePolicy = new CachePolicy(this, 'AuthorizationCachePolicy', {\n      defaultTtl: cdk.Duration.seconds(5),\n      minTtl: cdk.Duration.seconds(0),\n      maxTtl: cdk.Duration.seconds(60),\n      headerBehavior: CacheHeaderBehavior.allowList('Authorization'),\n      queryStringBehavior: CacheQueryStringBehavior.all(),\n      cookieBehavior: CacheCookieBehavior.all(),\n    });\n\n    const commonOriginRequestPolicy = new OriginRequestPolicy(this, 'CommonOriginRequestPolicy', {\n      queryStringBehavior: OriginRequestQueryStringBehavior.all(),\n      headerBehavior: OriginRequestHeaderBehavior.allowList('Origin'),\n      cookieBehavior: OriginRequestCookieBehavior.all(),\n    });\n\n    const createBehavior = (originDomain: string) => ({\n      origin: new origins.HttpOrigin(originDomain, {\n        protocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,\n      }),\n      allowedMethods: AllowedMethods.ALLOW_ALL,\n      cachePolicy: authorizationCachePolicy,\n      originRequestPolicy: commonOriginRequestPolicy,\n    });\n\n    const distribution = new Distribution(this, 'Distribution', {\n      defaultRootObject: '/',\n      domainNames: [this.fqdn],\n      certificate: acm.Certificate.fromCertificateArn(this, 'Certificate', certificateArn),\n      defaultBehavior: createBehavior(nextApp.domainName),\n      additionalBehaviors: {\n        '/api/*': createBehavior(serverApi.domainName),\n        '/api/v2/*': createBehavior(nestjsApi.domainName),\n      },\n    });\n\n    // Create a DNS record. in Production it will be an apex record, otherwise we set recordName\n    new route53.ARecord(this, 'AliasRecord', {\n      target: route53.RecordTarget.fromAlias(new alias.CloudFrontTarget(distribution)),\n      zone: route53.HostedZone.fromLookup(this, 'HostedZone', {\n        domainName: 'rs.school',\n      }),\n      recordName: this.fqdn,\n    });\n\n    new CfnOutput(this, 'Url', { value: this.url });\n  }\n}\n"
  },
  {
    "path": "setup/cdk/cdk.json",
    "content": "{\n  \"app\": \"npx tsx App.ts\",\n  \"context\": {\n    \"@aws-cdk/core:target-partitions\": [\"aws\", \"aws-cn\"]\n  }\n}\n"
  },
  {
    "path": "setup/cdk/package.json",
    "content": "{\n  \"type\": \"module\",\n  \"name\": \"cdk\",\n  \"dependencies\": {\n    \"aws-cdk\": \"2.1109.0\",\n    \"aws-cdk-lib\": \"2.241.0\",\n    \"constructs\": \"^10\",\n    \"tsx\": \"^4.19.2\"\n  }\n}\n"
  },
  {
    "path": "setup/docker-compose.yml",
    "content": "services:\n  postgres:\n    image: postgres:15.5\n    container_name: db\n    restart: always\n    #volumes:\n    # - /tmp/postgresql:/var/lib/postgresql\n    environment:\n      POSTGRES_PASSWORD: 12345678\n      POSTGRES_USER: rs_master\n      POSTGRES_DB: rs_school\n      PGDATA: /tmp\n    ports:\n      - 5432:5432\n"
  },
  {
    "path": "setup/dump-local.sh",
    "content": "pg_dump -h localhost --username rs_master rs_school --file backup-local.sql"
  },
  {
    "path": "setup/dump.sh",
    "content": "pg_dump -h $RSSCHOOL_AWS_RDS_HOST --username rs_master rs_school --file backup.sql\n"
  },
  {
    "path": "setup/nginx/nginx.conf",
    "content": "user  nginx;\nworker_processes  4;\n\nerror_log  /var/log/nginx/error.log info;\npid        /var/run/nginx.pid;\n\nevents {\n  worker_connections  1024;\n}\n\nhttp {\n  map $sent_http_content_type $expires {\n    default                    off;\n    text/html                  epoch;\n    text/css                   max;\n    application/javascript     max;\n    ~image/                    1d;\n  }\n\n  upstream client_target {\n    server client:8080;\n  }\n\n  upstream server_target {\n    server server:8080;\n  }\n\n  upstream nestjs_target {\n    server nestjs:8080;\n  }\n\n  default_type application/octet-stream;\n\n  server_tokens off;\n  sendfile on;\n  tcp_nopush on;\n  tcp_nodelay on;\n  keepalive_timeout 65;\n  types_hash_max_size 2048;\n  access_log off;\n  open_file_cache max=100;\n  client_max_body_size 50m;\n  proxy_read_timeout 120s;\n  proxy_send_timeout 120s;\n\n  server {\n    listen 8080;\n    server_name app.rs.school;\n\n    location /.well-known/acme-challenge/ {\n      root /var/www/certbot;\n    }\n\n    location / {\n      return 301 https://$host$request_uri;\n    }\n  }\n\n  server {\n    listen 443 ssl http2;\n    ssl_certificate /etc/letsencrypt/live/app.rs.school/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/app.rs.school/privkey.pem;\n\n    server_name app.rs.school;\n\n    keepalive_timeout 300;\n\n    server_tokens off;\n    etag off;\n    expires $expires;\n\n    gzip  on;\n    gzip_comp_level 5;\n    gzip_min_length 1100;\n    gzip_buffers    16 32k;\n    gzip_proxied any;\n    gzip_types\n      text/css\n      text/javascript\n      text/xml\n      text/plain\n      text/x-component\n      application/json\n      application/javascript\n      application/xml\n      application/rss+xml\n      font/truetype\n      font/opentype\n      application/vnd.ms-fontobject\n      image/svg+xml;\n\n    gzip_static on;\n    gzip_vary   on;\n\n    add_header X-Request-Time $request_time;\n\n    location /api/ {\n      rewrite ^(.*)\\/api(\\/.+)$ $1$2 break;\n      proxy_pass http://server_target;\n    }\n\n    location /api/v2/ {\n      rewrite ^(.*)\\/api/v2(\\/.+)$ $1$2 break;\n      proxy_pass http://nestjs_target;\n    }\n\n    location /certificate/ {\n      proxy_pass http://nestjs_target;\n    }\n\n    location / {\n      proxy_set_header x-domain app;\n      proxy_pass http://client_target;\n    }\n  }\n\n  server {\n    listen 443 ssl http2;\n    ssl_certificate /etc/letsencrypt/live/job.rs.school/fullchain.pem;\n    ssl_certificate_key /etc/letsencrypt/live/job.rs.school/privkey.pem;\n\n    server_name job.rs.school;\n\n    keepalive_timeout 300;\n\n    server_tokens off;\n    etag off;\n    expires $expires;\n\n    gzip  on;\n    gzip_comp_level 5;\n    gzip_min_length 1100;\n    gzip_buffers    16 32k;\n    gzip_proxied any;\n    gzip_types\n      text/css\n      text/javascript\n      text/xml\n      text/plain\n      text/x-component\n      application/json\n      application/javascript\n      application/xml\n      application/rss+xml\n      font/truetype\n      font/opentype\n      application/vnd.ms-fontobject\n      image/svg+xml;\n\n    gzip_static on;\n    gzip_vary   on;\n\n    add_header X-Request-Time $request_time;\n\n    location /api/ {\n      rewrite ^(.*)\\/api(\\/.+)$ $1$2 break;\n      proxy_pass http://server_target;\n    }\n\n    location /api/v2/ {\n      rewrite ^(.*)\\/api/v2(\\/.+)$ $1$2 break;\n      proxy_pass http://nestjs_target;\n    }\n\n    location / {\n      proxy_set_header x-domain job;\n      proxy_pass http://client_target;\n    }\n  }\n}\n"
  },
  {
    "path": "setup/restore-local.sh",
    "content": "podman exec  -i db psql -U rs_master -d rs_school < ./backup-local.sql\n"
  },
  {
    "path": "setup/restore.sh",
    "content": "podman exec -i db psql -U rs_master -d rs_school < ./backup.sql\n"
  },
  {
    "path": "skills-lock.json",
    "content": "{\n  \"version\": 1,\n  \"skills\": {\n    \"nestjs-best-practices\": {\n      \"source\": \"kadajett/agent-nestjs-skills\",\n      \"sourceType\": \"github\",\n      \"computedHash\": \"1b6f82e889d19d305e38e35594de08eca0242321f353cafa4cf5e61dd3aa1a73\"\n    },\n    \"typeorm\": {\n      \"source\": \"mindrally/skills\",\n      \"sourceType\": \"github\",\n      \"computedHash\": \"a78bf5815e02755d36eeda49e3e2e87b6e4e161cd90c6cbfb29dc447c4084b99\"\n    },\n    \"typescript-advanced-types\": {\n      \"source\": \"wshobson/agents\",\n      \"sourceType\": \"github\",\n      \"computedHash\": \"a03a83996ae9d2f4dd6d4db08899d61761c7d5351bce4c695d20a21197742a36\"\n    }\n  }\n}\n"
  },
  {
    "path": "tools/sloths/.eslintrc.cjs",
    "content": "/* eslint-env node */\nrequire('@rushstack/eslint-patch/modern-module-resolution');\n\nmodule.exports = {\n  root: true,\n  parser: 'vue-eslint-parser',\n  plugins: ['@typescript-eslint', 'vue', 'prettier'],\n  extends: [\n    'plugin:vue/vue3-essential',\n    'eslint:recommended',\n    'airbnb-base',\n    'airbnb-typescript/base',\n    'plugin:@typescript-eslint/recommended',\n    '@vue/eslint-config-typescript/recommended',\n    'plugin:prettier/recommended',\n    '@vue/eslint-config-prettier',\n  ],\n  parserOptions: {\n    project: './tsconfig.json',\n    ecmaVersion: 2020,\n    parser: '@typescript-eslint/parser',\n    sourceType: 'module',\n  },\n  settings: {\n    'import/resolver': {\n      typescript: {}, // this loads <rootdir>/tsconfig.json to eslint\n    },\n  },\n  rules: {\n    '@typescript-eslint/no-explicit-any': 'error',\n    '@typescript-eslint/no-unused-expressions': [\n      'error',\n      {\n        allowShortCircuit: true,\n      },\n    ],\n    '@typescript-eslint/array-type': [\n      'error',\n      {\n        default: 'array',\n      },\n    ],\n    'max-lines-per-function': ['error', 40],\n  },\n  ignorePatterns: ['*.config.ts'],\n};\n"
  },
  {
    "path": "tools/sloths/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# OS's/Editor's files and directories\n.DS_Store\n.idea\n.vscode\n"
  },
  {
    "path": "tools/sloths/.oxfmtrc.json",
    "content": "{\n  \"trailingComma\": \"es5\",\n  \"tabWidth\": 2,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"printWidth\": 120\n}\n"
  },
  {
    "path": "tools/sloths/README.md",
    "content": "# rs-sloths\n\nRS SLOTHS is a depository of sloths. View all existing stickers, download the necessary ones or chill a bit - why not? Feel a sloth!\n\n## Stack\n\nVue3, Pinia, Vue Router, TypeScript, Vite\n\n## Project Setup\n\n```sh\nnpm install\n```\n\n### Compile and Hot-Reload for Development\n\n```sh\nnpm run dev\n```\n\n### Type-Check, Compile and Minify for Production\n\n```sh\nnpm run build\n```\n\n### Lint with [ESLint](https://eslint.org/)\n\n```sh\nnpm run lint\n```"
  },
  {
    "path": "tools/sloths/env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tools/sloths/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon/favicon.ico\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/favicon/favicon-32x32.png\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>RS-SLOTHS</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "tools/sloths/package.json",
    "content": "{\n  \"name\": \"rs-sloths\",\n  \"version\": \"0.0.0\",\n  \"author\": \"WOV WOV Team\",\n  \"description\": \"Final project in FEJS2022Q1\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"run-p type-check build-only\",\n    \"preview\": \"vite preview --port 4173\",\n    \"build-only\": \"vite build\",\n    \"type-check\": \"vue-tsc --noEmit\",\n    \"lint\": \"eslint . --ext .vue,.js,.ts,.tsx --fix --ignore-path .gitignore\",\n    \"lint:staged\": \"lint-staged --relative\"\n  },\n  \"lint-staged\": {\n    \"*.{ts,vue}\": [\n      \"eslint --fix\"\n    ],\n    \"*.css\": [\n      \"oxfmt\"\n    ]\n  },\n  \"dependencies\": {\n    \"@intlify/vite-plugin-vue-i18n\": \"6.0.1\",\n    \"eslint-import-resolver-typescript\": \"3.5.0\",\n    \"file-saver\": \"2.0.5\",\n    \"jszip\": \"3.10.1\",\n    \"normalize.css\": \"8.0.1\",\n    \"pinia\": \"2.0.17\",\n    \"vue\": \"3.2.37\",\n    \"vue-i18n\": \"9.14.5\",\n    \"vue-router\": \"4.1.3\",\n    \"vue-three-shortkey\": \"4.0.1\"\n  },\n  \"devDependencies\": {\n    \"@rushstack/eslint-patch\": \"1.1.4\",\n    \"@types/file-saver\": \"2.0.5\",\n    \"@types/node\": \"16.11.47\",\n    \"@typescript-eslint/parser\": \"5.33.0\",\n    \"@vitejs/plugin-vue\": \"3.0.1\",\n    \"@vue/eslint-config-airbnb\": \"6.0.0\",\n    \"@vue/eslint-config-prettier\": \"7.0.0\",\n    \"@vue/eslint-config-typescript\": \"11.0.0\",\n    \"@vue/tsconfig\": \"0.1.3\",\n    \"eslint\": \"8.21.0\",\n    \"eslint-config-airbnb-base\": \"15.0.0\",\n    \"eslint-config-airbnb-typescript\": \"17.0.0\",\n    \"eslint-config-airbnb-typescript-vue\": \"3.0.0\",\n    \"eslint-plugin-vue\": \"9.3.0\",\n    \"lint-staged\": \"13.0.3\",\n    \"npm-run-all\": \"4.1.5\",\n    \"oxfmt\": \"0.36.0\",\n    \"typescript\": \"4.7.4\",\n    \"vite\": \"3.2.11\",\n    \"vue-eslint-parser\": \"9.0.3\",\n    \"vue-tsc\": \"0.39.5\"\n  }\n}\n"
  },
  {
    "path": "tools/sloths/public/cdn/cleaned/filelist.json",
    "content": "[\"codewars.svg\", \"congrats.svg\", \"deadline.svg\"]\n"
  },
  {
    "path": "tools/sloths/public/cdn/stickers/activist/metadata.json",
    "content": "{\n  \"name\": \"Activist\",\n  \"description\": \"Primus inter pares\",\n  \"tags\": [\"activist\", \"hero\"]\n}\n"
  },
  {
    "path": "tools/sloths/public/cdn/stickers/codewars/metadata.json",
    "content": "{\n  \"name\": \"Codewars\",\n  \"description\": \"Как-то летним днём\\nСамурай не сдал кросс-чек!\\nГрустно. 0 баллов.\",\n  \"tags\": [\"student\", \"samurai\", \"codewars\"]\n}\n"
  },
  {
    "path": "tools/sloths/public/cdn/stickers/congrats/metadata.json",
    "content": "{\n  \"name\": \"Congrats\",\n  \"description\": \"Bring on the Champagne!\",\n  \"tags\": [\"congratulations\", \"student\"]\n}\n"
  },
  {
    "path": "tools/sloths/public/cdn/stickers/metadata.json",
    "content": "{\n  \"stickers\": [\n    {\n      \"name\": \"Activist\",\n      \"description\": \"Primus inter pares\",\n      \"tags\": [\"activist\", \"hero\"],\n      \"id\": \"activist\"\n    },\n    {\n      \"name\": \"Codewars\",\n      \"description\": \"Как-то летним днём\\nСамурай не сдал кросс-чек!\\nГрустно. 0 баллов.\",\n      \"tags\": [\"student\", \"samurai\", \"codewars\"],\n      \"id\": \"codewars\"\n    },\n    {\n      \"name\": \"Congrats\",\n      \"description\": \"Bring on the Champagne!\",\n      \"tags\": [\"congratulations\", \"student\"],\n      \"id\": \"congrats\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tools/sloths/src/App.vue",
    "content": "<template>\n  <header-view />\n\n  <main class=\"main\">\n    <router-view v-slot=\"{ Component }\">\n      <Transition name=\"fade\" mode=\"out-in\">\n        <component :is=\"Component\" />\n      </Transition>\n    </router-view>\n  </main>\n\n  <footer-view />\n\n  <background-view :bgStyle=\"'background-main'\" />\n  <background-view :bgStyle=\"'background'\" />\n\n  <loader-view v-show=\"isLoad\" />\n\n  <alert-modal v-show=\"isAlert\" :header=\"header\" :message=\"message\" @closeAlertModal=\"isAlert = false\"></alert-modal>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapWritableState } from 'pinia';\n\nimport {\n  CDN_CLEANED_URL,\n  CDN_STICKERS_URL,\n  CLEANED_JSON_URL,\n  RESPONSE_STATUS_OK,\n  STICKERS_JSON_URL,\n} from '@/common/const';\n\nimport HeaderView from './components/header/HeaderView.vue';\nimport FooterView from './components/footer/FooterView.vue';\nimport LoaderView from './components/loader/LoaderView.vue';\nimport BackgroundView from './components/background/BackgroundView.vue';\nimport AlertModal from './components/modal/AlertModal.vue';\n\nimport useLoader from './stores/loader';\nimport useAlertModal from './stores/alert-modal';\nimport useAudioOn from './stores/audio-on';\n\nimport useCleanedStore from './stores/cleaned';\nimport useSlothsStore from './stores/sloths';\nimport type { MetadataSloths } from './common/types';\n\nexport default defineComponent({\n  name: 'App',\n\n  components: {\n    HeaderView,\n    FooterView,\n    LoaderView,\n    BackgroundView,\n    AlertModal,\n  },\n\n  computed: {\n    ...mapWritableState(useLoader, ['isLoad']),\n    ...mapWritableState(useAlertModal, ['isAlert', 'header', 'message']),\n    ...mapWritableState(useAudioOn, ['isAudioOn']),\n    ...mapWritableState(useCleanedStore, ['cleanedFilelist', 'originalFilelist']),\n    ...mapWritableState(useSlothsStore, ['sloths']),\n  },\n\n  created() {\n    this.isAlert = false;\n    this.header = 'modal.header.alert';\n    this.message = '';\n    this.$i18n.locale = 'en';\n    this.isLoad = true;\n    (async () => {\n      try {\n        await this.getStickers();\n        await this.getCleaned();\n      } finally {\n        this.isLoad = false;\n      }\n    })();\n  },\n\n  methods: {\n    async getCleaned(): Promise<void> {\n      try {\n        const response = await fetch(CLEANED_JSON_URL);\n\n        if (response.status !== RESPONSE_STATUS_OK) throw new Error(this.$t('catalog.stickersNotFound'));\n\n        const data: string[] = await response.json();\n        this.cleanedFilelist = data.map(file => `${CDN_CLEANED_URL}/${file}`);\n      } catch (error) {\n        this.showErrorModal(error);\n      }\n    },\n\n    async getStickers(): Promise<void> {\n      try {\n        const response = await fetch(STICKERS_JSON_URL);\n\n        if (response.status !== RESPONSE_STATUS_OK) throw new Error(this.$t('catalog.stickersNotFound'));\n\n        const data: MetadataSloths = await response.json();\n        this.sloths = data.stickers.map(sloth => ({\n          ...sloth,\n          image: `${CDN_STICKERS_URL}/${sloth.id}/image.svg`,\n          checked: false,\n        }));\n        this.originalFilelist = data.stickers.map(file => `${CDN_STICKERS_URL}/${file.id}/image.svg`);\n      } catch (error) {\n        this.showErrorModal(error);\n      }\n    },\n\n    showErrorModal(error: unknown) {\n      if (!(error instanceof Error)) return;\n      this.isAlert = true;\n      this.header = 'modal.header.error';\n      this.message = error.message;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/assets/locales/en.json",
    "content": "{\n  \"header\": {\n    \"title\": \"Header\",\n    \"switchers\": {\n      \"sound\": \"Turn on/off sound (Ctrl+1)\",\n      \"theme\": \"Switch theme (Ctrl+2)\"\n    }\n  },\n\n  \"footer\": {\n    \"title\": \"Footer\"\n  },\n\n  \"home\": {\n    \"title\": \"Home\",\n    \"more\": \"Read more\",\n    \"about\": {\n      \"project\": \"Our project is a depository of sloths. View all existing stickers, download the necessary ones or chill a bit - why not? Feel like a sloth a bit!\",\n      \"team\": \"We are a team of beginner frontend developers. We welcome you to our project, which we completed as part of the final task of the JS/FE2022Q1 Rolling Scopes School course!\"\n    }\n  },\n\n  \"about\": {\n    \"title\": \"About\",\n    \"sloths\": {\n      \"title\": \"About RS SLOTHS\",\n      \"main\": \"All sloths in one site!\",\n      \"descr\": \"We have gathered all the sloths together for you! Go to the catalog and see all the existing sloths! And if you want to use them somewhere, you can just select the ones you need and download it! Or you can go to the merch builder and prepare the source for something cool. Be active, but not too active. We are sloths here!\"\n    },\n    \"interactives\": {\n      \"title\": \"Interactive\",\n      \"main\": \"Sloths are about chilling!\",\n      \"descr\": \"We have tried to make sure that you can relax during your stay on our resource. Relax and play Memory Game, try to correctly guess all the sloths in the game of the same name, or create memes based on existing stickers: fun time, work hour!\"\n    },\n    \"teammates\": {\n      \"main\": \"WOV WOV Team is a team of like-minded people who strive to create something new, interesting and beautiful. We're for a businesslike attitude to humor and a humorous attitude to business!\",\n      \"wiijoy\": \"Designer, memolog and Vue.js promoter. Performed the initial setup, created a design layout and carried out work on the front-end part of the project.\",\n      \"ogimly\": \"Queen of TypeScript, mistress of API requests and just a good person. Worked on the front-end part of the project, completed all the work on communicating the front-end component with its server part.\",\n      \"vanet\": \"Genius, billionaire, playboy, philanthropist. In short, team-lead. Gathered a team, solved all organizational questions (and just solved), developed the backend of the project.\"\n    }\n  },\n\n  \"catalog\": {\n    \"title\": \"Catalog\",\n    \"search\": \"...caption or description\",\n    \"sorting\": \"Sort by...\",\n    \"info\": \"Detail info\",\n    \"count\": \"Total count\",\n    \"stickersNotFound\": \"Sloth stickers not found. Please, contact the site administrator\",\n    \"idNotFound\": \"Sloth not found\",\n    \"tagsNotFound\": \"Tags not found. Please, contact the site administrator\"\n  },\n\n  \"guess\": {\n    \"title\": \"Guess-a-Sloth Game\",\n    \"description\": \"Try to guess which sloth is hiding behind the dark outline? Only a real sloth can guess everyone!\",\n    \"rules\": \"(Press 1-4 to select an answer, press Enter to send)\",\n    \"start\": \"Let's Go!\",\n    \"congrats\": \"Congratulations!\",\n    \"win\": \"You guessed everyone!\",\n    \"result\": \"Your result:\",\n    \"guess\": \"Guess image\",\n    \"guesses\": \"guesses\",\n    \"next\": \"Send an answer\",\n    \"points\": \"point | points\",\n    \"results\": \"Guess-a-Sloth Results\"\n  },\n\n  \"memory\": {\n    \"title\": \"Memory Game\",\n    \"start\": \"Let's Go!\",\n    \"congrats\": \"Congratulations!\",\n    \"win\": \"You won! Your result:\",\n    \"steps\": \"step | steps\",\n    \"time\": \"seconds\",\n    \"level\": \"Choose your level\",\n    \"junior\": \"Junior\",\n    \"middle\": \"Middle\",\n    \"senior\": \"Senior\",\n    \"results\": \"Memory Results\"\n  },\n\n  \"create\": {\n    \"title\": \"Create a meme\",\n    \"description\": \"Choose an image, enter text, use mouse to move and scroll wheel to scale objects, copy the result at the end\",\n    \"top\": \"Text Top\",\n    \"bottom\": \"Text Bottom\",\n    \"margin\": \"Image Margin\",\n    \"color\": \"Text Color\",\n    \"stroke\": \"Text Stroke Color\",\n    \"backgroundColor\": \"Background Color\",\n    \"backgroundTransparent\": \"Transparent Background\"\n  },\n\n  \"merch\": {\n    \"title\": \"Merch\",\n    \"description\": \"Choose an image, enter text, use mouse to move and scroll wheel to scale objects, copy the result at the end\",\n    \"bottom\": \"Text\",\n    \"kText\": \"Text width (relative to image width)\",\n    \"kFont\": \"Font size (relative to image width)\",\n    \"marginTop\": \"Image Margin Top\",\n    \"marginLeft\": \"Image Margin Left\",\n    \"color\": \"Text Color\",\n    \"backgroundColor\": \"Item Color\"\n  },\n\n  \"modal\": {\n    \"header\": {\n      \"alert\": \"Alert!\",\n      \"error\": \"ERROR!\"\n    },\n    \"body\": {\n      \"download\": \"Download selected files?\"\n    },\n    \"btn\": {}\n  },\n\n  \"loader\": {\n    \"text\": \"Sloth's loading in progress\"\n  },\n\n  \"404\": {\n    \"title\": \"404: Page Not Found\",\n    \"text\": \"Sloth made a bug or didn't make this page.\\nPlease check the correctness of path again\"\n  },\n\n  \"sorting\": {\n    \"name+\": \"...Name (ascending)\",\n    \"name-\": \"...Name (descending)\"\n  },\n\n  \"btn\": {\n    \"close\": \"Close\",\n    \"reset\": \"Reset filters\",\n    \"show\": \"Detailed info\",\n    \"yes\": \"Yes\",\n    \"no\": \"No\",\n    \"copy\": \"Copy\",\n    \"download\": \"Download\",\n    \"trueSize\": \"True Size\",\n    \"center\": \"on Center\",\n    \"scaleUp\": \"Scale Up\",\n    \"scaleDown\": \"Scale Down\",\n    \"count-\": \"Count ▼\",\n    \"count+\": \"Count ▲\",\n    \"time-\": \"Time ▼\",\n    \"time+\": \"Time ▲\",\n    \"createdAt-\": \"Creation date ▼\",\n    \"createdAt+\": \"Creation date ▲\",\n    \"change\": \"Change kit\"\n  },\n\n  \"pagination\": {\n    \"perPage\": \"Items per page\",\n    \"next\": \"❯\",\n    \"prev\": \"❮\",\n    \"top\": \"❮❮\",\n    \"bottom\": \"❯❯\",\n    \"nextTitle\": \"❯ (Page Down)\",\n    \"prevTitle\": \"❮ (Page Up)\",\n    \"topTitle\": \"❮❮ (Home)\",\n    \"bottomTitle\": \"❯❯ (End)\",\n    \"count\": \"Total count\"\n  },\n\n  \"results\": {\n    \"user\": \"Personal results\"\n  }\n}\n"
  },
  {
    "path": "tools/sloths/src/assets/styles/base.css",
    "content": "*,\n*::before,\n*::after {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n  position: relative;\n  font-weight: normal;\n  font-family: 'Nunito Sans';\n}\n\n*::-webkit-scrollbar {\n  width: 5px;\n  height: 5px;\n  background: none;\n}\n\n*::-webkit-scrollbar-thumb {\n  width: 5px;\n  height: 5px;\n  background-color: var(--color-background-inverse);\n  border-radius: 1rem;\n}\n\nhtml {\n  font-size: 10px;\n}\n\n#app {\n  width: 100%;\n  height: 100vh;\n  display: grid;\n  grid-template-rows: 80px auto 60px;\n  background-color: var(--color-background-soft);\n  overflow: hidden;\n  position: relative;\n  font-size: 1.6rem;\n  transition: 0.5s ease;\n}\n\n.main {\n  height: 100%;\n  overflow-y: auto;\n  overflow-x: hidden;\n  z-index: 3;\n}\n\n.list-aside {\n  width: var(--width-panel);\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  gap: var(--gap-2);\n}\n\n.list-main {\n  width: calc(100% - var(--width-panel) - var(--gap));\n}\n\n@media (max-width: 768px) {\n  .list-aside,\n  .list-main {\n    width: calc(100% - var(--gap));\n  }\n}\n\n.select-element {\n  padding: 0.5rem;\n  cursor: pointer;\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  border-radius: 1rem;\n  appearance: none;\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  transition: 0.5s ease;\n}\n\n.select-element,\n.select-element:focus {\n  outline: none;\n}\n\n.select-element:focus {\n  border-color: var(--color-border-inverse);\n}\n\n.input-text {\n  padding: 0.5rem;\n  border: none;\n  border-bottom: 0.2rem solid gray;\n  background-color: var(--color-background);\n  color: inherit;\n}\n"
  },
  {
    "path": "tools/sloths/src/assets/styles/fonts.css",
    "content": "@font-face {\n  font-family: 'Nunito Sans';\n  src:\n    url('../fonts/NunitoSans-Light.woff2') format('woff2'),\n    url('../fonts/NunitoSans-Light.woff') format('woff');\n  font-weight: 300;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: 'Nunito Sans';\n  src:\n    url('../fonts/NunitoSans-Bold.woff2') format('woff2'),\n    url('../fonts/NunitoSans-Bold.woff') format('woff');\n  font-weight: bold;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: 'Nunito Sans';\n  src:\n    url('../fonts/NunitoSans-Black.woff2') format('woff2'),\n    url('../fonts/NunitoSans-Black.woff') format('woff');\n  font-weight: 900;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: 'Nunito Sans';\n  src:\n    url('../fonts/NunitoSans-Regular.woff2') format('woff2'),\n    url('../fonts/NunitoSans-Regular.woff') format('woff');\n  font-weight: normal;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: 'Arial';\n  src:\n    url('../fonts/Arial-Black.woff2') format('woff2'),\n    url('../fonts/Arial-Black.woff') format('woff');\n  font-weight: 900;\n  font-style: normal;\n  font-display: swap;\n}\n"
  },
  {
    "path": "tools/sloths/src/assets/styles/main.css",
    "content": "@import './fonts.css';\n@import './variables.css';\n\n@import './base.css';\n"
  },
  {
    "path": "tools/sloths/src/assets/styles/variables.css",
    "content": ":root {\n  --light-main: #dcdbe1;\n  --light-addict: #fdfdff;\n\n  --dark-main: #1f1f1f;\n  --dark-addict: #303032;\n\n  --blue-main: #e3e8ff;\n\n  --gray-main: #4b4b4b;\n\n  --red-active: #b44a4a;\n  --red-disactive: #deaaaa;\n\n  --green-active: #448447;\n  --green-disactive: #bed9bf;\n\n  --yellow-main: #ffdb1f;\n  --sloth-main: #c9a67a;\n\n  --dark-opacity: rgba(31, 31, 31, 0.8);\n\n  --width-panel: 30rem;\n  --gap: 1rem;\n  --gap-2: 2rem;\n}\n\n/* semantic color variables for this project */\n:root {\n  --color-background: var(--light-main);\n  --color-background-soft: var(--light-addict);\n\n  --color-background-inverse: var(--dark-addict);\n  --color-background-inverse-soft: var(--dark-main);\n\n  --color-background-opacity: rgba(220, 219, 225, 0.5);\n\n  --color-heading: var(--dark-addict);\n  --color-text: var(--dark-main);\n  --color-text-inverse: var(--light-main);\n\n  --color-border-theme: var(--light-main);\n  --color-border-inverse: var(--dark-main);\n  --color-border-inverse-soft: var(--gray-main);\n}\n\n:root.dark {\n  --color-background: var(--dark-main);\n  --color-background-soft: var(--dark-addict);\n\n  --color-background-inverse: var(--light-addict);\n  --color-background-inverse-soft: var(--light-main);\n\n  --color-background-opacity: rgba(31, 31, 31, 0.5);\n\n  --color-heading: var(--light-addict);\n  --color-text: var(--light-main);\n  --color-text-inverse: var(--dark-main);\n\n  --color-border-theme: var(--dark-main);\n  --color-border-inverse: var(--light-main);\n  --color-border-inverse-soft: var(--gray-main);\n}\n"
  },
  {
    "path": "tools/sloths/src/common/const.ts",
    "content": "/* eslint-disable import/prefer-default-export */\nimport type { MemoryLevel, SelectOptions } from './types';\n\nexport const MILLISECONDS_IN_SECOND = 1000;\n\n// Services\nexport const CDN_URL = import.meta.env.VITE_CDN_URL;\nexport const CDN_STICKERS_URL = `${CDN_URL}/stickers`;\nexport const CDN_CLEANED_URL = `${CDN_URL}/cleaned`;\nexport const STICKERS_JSON_URL = `${CDN_STICKERS_URL}/metadata.json`;\nexport const CLEANED_JSON_URL = `${CDN_CLEANED_URL}/filelist.json`;\n\nexport const RESPONSE_STATUS_OK = 200;\n\n// Sorting\nexport const SLOTH_SORTING: SelectOptions[] = [\n  {\n    value: 'name-asc',\n    text: 'sorting.name+',\n    type: 'name',\n  },\n  {\n    value: 'name-desc',\n    text: 'sorting.name-',\n    type: 'name',\n  },\n];\nexport const GAME_RESULT_SORTING: SelectOptions[] = [\n  {\n    value: 'count-asc',\n    text: 'btn.count+',\n    type: 'count',\n  },\n  {\n    value: 'count-desc',\n    text: 'btn.count-',\n    type: 'count',\n  },\n  {\n    value: 'time-asc',\n    text: 'btn.time+',\n    type: 'time',\n  },\n  {\n    value: 'time-desc',\n    text: 'btn.time-',\n    type: 'time',\n  },\n  {\n    value: 'createdAt-asc',\n    text: 'btn.createdAt+',\n    type: 'createdAt',\n  },\n  {\n    value: 'createdAt-desc',\n    text: 'btn.createdAt-',\n    type: 'createdAt',\n  },\n];\n\n// Pagination\nexport const PAGINATION_OPTIONS: number[] = [10, 15, 20, 25, 50, 100];\n\n// Memory Game\nexport const MEMORY_LEVELS: MemoryLevel[] = [\n  {\n    level: 'junior',\n    n: 4,\n  },\n  {\n    level: 'middle',\n    n: 8,\n  },\n  {\n    level: 'senior',\n    n: 12,\n  },\n];\nexport const MEMORY_GAME_TIMEOUT = MILLISECONDS_IN_SECOND;\nexport const MEMORY_GAME_SPRITE = './img/memory/memory-sprite.svg';\nexport const MEMORY_GAME_WINNER = './img/memory/winner1.svg';\n\n// Guess Game\nexport const GUESS_GAME_WINNER = './img/guess/winner2.svg';\n\nexport const CATALOG_SLOTH_PREVIEW = './img/preview.svg';\n\nexport const GUESS_SLOTHS = [\n  { caption: 'Error', img: './img/guess/painted/01.svg' },\n  { caption: 'Activist', img: './img/guess/painted/02.svg' },\n  { caption: 'Deadline', img: './img/guess/painted/03.svg' },\n  { caption: 'Not a Bug', img: './img/guess/painted/04.svg' },\n  { caption: 'Walk', img: './img/guess/painted/05.svg' },\n  { caption: 'Google', img: './img/guess/painted/06.svg' },\n  { caption: 'I Break', img: './img/guess/painted/07.svg' },\n  { caption: 'How I See', img: './img/guess/painted/08.svg' },\n  { caption: 'Lazy', img: './img/guess/painted/09.svg' },\n  { caption: 'What Is It', img: './img/guess/painted/10.svg' },\n  { caption: 'Works on My Machine', img: './img/guess/painted/11.svg' },\n  { caption: 'This Is Love', img: './img/guess/painted/12.svg' },\n  { caption: 'Codewars', img: './img/guess/painted/13.svg' },\n  { caption: 'Finished Work', img: './img/guess/painted/14.svg' },\n  { caption: 'Git Problem', img: './img/guess/painted/15.svg' },\n  { caption: 'GitHub Friends', img: './img/guess/painted/16.svg' },\n  { caption: 'Helper', img: './img/guess/painted/17.svg' },\n  { caption: 'Hero', img: './img/guess/painted/18.svg' },\n  { caption: 'One Hour Before Deadline', img: './img/guess/painted/19.svg' },\n  { caption: 'Student 1', img: './img/guess/painted/20.svg' },\n  { caption: 'Train', img: './img/guess/painted/21.svg' },\n  { caption: 'Welcome', img: './img/guess/painted/22.svg' },\n  { caption: 'It Is OK', img: './img/guess/painted/23.svg' },\n  { caption: 'Interview with Mentor', img: './img/guess/painted/24.svg' },\n  { caption: 'Lecture with Mentor', img: './img/guess/painted/25.svg' },\n  { caption: 'Mentor', img: './img/guess/painted/26.svg' },\n  { caption: 'Mentor with His Students', img: './img/guess/painted/27.svg' },\n  { caption: 'Time to Pay', img: './img/guess/painted/28.svg' },\n  { caption: 'Wanted Mentors', img: './img/guess/painted/29.svg' },\n  { caption: 'WTF', img: './img/guess/painted/30.svg' },\n  { caption: 'Expert', img: './img/guess/painted/31.svg' },\n  { caption: 'Shocked', img: './img/guess/painted/32.svg' },\n  { caption: 'Congratulation', img: './img/guess/painted/33.svg' },\n  { caption: 'Read the Chat', img: './img/guess/painted/34.svg' },\n  { caption: 'So Little Work I Done', img: './img/guess/painted/35.svg' },\n  { caption: 'Student without Mentor', img: './img/guess/painted/36.svg' },\n  { caption: 'Thank you', img: './img/guess/painted/37.svg' },\n  { caption: \"it's a Good Job\", img: './img/guess/painted/38.svg' },\n  { caption: 'Congrats', img: './img/guess/painted/39.svg' },\n  { caption: 'No Mentor', img: './img/guess/painted/40.svg' },\n];\n"
  },
  {
    "path": "tools/sloths/src/common/types.ts",
    "content": "export type Sloth = {\n  [keyof: string]: string | boolean | string[];\n  id: string;\n  name: string;\n  description: string;\n  tags: string[];\n  checked: boolean;\n  image: string;\n};\nexport type MetadataSloths = {\n  stickers: MetadataSloth[];\n};\nexport type MetadataSloth = {\n  id: string;\n  name: string;\n  description: string;\n  tags: string[];\n};\n\nexport type GameResult = {\n  id?: string;\n  count: number;\n  time: number;\n  createdAt: number;\n  user?: {\n    github: string;\n    id: string;\n    name: string;\n  };\n};\n\nexport type MemoryLevel = {\n  level: string;\n  n: number;\n};\n\nexport type QueryStringOptions = {\n  page?: number;\n  limit?: number;\n  order?: string;\n  searchText?: string;\n  filter?: string;\n};\n\nexport type SelectOptions = {\n  value: string;\n  text: string;\n  type: string;\n};\n\nexport type TagCloud = Set<string>;\n\nexport type PageSettings = {\n  currPage: number;\n  perPage: number;\n  searchText: string;\n  selected: string[];\n  sorting: string;\n  checked?: string[];\n};\n\nexport type CanvasProperties = {\n  scaleSteps: number;\n  scaleMin: number;\n  scaleTrue: number;\n  scaleMax: number;\n  backgroundTransparent: boolean;\n  backgroundColor: string;\n  itemColor: string;\n  textColor: string;\n  strokeColor: string;\n};\n\nexport type CanvasElement = {\n  isResizable: boolean;\n  text: string;\n  left: number;\n  top: number;\n  bottom: number;\n  scaledLeft: number;\n  scaledTop: number;\n  scaledBottom: number;\n  width: number;\n  height: number;\n  scaleSteps: number;\n  scaleMin: number;\n  scaleTrue: number;\n  scaleMax: number;\n  scaledWidth: number;\n  scaledHeight: number;\n  isHovered: boolean;\n  isSelected: boolean;\n  isBorderHovered: boolean;\n  isLeftBorderSelected: boolean;\n  isRightBorderSelected: boolean;\n  selectedPos: CanvasPos;\n};\n\nexport type CanvasPos = {\n  x: number;\n  y: number;\n};\n\nexport type CanvasRectXY = {\n  x1: number;\n  x2: number;\n  y1: number;\n  y2: number;\n};\n"
  },
  {
    "path": "tools/sloths/src/components/about/AboutSection.vue",
    "content": "<template>\n  <section :class=\"`about__section about__section_${section}-${currTheme}`\">\n    <h2 class=\"about__title\">{{ $t(`about.${section}.title`) }}</h2>\n    <div class=\"about__project\">\n      <p class=\"about__main\">{{ $t(`about.${section}.main`) }}</p>\n      <p class=\"about__descr\">{{ $t(`about.${section}.descr`) }}</p>\n    </div>\n  </section>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport { mapWritableState } from 'pinia';\nimport useThemeProp from '../../stores/theme';\n\nexport default defineComponent({\n  name: 'AboutSection',\n\n  props: {\n    section: {\n      type: String,\n      required: true,\n    },\n    softClass: {\n      type: String,\n      required: true,\n      default: () => '',\n    },\n    index: {\n      type: Number,\n      required: true,\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n  },\n});\n</script>\n\n<style scoped>\n.about__section {\n  display: flex;\n  flex-direction: column;\n  gap: 2rem;\n  position: relative;\n}\n\n.about__section_sloths-light::before,\n.about__section_sloths-dark::before,\n.about__section_interactives-light::before,\n.about__section_interactives-dark::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  width: 30rem;\n  height: 30rem;\n  transition: 0.5s ease;\n}\n\n.about__title {\n  font-size: 2.8rem;\n  font-weight: 700;\n  text-transform: uppercase;\n  text-align: center;\n  color: var(--color-heading);\n  transition: 0.5s ease;\n}\n\n.about__project {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n}\n\n.about__main {\n  font-size: 2.8rem;\n  text-align: v-bind(softClass);\n  color: var(--color-heading);\n  transition: 0.5s ease;\n}\n\n.about__descr {\n  font-size: 2.2rem;\n  line-height: 3.2rem;\n  color: var(--color-text);\n  transition: 0.5s ease;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/about/AboutTeammate.vue",
    "content": "<template>\n  <div class=\"teammate\">\n    <img class=\"teammate__photo\" :src=\"`/img/photo/${teammate}.png`\" alt=\"photo\" />\n    <div class=\"teammate__wrap\">\n      <div class=\"teammate__main\">{{ teammate }}</div>\n      <div class=\"teammate__descr\">{{ $t(`about.teammates.${teammate}`) }}</div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'AboutTeammate',\n\n  props: {\n    teammate: {\n      type: String,\n      required: true,\n    },\n  },\n});\n</script>\n\n<style scoped>\n.teammate {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 5rem;\n}\n\n.teammate__photo {\n  border-radius: 50%;\n  border: 0.5rem solid var(--color-border-inverse);\n  height: 25rem;\n  transition: 0.5s ease;\n}\n\n.teammate__wrap {\n  display: flex;\n  flex-direction: column;\n  gap: 2rem;\n}\n\n.teammate__main {\n  font-size: 2.8rem;\n  color: var(--color-heading);\n  text-transform: uppercase;\n  text-align: center;\n  transition: 0.5s ease;\n}\n\n.teammate__descr {\n  font-size: 2.2rem;\n  line-height: 3.2rem;\n  color: var(--color-text);\n  transition: 0.5s ease;\n}\n\n@media (max-width: 1200px) {\n  .teammate {\n    flex-direction: row;\n  }\n}\n\n@media (max-width: 768px) {\n  .teammate {\n    flex-direction: column;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/background/BackgroundView.vue",
    "content": "<template>\n  <div :class=\"currClassGroup\"></div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapWritableState } from 'pinia';\n\nimport useThemeProp from '../../stores/theme';\n\nexport default defineComponent({\n  name: 'BackgroundView',\n\n  props: {\n    bgStyle: {\n      type: String,\n      default: () => 'background',\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n\n    currRoute() {\n      return String(this.$route.name);\n    },\n\n    currClassGroup() {\n      return this.bgStyle === 'background'\n        ? `${this.bgStyle} ${this.bgStyle}-${this.currRoute}-${this.currTheme}`\n        : `${this.bgStyle} ${this.bgStyle}-${this.currTheme}`;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.background,\n.background-main {\n  height: 100vh;\n  width: 100vw;\n  position: fixed;\n  z-index: 1;\n  transition: 0.5s ease;\n}\n\n.background {\n  height: calc(100vh - 5rem);\n  margin: 2rem 0;\n  z-index: 2;\n  left: 0;\n  bottom: 0;\n}\n\n.background-main-light {\n  background: no-repeat center center / cover url('../../assets/backgrounds/bg-stars-light.svg');\n}\n\n.background-main-dark {\n  background: no-repeat center center / cover url('../../assets/backgrounds/bg-stars-dark.svg');\n}\n\n.background-home-light {\n  background: no-repeat left 5rem bottom / contain url('../../assets/backgrounds/bg-home-light.svg');\n}\n\n.background-home-dark {\n  background: no-repeat left 5rem center / contain url('../../assets/backgrounds/bg-home-dark.svg');\n}\n\n.background-about-light {\n  background: no-repeat center center / contain url('../../assets/backgrounds/bg-about-light.svg');\n}\n\n.background-about-dark {\n  background: no-repeat center center / contain url('../../assets/backgrounds/bg-about-dark.svg');\n}\n\n.background-memory-light {\n  background: no-repeat center center / contain url('../../assets/backgrounds/bg-memory-light.svg');\n}\n\n.background-memory-dark {\n  background: no-repeat center center / contain url('../../assets/backgrounds/bg-memory-dark.svg');\n}\n\n.background-404-light {\n  background: no-repeat left 5rem bottom / contain url('../../assets/backgrounds/bg-404-light.svg');\n}\n\n.background-404-dark {\n  background: no-repeat left 5rem bottom / contain url('../../assets/backgrounds/bg-404-dark.svg');\n}\n\n.background-catalog-light {\n  background: no-repeat left -15rem bottom / contain url('../../assets/backgrounds/bg-catalog-light.svg');\n}\n\n.background-catalog-dark {\n  background: no-repeat left -15rem bottom / contain url('../../assets/backgrounds/bg-catalog-dark.svg');\n}\n\n.background-create-light {\n  background: no-repeat left 5rem bottom / contain url('../../assets/backgrounds/bg-create-light.svg');\n}\n\n.background-create-dark {\n  background: no-repeat left 5rem bottom / contain url('../../assets/backgrounds/bg-create-dark.svg');\n}\n\n.background-guess-light {\n  background: no-repeat left -10rem bottom / contain url('../../assets/backgrounds/bg-guess-light.svg');\n}\n\n.background-guess-dark {\n  background: no-repeat left -10rem bottom / contain url('../../assets/backgrounds/bg-guess-dark.svg');\n}\n\n.background-merch-light {\n  background: no-repeat left 5rem bottom / contain url('../../assets/backgrounds/bg-merch-light.svg');\n}\n\n.background-merch-dark {\n  background: no-repeat left 5rem bottom / contain url('../../assets/backgrounds/bg-merch-dark.svg');\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/buttons/CustomBtn.vue",
    "content": "<template>\n  <button :class=\"className\" @click=\"onClick\" :disabled=\"disabled\" :title=\"text\">\n    {{ text }}\n  </button>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, type PropType } from 'vue';\nimport { mapWritableState } from 'pinia';\nimport useThemeProp from '@/stores/theme';\n\nexport default defineComponent({\n  name: 'CustomBtn',\n\n  props: {\n    text: {\n      type: String,\n      default: '',\n    },\n    className: {\n      type: String,\n      default: 'btn btn-primary',\n    },\n    onClick: {\n      type: Function as PropType<(payload: MouseEvent) => void>,\n      default: () => {},\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n  },\n});\n</script>\n\n<style>\n.btn {\n  display: inline-block;\n  padding: 0.4em 0.8em;\n  background: none;\n  cursor: pointer;\n  user-select: none;\n  transition: 0.3s;\n  border: none;\n}\n\n.btn-primary {\n  border-radius: 0.3rem;\n  color: var(--color-text-inverse);\n  background-color: var(--color-background-inverse);\n}\n\n.btn-primary:hover:not([disabled]) {\n  background-color: var(--color-background-inverse-soft);\n}\n\n.btn-primary:disabled,\n.btn-pagination:disabled {\n  cursor: default;\n  color: gray;\n  background-color: var(--color-background-inverse-soft);\n}\n\n.btn-pagination {\n  width: 4rem;\n  height: 4rem;\n  border-radius: 50%;\n  color: var(--color-text-inverse);\n  background-color: var(--color-background-inverse);\n}\n\n.btn-pagination:hover:not([disabled]) {\n  background-color: var(--color-background-inverse-soft);\n}\n\n.btn-tab {\n  border: 1px solid;\n  border-top-left-radius: 5px;\n  border-top-right-radius: 5px;\n  margin-bottom: -1px;\n  margin-right: -1px;\n  color: var(--color-text);\n  background-color: var(--color-background-soft);\n}\n\n.btn-tab:hover {\n  background-color: var(--color-background);\n}\n\n.btn-tab.btn-tab_active,\n.btn-tab.btn-tab_active:hover {\n  color: var(--color-text-inverse);\n  background-color: var(--color-background-inverse);\n}\n\n.btn-search {\n  position: absolute;\n  top: 0;\n  right: -0.5rem;\n  background: none;\n  color: inherit;\n}\n\n.btn__text {\n  color: var(--color-text-inverse);\n}\n\n.btn-link {\n  color: var(--color-text);\n  border: 0.2rem solid var(--color-border-inverse);\n  border-radius: 0.5rem;\n  text-decoration: none;\n}\n\n.btn-link:hover:not([disabled]) {\n  border-color: var(--color-border-theme);\n  background-color: var(--color-background-soft);\n}\n\n@media (max-width: 1200px) {\n  .btn-tab {\n    border-radius: 0.5rem;\n    margin: 0;\n    text-align: center;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/buttons/IconBtn.vue",
    "content": "<template>\n  <button :class=\"classNameTheme\" @click=\"onClick\" :disabled=\"disabled\" :title=\"text\"></button>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, type PropType } from 'vue';\nimport { mapWritableState } from 'pinia';\nimport useThemeProp from '@/stores/theme';\n\nexport default defineComponent({\n  name: 'IconBtn',\n\n  props: {\n    text: {\n      type: String,\n      default: '',\n    },\n    className: {\n      type: String,\n      default: 'btn btn-icon',\n    },\n    onClick: {\n      type: Function as PropType<(payload: MouseEvent) => void>,\n      default: () => {},\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n\n    classNameTheme() {\n      return this.className.includes('icon-')\n        ? this.className.replace('icon-', `icon-${this.currTheme}-`)\n        : this.className;\n    },\n  },\n});\n</script>\n\n<style>\n.btn-icon {\n  width: 3rem;\n  height: 3rem;\n  border-radius: 0.5rem;\n  background-color: var(--color-background-inverse);\n  background-size: 65%;\n  background-repeat: no-repeat;\n  background-position: center center;\n}\n\n.btn-icon:hover {\n  background-color: var(--color-background-inverse-soft);\n}\n\n.icon-light-download {\n  background-image: url('@/assets/icons/btn/download.svg');\n}\n\n.icon-dark-download {\n  background-image: url('@/assets/icons/btn/download-black.svg');\n}\n\n.icon-light-copy {\n  background-image: url('@/assets/icons/btn/clipboard.svg');\n}\n\n.icon-dark-copy {\n  background-image: url('@/assets/icons/btn/clipboard-black.svg');\n}\n\n.icon-light-plus {\n  background-image: url('@/assets/icons/btn/plus-square.svg');\n}\n\n.icon-dark-plus {\n  background-image: url('@/assets/icons/btn/plus-square-black.svg');\n}\n\n.icon-light-true {\n  background-image: url('@/assets/icons/btn/check-square.svg');\n}\n\n.icon-dark-true {\n  background-image: url('@/assets/icons/btn/check-square-black.svg');\n}\n\n.icon-light-center {\n  background-image: url('@/assets/icons/btn/center-square.svg');\n}\n\n.icon-dark-center {\n  background-image: url('@/assets/icons/btn/center-square-black.svg');\n}\n\n.icon-light-minus {\n  background-image: url('@/assets/icons/btn/dash-square.svg');\n}\n\n.icon-dark-minus {\n  background-image: url('@/assets/icons/btn/dash-square-black.svg');\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/buttons/ImageBtn.vue",
    "content": "<template>\n  <button :class=\"className\" @click=\"onClick\" :disabled=\"disabled\" :title=\"text\">\n    <img class=\"image\" :src=\"imgPath\" :alt=\"text\" />\n  </button>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, type PropType } from 'vue';\nimport { mapWritableState } from 'pinia';\nimport useThemeProp from '@/stores/theme';\n\nexport default defineComponent({\n  name: 'ImageBtn',\n\n  props: {\n    text: {\n      type: String,\n      default: '',\n    },\n    className: {\n      type: String,\n      default: 'btn btn-img',\n    },\n    imgPath: {\n      type: String,\n      default: '',\n    },\n    onClick: {\n      type: Function as PropType<(payload: MouseEvent) => void>,\n      default: () => {},\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n  },\n});\n</script>\n\n<style>\n.btn-img .image {\n  overflow: hidden;\n  border: 0.2rem solid var(--color-border-inverse);\n}\n\n.btn-img:hover:not([disabled]) {\n  transform: scale(1.1) rotate(10deg);\n}\n\n.btn-merch {\n  width: 20rem;\n  height: 20rem;\n  border-radius: 50%;\n  border: 0.2rem solid var(--color-border-inverse);\n  align-self: center;\n}\n\n.btn-merch > .image {\n  height: 100%;\n  width: 100%;\n  padding: 0.5rem;\n  object-fit: contain;\n  border: none;\n}\n\n.btn-memory {\n  width: 6rem;\n  height: 6rem;\n  border-radius: 50%;\n  padding: 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: var(--color-background-inverse-soft);\n  transition: 0.5s ease;\n}\n\n.btn-memory > .image {\n  border: none;\n  border-radius: 0.5rem;\n}\n\n.btn-memory:disabled {\n  cursor: default;\n}\n\n.btn-memory:hover:not([disabled]) {\n  transform: scale(1.1) rotate(10deg);\n}\n\n.btn-download {\n  margin: 0 auto;\n  padding: 0;\n  width: 24rem;\n}\n\n.btn-download > .image {\n  border: none;\n}\n\n.btn-download:hover:not([disabled]) {\n  transform: scale(1.1);\n}\n\n.btn-download:disabled {\n  filter: grayscale(1);\n  transform: scale(0.9);\n  cursor: default;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/catalog/SlothCard.vue",
    "content": "<template>\n  <div :class=\"`${pageName}-sloth-info`\">\n    <div v-if=\"catalogPage && !isDownload\" class=\"catalog-sloth-info__inner\">\n      <div class=\"catalog-sloth-info__sloth\">\n        <img class=\"catalog-sloth-info__img\" :src=\"imageUrl\" :alt=\"slothInfo.name\" />\n        <div class=\"sloth-info__tags tags\">\n          <span class=\"sloth-info__tag\" v-for=\"tag in slothInfo.tags\" :key=\"tag\">{{ tag }}</span>\n        </div>\n      </div>\n      <custom-btn :className=\"'icon ' + classNameCheckIcon\" @click=\"$emit('checkSloth', slothInfo)\"></custom-btn>\n      <div>\n        <div class=\"catalog-sloth-info__props\">\n          <p class=\"sloth-info__property sloth-info__property_text\">{{ slothInfo.name }}</p>\n        </div>\n      </div>\n      <custom-btn\n        :text=\"$t('btn.show')\"\n        className=\"btn btn-primary\"\n        @click=\"$emit('showSloth', slothInfo)\"\n      ></custom-btn>\n    </div>\n\n    <div v-else class=\"download-sloth-info__inner\">\n      <custom-btn\n        :className=\"'download-icon ' + classNameCheckIcon\"\n        @click=\"$emit('checkSloth', slothInfo)\"\n      ></custom-btn>\n      <div class=\"download-sloth-info__sloth\">\n        <img class=\"download-sloth-info__img\" :src=\"imageUrl\" :alt=\"slothInfo.name\" />\n      </div>\n      <p class=\"sloth-info__property\">{{ slothInfo.name }}</p>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, type PropType } from 'vue';\nimport type { Sloth } from '@/common/types';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\n\nexport default defineComponent({\n  name: 'SlothCard',\n\n  components: {\n    CustomBtn,\n  },\n\n  data() {\n    return {\n      isApproveShow: false,\n    };\n  },\n\n  props: {\n    slothInfo: {\n      type: Object as PropType<Sloth>,\n      required: true,\n    },\n    isDownload: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  computed: {\n    imageUrl(): string {\n      return this.slothInfo.image;\n    },\n\n    pageName(): string {\n      if (this.isDownload) return 'download';\n      return 'catalog';\n    },\n\n    catalogPage(): boolean {\n      return this.$route.name === 'catalog';\n    },\n\n    classNameCheckIcon(): string {\n      return this.slothInfo.checked ? 'icon_check-on' : 'icon_check-off';\n    },\n  },\n\n  methods: {\n    closeModal() {\n      this.isApproveShow = false;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.catalog-sloth-info {\n  overflow: hidden;\n  background-color: var(--color-background-soft);\n  border: 1px solid gray;\n}\n\n.catalog-sloth-info {\n  position: relative;\n  padding: 1rem;\n  width: 30rem;\n  border-radius: 1rem;\n}\n\n.catalog-sloth-info:hover {\n  box-shadow: 0px 0px 0.5rem gray;\n}\n\n.catalog-sloth-info__inner,\n.download-sloth-info__inner {\n  display: flex;\n  align-items: center;\n  gap: var(--gap);\n}\n\n.catalog-sloth-info__inner {\n  flex-direction: column;\n  justify-content: center;\n}\n\n.catalog-sloth-info__sloth {\n  position: relative;\n  overflow: hidden;\n}\n\n.catalog-sloth-info__img {\n  width: 100%;\n  height: 25rem;\n  object-fit: contain;\n}\n\n.catalog-sloth-info__props {\n  display: flex;\n  flex-direction: column;\n}\n\n.catalog-sloth-info__props {\n  text-align: center;\n  align-items: center;\n  gap: var(--gap);\n}\n\n.sloth-info__property {\n  font-size: 2rem;\n}\n\n.sloth-info__property_text {\n  height: 5rem;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.sloth-info__btn {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: var(--gap);\n}\n\n.btn-horizontal {\n  flex-direction: row;\n}\n\n.sloth-info__tags {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  transform: translateY(-500px);\n  transition: transform 0.3s;\n  justify-content: center;\n  z-index: 10;\n}\n\n.catalog-sloth-info__sloth:hover .sloth-info__tags {\n  transform: translateY(0);\n}\n\n.sloth-info__tag {\n  padding: 0.5rem 0.7rem;\n  cursor: default;\n  color: inherit;\n  background-color: var(--color-background);\n  border-radius: 1rem;\n  border: 1px solid gray;\n}\n\n.icon,\n.download-icon {\n  width: 3rem;\n  height: 3rem;\n  cursor: pointer;\n  border: none;\n  background-color: transparent;\n  background-repeat: no-repeat;\n  background-size: contain;\n  background-position: center center;\n}\n\n.icon {\n  position: absolute;\n  bottom: 0rem;\n  right: 0rem;\n}\n\n.icon_check-on {\n  background-image: url('@/assets/icons/btn/check-circle-fill.svg');\n}\n\n.icon_check-off {\n  background-image: url('@/assets/icons/btn/check-circle.svg');\n}\n\n.download-sloth-info {\n  position: relative;\n  height: 6rem;\n}\n\n.download-sloth-info__img {\n  height: 6rem;\n}\n\n.sloth-info__select {\n  padding: 0.5rem;\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  background-color: var(--color-background);\n  color: inherit;\n  width: 5rem;\n  border-radius: 1rem;\n  transition: 0.5s ease;\n}\n\n.sloth-info__user-other {\n  align-self: end;\n}\n\n.sloth-info__text__sloth {\n  height: 2rem;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/catalog/SlothInfo.vue",
    "content": "<template>\n  <div class=\"sloth-info\">\n    <modal-window v-show=\"isSlothInfoVisible\" @close=\"closeModal\">\n      <template v-slot:header> {{ header }} </template>\n\n      <template v-slot:body>\n        <div>\n          <div class=\"sloth-info__props\">\n            <div class=\"sloth-info__sloth\">\n              <img class=\"sloth-info__img\" :src=\"imageUrl\" :alt=\"slothInfo.name\" />\n            </div>\n            <div class=\"sloth-info__property property-center\">\n              <p class=\"sloth-info__text\">{{ slothInfo.description }}</p>\n            </div>\n            <div class=\"sloth-info__property property-center\">\n              <div class=\"tags\">\n                <span class=\"tag\" v-for=\"tag in slothInfo.tags\" :key=\"tag\">{{ tag }}</span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </template>\n    </modal-window>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { storeToRefs } from 'pinia';\nimport ModalWindow from '@/components/modal/ModalWindow.vue';\nimport useSlothInfo from '@/stores/sloth-info';\nimport { CATALOG_SLOTH_PREVIEW } from '@/common/const';\n\nconst { slothInfo, tagsStr } = storeToRefs(useSlothInfo());\n\nexport default defineComponent({\n  name: 'SlothInfo',\n\n  components: {\n    ModalWindow,\n  },\n\n  data() {\n    return {\n      slothInfo,\n      tags: tagsStr,\n      newFile: {} as File,\n      preview: CATALOG_SLOTH_PREVIEW,\n      isModalVisible: false,\n    };\n  },\n\n  props: {\n    isSlothInfoVisible: {\n      type: Boolean,\n      default: false,\n    },\n    headerText: {\n      type: String,\n      required: true,\n    },\n  },\n\n  computed: {\n    header(): string {\n      return this.slothInfo.name;\n    },\n\n    imageUrl(): string {\n      return this.slothInfo.image ? this.slothInfo.image : CATALOG_SLOTH_PREVIEW;\n    },\n  },\n\n  methods: {\n    closeModal() {\n      this.$emit('closeSlothInfo');\n\n      const { uploadBtn } = this.$refs;\n      if (uploadBtn instanceof HTMLInputElement) uploadBtn.value = '';\n      this.preview = CATALOG_SLOTH_PREVIEW;\n      this.newFile = {} as File;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.sloth-info__props {\n  max-width: 50rem;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: var(--gap);\n}\n\n.sloth-info__sloth,\n.sloth-info__property {\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--gap);\n}\n\n.property-center {\n  justify-content: center;\n}\n\n.sloth-info__sloth {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.sloth-info__img {\n  height: 20rem;\n}\n\n.sloth-info__text {\n  text-align: center;\n  white-space: pre-wrap;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/controls-list/ControlsList.vue",
    "content": "<template>\n  <div class=\"controls\">\n    <search-text ref=\"search\" @search=\"$emit('search')\" :placeholder=\"placeholder\"></search-text>\n    <tag-cloud ref=\"tags\" @tags=\"$emit('tags')\" :tags=\"tags\"></tag-cloud>\n    <sorting-list ref=\"sorting\" @sorting=\"$emit('sorting')\" :title=\"title\" :options=\"options\"></sorting-list>\n    <custom-btn @click=\"clearAll\" :text=\"$t('btn.reset')\" className=\"btn btn-primary\"></custom-btn>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, type PropType } from 'vue';\nimport type { SelectOptions } from '@/common/types';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\nimport SearchText, { type SearchTextElement } from '@/components/controls-list/SearchText.vue';\nimport TagCloud, { type TagCloudElement } from '@/components/controls-list/TagCloud.vue';\nimport SortingList, { type SortingListElement } from '@/components/controls-list/SortingList.vue';\n\nexport default defineComponent({\n  name: 'ControlsList',\n\n  components: {\n    CustomBtn,\n    SearchText,\n    TagCloud,\n    SortingList,\n  },\n\n  props: {\n    placeholder: {\n      type: String,\n      required: true,\n    },\n\n    title: {\n      type: String,\n      required: true,\n    },\n\n    options: {\n      type: Object as PropType<SelectOptions[]>,\n      required: true,\n    },\n\n    tags: {\n      type: Object as PropType<string[]>,\n      required: true,\n    },\n  },\n\n  methods: {\n    clearAll() {\n      const search = this.$refs.search as SearchTextElement;\n      const tags = this.$refs.tags as TagCloudElement;\n      const sorting = this.$refs.sorting as SortingListElement;\n\n      if (search) search.clearSearchText();\n      if (tags) tags.clearSelected();\n      if (sorting) sorting.clearSorting();\n\n      this.$emit('clearAll');\n    },\n  },\n});\n</script>\n\n<style scoped>\n.controls {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/controls-list/PaginationList.vue",
    "content": "<template>\n  <div class=\"pagination\">\n    <div class=\"pagination__per-page\">\n      <label class=\"pagination__label\" for=\"perPage\">{{ $t('pagination.perPage') }}: </label>\n      <select\n        class=\"pagination__select select-element\"\n        name=\"perPage\"\n        id=\"perPage\"\n        v-model=\"perPage\"\n        @change=\"setPerPage\"\n      >\n        <option v-for=\"item in perPageArr\" :key=\"item\" :value=\"item\">{{ item }}</option>\n      </select>\n    </div>\n    <div class=\"pagination__btns\">\n      <custom-btn\n        :text=\"$t('pagination.top')\"\n        :title=\"$t('pagination.topTitle')\"\n        className=\"btn btn-pagination\"\n        @click=\"goTop\"\n        v-shortkey=\"['home']\"\n        @shortkey=\"goTop\"\n        :disabled=\"top\"\n      ></custom-btn>\n      <custom-btn\n        :text=\"$t('pagination.prev')\"\n        :title=\"$t('pagination.prevTitle')\"\n        className=\"btn btn-pagination\"\n        @click=\"goPrev\"\n        v-shortkey=\"['pageup']\"\n        @shortkey=\"goPrev\"\n        :disabled=\"top\"\n      ></custom-btn>\n\n      <div class=\"pagination__page\">\n        <span>{{ currPageNumber }}</span>\n      </div>\n\n      <custom-btn\n        :text=\"$t('pagination.next')\"\n        :title=\"$t('pagination.nextTitle')\"\n        className=\"btn btn-pagination\"\n        @click=\"goNext\"\n        v-shortkey=\"['pagedown']\"\n        @shortkey=\"goNext\"\n        :disabled=\"bottom\"\n      ></custom-btn>\n      <custom-btn\n        :text=\"$t('pagination.bottom')\"\n        :title=\"$t('pagination.bottomTitle')\"\n        className=\"btn btn-pagination\"\n        @click=\"goBottom\"\n        v-shortkey=\"['end']\"\n        @shortkey=\"goBottom\"\n        :disabled=\"bottom\"\n      ></custom-btn>\n    </div>\n    <div class=\"pagination__count\">\n      <h3 class=\"pagination__value\">{{ $t('pagination.count') }}: {{ size }}</h3>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { PAGINATION_OPTIONS } from '@/common/const';\nimport usePagination from '@/stores/pagination';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\nimport { defineComponent } from 'vue';\n\nconst { getPerPage, getCurrPage, setPerPage, setCurrPage } = usePagination();\n\nconst paginationList = defineComponent({\n  name: 'PaginationList',\n\n  components: {\n    CustomBtn,\n  },\n\n  data() {\n    return {\n      perPageArr: PAGINATION_OPTIONS,\n      perPage: getPerPage(),\n      currPageNumber: getCurrPage(),\n    };\n  },\n\n  props: {\n    size: {\n      type: Number,\n      required: true,\n    },\n  },\n\n  computed: {\n    pagesCount(): number {\n      return Math.ceil(this.size / this.perPage);\n    },\n\n    top(): boolean {\n      return this.currPageNumber === 1;\n    },\n\n    bottom(): boolean {\n      return this.currPageNumber === this.pagesCount;\n    },\n  },\n\n  methods: {\n    getPage() {\n      setCurrPage(this.currPageNumber);\n      setPerPage(this.perPage);\n      this.$emit('getPage');\n    },\n\n    setPerPage() {\n      this.currPageNumber = 1;\n      this.getPage();\n    },\n\n    goTop() {\n      if (!this.top) {\n        this.currPageNumber = 1;\n        this.getPage();\n      }\n    },\n\n    goNext() {\n      if (!this.bottom) {\n        if (this.currPageNumber < this.pagesCount) this.currPageNumber += 1;\n        this.getPage();\n      }\n    },\n\n    goPrev() {\n      if (!this.top) {\n        if (this.currPageNumber > 1) this.currPageNumber -= 1;\n        this.getPage();\n      }\n    },\n\n    goBottom() {\n      if (!this.bottom) {\n        this.currPageNumber = this.pagesCount;\n        this.getPage();\n      }\n    },\n  },\n});\nexport default paginationList;\nexport type PaginationListElement = InstanceType<typeof paginationList>;\n</script>\n\n<style>\n.pagination {\n  position: relative;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-right: var(--gap);\n}\n\n.pagination__per-page {\n  flex: 1;\n}\n\n.pagination__select {\n  width: 5rem;\n  text-align: center;\n  color: inherit;\n  background-color: var(--color-background);\n}\n\n.pagination__btns {\n  flex: 1;\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: var(--gap);\n}\n\n.pagination__page {\n  width: 4rem;\n  height: 4rem;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 50%;\n  background-color: var(--color-background);\n}\n\n.pagination__page span {\n  font-weight: bold;\n}\n\n.pagination__count {\n  flex: 1;\n}\n\n.pagination__value {\n  text-align: right;\n}\n\n@media (max-width: 1100px) {\n  .pagination {\n    display: grid;\n    grid-template-columns: repeat(2, auto);\n    grid-template-rows: repeat(2, auto);\n    grid-template-areas:\n      'A B'\n      'C C';\n    justify-content: center;\n    gap: 1rem;\n  }\n\n  .pagination__per-page {\n    grid-area: A;\n  }\n\n  .pagination__count {\n    grid-area: B;\n  }\n\n  .pagination__btns {\n    grid-area: C;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/controls-list/SearchText.vue",
    "content": "<template>\n  <div class=\"search\">\n    <input\n      type=\"text\"\n      class=\"search__text\"\n      :placeholder=\"placeholder\"\n      :title=\"placeholder\"\n      v-model=\"searchText\"\n      @change=\"search\"\n    />\n    <custom-btn @click=\"clearSearch\" text=\"X\" className=\"btn btn-search\"></custom-btn>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport useSearchText from '@/stores/search-text';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\nimport { defineComponent } from 'vue';\n\nconst { getSearchText, setSearchText } = useSearchText();\n\nconst searchText = defineComponent({\n  name: 'SearchText',\n\n  components: {\n    CustomBtn,\n  },\n\n  data() {\n    return {\n      searchText: getSearchText(),\n    };\n  },\n\n  props: {\n    placeholder: {\n      type: String,\n      required: true,\n    },\n  },\n\n  methods: {\n    search() {\n      setSearchText(this.searchText);\n      this.$emit('search');\n    },\n\n    clearSearchText() {\n      this.searchText = '';\n      setSearchText(this.searchText);\n    },\n\n    clearSearch() {\n      this.searchText = '';\n      this.search();\n    },\n  },\n});\nexport default searchText;\nexport type SearchTextElement = InstanceType<typeof searchText>;\n</script>\n\n<style>\n.search {\n  position: relative;\n  color: var(--color-text);\n}\n\n.search__text {\n  padding: 0.5rem;\n  padding-right: 2rem;\n  width: 100%;\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  background-color: var(--color-background);\n  color: inherit;\n  border-radius: 1rem;\n  transition: 0.5s ease;\n}\n\n.search__text,\n.search__text:focus {\n  outline: none;\n}\n\n.search__text:focus {\n  border-color: var(--color-border-inverse);\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/controls-list/SortingList.vue",
    "content": "<template>\n  <div class=\"sorting\">\n    <select name=\"select\" class=\"sorting__select select-element\" v-model=\"sorting\" @change=\"sortingList\">\n      <option disabled value=\"\">{{ title }}</option>\n      <option v-for=\"item in options\" :key=\"item.value\" :value=\"item.value\">{{ $t(item.text) }}</option>\n    </select>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport type { SelectOptions } from '@/common/types';\nimport useSortingList from '@/stores/sorting-list';\nimport { defineComponent, type PropType } from 'vue';\n\nconst { getSortingList, setSortingList } = useSortingList();\n\nconst sortingList = defineComponent({\n  name: 'SortingList',\n\n  data() {\n    return {\n      sorting: getSortingList(),\n    };\n  },\n\n  props: {\n    title: {\n      type: String,\n      required: true,\n    },\n\n    options: {\n      type: Object as PropType<SelectOptions[]>,\n      required: true,\n    },\n  },\n\n  methods: {\n    sortingList() {\n      setSortingList(this.sorting);\n      this.$emit('sorting');\n    },\n\n    clearSorting() {\n      this.sorting = this.options[0].value;\n      setSortingList(this.sorting);\n    },\n  },\n});\nexport default sortingList;\nexport type SortingListElement = InstanceType<typeof sortingList>;\n</script>\n\n<style>\n.sorting {\n  cursor: pointer;\n  color: var(--color-text);\n}\n\n.sorting__select {\n  width: 100%;\n  color: inherit;\n  background-color: var(--color-background);\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  border-radius: 1rem;\n  transition: 0.5s ease;\n}\n\n.sorting__select,\n.sorting__select:focus {\n  outline: none;\n}\n\n.sorting__select:focus {\n  border-color: var(--color-border-inverse);\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/controls-list/TagCloud.vue",
    "content": "<template>\n  <div class=\"tags tags-center\">\n    <span class=\"tag\" :class=\"{ active: isTagSelected(item) }\" v-for=\"item in tags\" :key=\"item\" @click=\"select(item)\">\n      {{ item }}\n    </span>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport useSelectedTags from '@/stores/tag-cloud';\nimport { defineComponent, type PropType } from 'vue';\n\nconst { getSelected, setSelected } = useSelectedTags();\n\nconst tagCloud = defineComponent({\n  name: 'TagCloud',\n\n  data() {\n    return {\n      selected: [] as string[],\n    };\n  },\n\n  props: {\n    tags: {\n      type: Object as PropType<string[]>,\n      required: true,\n    },\n  },\n\n  mounted() {\n    this.selected = getSelected();\n  },\n\n  methods: {\n    isTagSelected(tag: string) {\n      return this.selected.includes(tag);\n    },\n\n    select(tag: string) {\n      const i = this.selected.indexOf(tag);\n      if (i !== -1) {\n        this.selected.splice(i, 1);\n      } else {\n        this.selected.push(tag);\n      }\n\n      setSelected(this.selected);\n      this.$emit('tags');\n    },\n\n    clearSelected() {\n      this.selected = [] as string[];\n\n      setSelected(this.selected);\n    },\n  },\n});\nexport default tagCloud;\nexport type TagCloudElement = InstanceType<typeof tagCloud>;\n</script>\n\n<style>\n.tags {\n  display: flex;\n  flex-wrap: wrap;\n  flex-direction: row;\n  align-items: center;\n  justify-content: flex-start;\n  gap: calc(var(--gap) / 2);\n  color: var(--color-text);\n}\n\n.tags-center {\n  justify-content: center;\n}\n\n.tag {\n  padding: 0.5rem 0.7rem;\n  cursor: pointer;\n  color: inherit;\n  background-color: var(--color-background);\n  transition:\n    background-color 0.3s ease,\n    color 0.5s ease;\n  border-radius: 1rem;\n}\n\n.tag:hover,\n.active {\n  color: var(--color-text-inverse);\n  background-color: var(--color-background-inverse);\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/footer/FooterView.vue",
    "content": "<template>\n  <footer class=\"footer\" :class=\"`footer-${currTheme}`\">\n    <a class=\"rss\" href=\"https://rs.school/js/\" target=\"_blank\"></a>\n    <div class=\"team\">\n      <a class=\"team__item link\" href=\"https://github.com/WiiJoy\" target=\"_blank\">WiiJoy</a>\n      <a class=\"team__item link\" href=\"https://github.com/Ogimly\" target=\"_blank\">Ogimly</a>\n      <a class=\"team__item link\" href=\"https://github.com/ThorsAngerVaNeT\" target=\"_blank\">VaNeT</a>\n      <p class=\"team__item\">© 2022</p>\n    </div>\n  </footer>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapWritableState } from 'pinia';\n\nimport useThemeProp from '../../stores/theme';\n\nexport default defineComponent({\n  name: 'FooterView',\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n  },\n});\n</script>\n\n<style scoped>\n.footer {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 3rem;\n  z-index: 3;\n  transition: 0.5s ease;\n}\n\n.footer-light {\n  background: repeat-x left bottom / contain url('../../assets/backgrounds/bg-footer-light.svg');\n}\n\n.footer-dark {\n  background: repeat-x left bottom / contain url('../../assets/backgrounds/bg-footer-dark.svg');\n}\n\n.rss {\n  height: 2rem;\n  width: 5rem;\n  background: center center / contain url('../../assets/icons/rs-school.png') no-repeat;\n}\n\n.team {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  gap: 1rem;\n}\n\n.team__item {\n  color: var(--color-text);\n  text-decoration: none;\n  font-size: 1.2rem;\n  transition: 0.3s;\n}\n\n.link:hover {\n  color: var(--color-heading);\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/guess/GuessInfo.vue",
    "content": "<template>\n  <div class=\"game-info\">\n    <div class=\"game-info__title\">{{ $t('results.user') }}</div>\n    <div class=\"game-info__btns\">\n      <custom-btn\n        v-for=\"(indexAll, index) in sortingOptions\"\n        :key=\"`${index}_${sortingOptionsAll[indexAll].text}`\"\n        :text=\"$t(sortingOptionsAll[indexAll].text)\"\n        className=\"btn btn-primary\"\n        @click=\"setSorting(index)\"\n      ></custom-btn>\n    </div>\n\n    <div class=\"results results_admin\">\n      <div class=\"results__item\" v-for=\"(res, index) in results\" :key=\"`${index}_${res.count}`\">\n        <span class=\"result__index\">{{ `${index + 1}.` }}</span>\n        <span class=\"result__steps\">{{ `${res.count} ${$t('guess.points', res.count)}` }}</span>\n        <span class=\"result__time\">{{ `${res.time / millisecondsInSecond} s` }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport type { GameResult } from '@/common/types';\nimport { defineComponent } from 'vue';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\nimport { GAME_RESULT_SORTING, MILLISECONDS_IN_SECOND } from '@/common/const';\nimport useLoader from '@/stores/loader';\nimport { mapWritableState } from 'pinia';\nimport isEven from '@/utils/game-utils';\nimport sortMixins from '@/components/mixins/sort-mixin';\n\nexport default defineComponent({\n  name: 'GuessInfo',\n\n  mixins: [sortMixins],\n\n  components: {\n    CustomBtn,\n  },\n\n  data() {\n    return {\n      count: 0,\n      results: [] as GameResult[],\n      sortingOptionsAll: GAME_RESULT_SORTING,\n      sortingOptions: [] as number[],\n      sorting: 0,\n    };\n  },\n\n  props: {\n    isVisible: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useLoader, ['isLoad']),\n\n    millisecondsInSecond(): number {\n      return MILLISECONDS_IN_SECOND;\n    },\n  },\n\n  watch: {\n    isVisible() {\n      this.getGameInfo();\n      this.takeSort();\n    },\n  },\n\n  async mounted() {\n    this.getGameInfo();\n\n    this.sortingOptions = this.sortingOptionsAll.map((el, i) => i).filter(el => isEven(el));\n  },\n\n  methods: {\n    getGameInfo() {\n      const levelData = localStorage.getItem('rs-sloths-guess');\n\n      if (levelData) {\n        this.results = JSON.parse(levelData);\n      }\n\n      this.count = this.results.length;\n    },\n\n    setSorting(i: number) {\n      if (isEven(this.sortingOptions[i])) {\n        this.sortingOptions[i] += 1;\n      } else {\n        this.sortingOptions[i] -= 1;\n      }\n\n      this.sorting = this.sortingOptions[i];\n\n      this.takeSort();\n    },\n\n    takeSort() {\n      this.results.sort((a, b) => {\n        const item1: number = this.sortTypes(this.sorting, a);\n        const item2: number = this.sortTypes(this.sorting, b);\n\n        return this.sortElems(item1, item2, this.sorting);\n      });\n    },\n  },\n});\n</script>\n\n<style scoped>\n.game-info {\n  display: flex;\n  flex-direction: column;\n  gap: 2rem;\n}\n\n.game-info__title {\n  padding-top: 1rem;\n  color: var(--color-text);\n  font-size: 2.4rem;\n  transition: 0.5s ease;\n}\n\n.game-info__btns {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n  gap: var(--gap);\n}\n\n.results {\n  padding: 1rem;\n  width: 28rem;\n  min-height: 25rem;\n  max-height: 34rem;\n  overflow-y: auto;\n  border-radius: 1rem;\n  border: 0.2rem solid var(--color-border-inverse);\n  background-color: var(--color-background-opacity);\n  color: var(--color-text);\n  transition: 0.5s ease;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 1rem;\n}\n.results_admin {\n  width: 40rem;\n}\n\n.results__item {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 2rem;\n  width: 100%;\n  color: var(--color-text);\n}\n\n.result__index {\n  width: 3rem;\n  font-weight: 700;\n}\n\n.result__user {\n  width: 11rem;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.result__steps {\n  width: 8rem;\n}\n\n.result__time {\n  width: 8rem;\n}\n\n.game-info__again {\n  display: flex;\n  align-items: center;\n  gap: 2rem;\n  transition: 0.5s ease;\n}\n\n@media (max-width: 1200px) {\n  .game-info__title {\n    text-align: center;\n  }\n\n  .results,\n  .game-info__again {\n    justify-content: center;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/header/HeaderView.vue",
    "content": "<template>\n  <header class=\"header\" :class=\"moreClass\">\n    <router-link class=\"header__title\" v-show=\"!home\" to=\"/\">RS SLOTHS</router-link>\n    <h2 v-show=\"!home\" class=\"section__title\">{{ routeTitle }}</h2>\n    <div class=\"header__tools\">\n      <sound-switcher />\n      <theme-switcher />\n    </div>\n  </header>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport ThemeSwitcher from '../switchers/ThemeSwitcher.vue';\nimport SoundSwitcher from '../switchers/SoundSwitcher.vue';\n\nexport default defineComponent({\n  name: 'HeaderView',\n\n  components: { ThemeSwitcher, SoundSwitcher },\n\n  computed: {\n    currRoute(): string {\n      return String(this.$route.name);\n    },\n    home(): boolean {\n      return this.currRoute === 'home';\n    },\n    moreClass(): string {\n      return !this.home ? '' : 'header_home';\n    },\n    routeTitle(): string {\n      return this.$route.name ? this.$t(`${this.currRoute}.title`) : '';\n    },\n  },\n});\n</script>\n\n<style scoped>\n.header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 3rem;\n  z-index: 3;\n}\n\n.header_home {\n  justify-content: flex-end;\n}\n\n.header__title {\n  font-family: Arial, sans-serif;\n  font-weight: 900;\n  font-size: 2.4rem;\n  text-decoration: none;\n  color: var(--color-text);\n  transition: 0.5s ease;\n}\n\n.header__title:hover {\n  color: var(--color-heading);\n}\n\n.header__tools {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  gap: 2rem;\n}\n\n.section__title {\n  font-weight: 100;\n  text-transform: uppercase;\n  color: var(--color-text);\n  user-select: none;\n  cursor: default;\n  transition: 0.5s ease;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/home/HomeAbout.vue",
    "content": "<template>\n  <div class=\"home__about\">\n    <div class=\"home__logo\"></div>\n    <div class=\"home__about__wrap\">\n      <input class=\"home__radio\" type=\"radio\" name=\"about-radio\" id=\"slide1\" checked />\n      <input class=\"home__radio\" type=\"radio\" name=\"about-radio\" id=\"slide2\" />\n\n      <div class=\"home__slider\">\n        <div class=\"home__slide home__slide_1\">{{ $t('home.about.project') }}</div>\n        <div class=\"home__slide home__slide_2\">{{ $t('home.about.team') }}</div>\n      </div>\n\n      <div class=\"home__controls\">\n        <label for=\"slide1\"><span class=\"home__label\"></span></label>\n        <label for=\"slide2\"><span class=\"home__label\"></span></label>\n      </div>\n    </div>\n    <custom-btn :text=\"$t('home.more')\" className=\"btn btn-primary\" :onClick=\"handleBtnClick\"></custom-btn>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport CustomBtn from '../buttons/CustomBtn.vue';\n\nexport default defineComponent({\n  name: 'HomeAbout',\n\n  components: {\n    CustomBtn,\n  },\n\n  methods: {\n    handleBtnClick() {\n      this.$router.push({ name: 'about' });\n    },\n  },\n});\n</script>\n\n<style scoped>\n.home__about {\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  gap: 2rem;\n}\n\n.home__logo {\n  width: 30rem;\n  height: 30rem;\n  justify-self: start;\n  background: no-repeat center center / contain url('/img/logo.svg');\n}\n\n.home__radio {\n  display: none;\n}\n\n.home__about__wrap {\n  position: relative;\n  margin: 0 auto;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n}\n\n.home__slider {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n  height: 12rem;\n}\n\n.home__slide {\n  position: absolute;\n  opacity: 0;\n  transition: 0.5s;\n  color: var(--color-text);\n  font-weight: 300;\n  font-size: 2rem;\n}\n\n.home__controls {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 1rem;\n}\n\n.home__controls label {\n  box-sizing: unset;\n  cursor: pointer;\n  width: 1.5rem;\n  height: 1.5rem;\n  padding: 1.5rem;\n  cursor: default;\n}\n\n.home__label {\n  display: block;\n  border: 0.3rem solid var(--color-border-inverse);\n  height: 100%;\n  width: 100%;\n  border-radius: 50%;\n  background-color: var(--color-border-inverse);\n  transition: 0.3s;\n}\n\n#slide1:checked ~ .home__controls [for='slide1'] .home__label,\n#slide2:checked ~ .home__controls [for='slide2'] .home__label {\n  background-color: var(--sloth-main);\n}\n\n#slide1:not(#slide1:checked) ~ .home__controls [for='slide1']:hover,\n#slide2:not(#slide2:checked) ~ .home__controls [for='slide2']:hover {\n  cursor: pointer;\n}\n\n#slide1:not(#slide1:checked) ~ .home__controls [for='slide1']:hover .home__label,\n#slide2:not(#slide2:checked) ~ .home__controls [for='slide2']:hover .home__label {\n  background-color: var(--green-active);\n  border-color: var(--green-active);\n}\n\n#slide1:not(#slide1:checked) ~ .home__controls [for='slide1']:active .home__label,\n#slide2:not(#slide2:checked) ~ .home__controls [for='slide2']:active .home__label {\n  background-color: var(--blue-main);\n  border-color: var(--blue-main);\n}\n\n#slide1:checked ~ .home__slider > .home__slide_1 {\n  opacity: 1;\n}\n\n#slide2:checked ~ .home__slider > .home__slide_2 {\n  opacity: 1;\n}\n\n@media (max-width: 1200px) {\n  .home__about {\n    grid-area: A;\n  }\n}\n\n@media (max-width: 420px) {\n  .home__slider {\n    height: 14rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/home/HomeCatalog.vue",
    "content": "<template>\n  <div class=\"home-catalog\">\n    <div class=\"home-catalog__wrapper\">\n      <div class=\"home-catalog_up\"></div>\n      <div class=\"home-catalog_down\"></div>\n    </div>\n    <div class=\"home-catalog__name\">{{ $t('catalog.title') }}</div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'HomeCatalog',\n});\n</script>\n\n<style scoped>\n.home-catalog {\n  display: flex;\n  flex-direction: column;\n  gap: 2rem;\n  cursor: pointer;\n}\n\n.home-catalog__wrapper {\n  position: relative;\n  width: 40rem;\n  height: 40rem;\n}\n\n.home-catalog_up,\n.home-catalog_down {\n  position: absolute;\n  width: 100%;\n  height: 24rem;\n  transition: 0.3s;\n}\n\n.home-catalog_up {\n  top: 50%;\n  transform: translate(0, -50%);\n  z-index: 2;\n  background: no-repeat center center / contain url('/img/home/catalog-up.svg');\n}\n\n.home-catalog_down {\n  bottom: 50%;\n  transform: translate(0, 50%);\n  z-index: 3;\n  background: no-repeat center center / contain url('/img/home/catalog-down.svg');\n}\n\n.home-catalog__name {\n  text-align: center;\n  opacity: 0;\n  color: var(--color-text);\n  font-size: 2.4rem;\n  font-weight: 300;\n  transition: 0.3s;\n}\n\n.home-catalog:hover .home-catalog_up {\n  top: 0;\n  transform: translate(0, 0);\n}\n\n.home-catalog:hover .home-catalog_down {\n  bottom: 0;\n  transform: translate(0, 0);\n}\n\n.home-catalog:hover .home-catalog__name {\n  opacity: 1;\n}\n\n@media (max-width: 1400px) {\n  .home-catalog {\n    grid-area: B;\n  }\n}\n\n@media (max-width: 768px) {\n  .home-catalog_up {\n    top: 0;\n    transform: translate(0, 0);\n  }\n\n  .home-catalog_down {\n    bottom: 0;\n    transform: translate(0, 0);\n  }\n\n  .home-catalog__name {\n    opacity: 1;\n  }\n}\n\n@media (max-width: 420px) {\n  .home-catalog__wrapper {\n    width: 30rem;\n    height: 36rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/home/HomeCategory.vue",
    "content": "<template>\n  <div :class=\"`home-category home-category_${category}`\">\n    <div class=\"home-category__img\"></div>\n    <div class=\"home-category__name\">{{ $t(`${category}.title`) }}</div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'HomeCategory',\n\n  props: {\n    category: {\n      type: String,\n      default: () => '',\n    },\n  },\n});\n</script>\n\n<style scoped>\n.home-category {\n  width: 15rem;\n  display: flex;\n  flex-direction: column;\n  gap: 1.5rem;\n  cursor: pointer;\n}\n\n.home-category__img {\n  height: 15rem;\n  width: 100%;\n  border-radius: 50%;\n  border: 0.3rem var(--color-border-inverse) solid;\n  background-color: var(--color-background-soft);\n  transition: 0.3s;\n}\n\n.home-category_memory > .home-category__img {\n  background: no-repeat center center / contain url('/img/home/memory.svg') var(--color-background-soft);\n}\n\n.home-category_guess > .home-category__img {\n  background: no-repeat center center / contain url('/img/home/guess.svg') var(--color-background-soft);\n}\n\n.home-category_create > .home-category__img {\n  background: no-repeat center center / contain url('/img/home/create.svg') var(--color-background-soft);\n}\n\n.home-category_merch > .home-category__img {\n  background: no-repeat center center / contain url('/img/home/merch.svg') var(--color-background-soft);\n}\n\n.home-category__name {\n  text-align: center;\n  color: var(--color-text);\n  font-weight: 300;\n  font-size: 2rem;\n  opacity: 0;\n  transition: 0.3s;\n}\n\n.home-category:hover .home-category_up {\n  top: 20%;\n  transform: translate(0, 0);\n}\n\n.home-category:hover .home-category_down {\n  top: none;\n  bottom: 20%;\n  transform: translate(0, 0);\n}\n\n.home-category:hover .home-category__img {\n  transform: scale(1.1) rotate(-10deg);\n}\n\n.home-category:hover .home-category__name {\n  opacity: 1;\n}\n\n@media (max-width: 768px) {\n  .home-category__name {\n    opacity: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/loader/LoaderView.vue",
    "content": "<template>\n  <div class=\"loader\">\n    <div class=\"loader__wrap\">\n      <div v-for=\"n in 8\" :key=\"`sloth_${n}`\" class=\"sloth-loader\" :class=\"`sloth-${n}`\"></div>\n    </div>\n    <span class=\"loader__text\">{{ $t('loader.text') }}</span>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'LoaderView',\n});\n</script>\n\n<style scoped>\n.loader {\n  position: absolute;\n  height: 100vh;\n  width: 100vw;\n  z-index: 1000;\n  background-color: var(--dark-opacity);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  gap: 2rem;\n}\n\n.loader__wrap {\n  width: 10rem;\n  height: 10rem;\n  position: relative;\n}\n\n.loader__wrap > .sloth-loader {\n  position: absolute;\n  height: 2rem;\n  width: 2.2rem;\n  background: no-repeat center center / contain url('../../assets/icons/loader/loader-point.svg');\n  opacity: 0;\n  animation-name: scaleAnim;\n  animation-duration: 1.2s;\n  animation-iteration-count: infinite;\n}\n\n.sloth-1 {\n  left: 0.3rem;\n  top: 4rem;\n  animation-delay: 0.45s;\n}\n\n.sloth-2 {\n  left: 1.5rem;\n  top: 1.5rem;\n  animation-delay: 0.6s;\n}\n\n.sloth-3 {\n  left: 4rem;\n  top: 0.3rem;\n  animation-delay: 0.75s;\n}\n\n.sloth-4 {\n  right: 1.5rem;\n  top: 1.5rem;\n  animation-delay: 0.9s;\n}\n\n.sloth-5 {\n  right: 0.3rem;\n  top: 4rem;\n  animation-delay: 1.05s;\n}\n\n.sloth-6 {\n  right: 1.5rem;\n  bottom: 1.5rem;\n  animation-delay: 1.2s;\n}\n\n.sloth-7 {\n  left: 4rem;\n  bottom: 0.3rem;\n  animation-delay: 1.35s;\n}\n\n.sloth-8 {\n  left: 1.5rem;\n  bottom: 1.5rem;\n  animation-delay: 1.5s;\n}\n\n@keyframes scaleAnim {\n  0% {\n    transform: scale(1.6);\n    opacity: 1;\n  }\n  100% {\n    transform: scale(1);\n    opacity: 0;\n  }\n}\n\n.loader__text {\n  color: var(--color-text-inverse);\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/memory/GameField.vue",
    "content": "<template>\n  <div class=\"game-field\">\n    <div class=\"game-field__tools\">\n      <div class=\"game-field__steps\">\n        <p>{{ steps }}</p>\n      </div>\n      <image-btn\n        :imgPath=\"`./img/memory/reload-${currTheme}.svg`\"\n        :text=\"$t('memory.start')\"\n        className=\"btn btn-img btn-memory\"\n        :disabled=\"steps === 0\"\n        :onClick=\"startGame\"\n      ></image-btn>\n    </div>\n    <transition-group name=\"shuffle-list\" tag=\"div\" class=\"game-field__cards\">\n      <div\n        class=\"game-field__card\"\n        :class=\"`game-field__card_${level.level}`\"\n        v-for=\"(item, index) in cards\"\n        :key=\"item.index\"\n        :item=\"item\"\n        @click=\"gameHandler(index)\"\n      >\n        <transition name=\"flip\" mode=\"out-in\" @before-leave=\"isAnimated = true\" @after-enter=\"isAnimated = false\">\n          <svg v-if=\"!getIsOpen(index)\" alt=\"cover\" class=\"game-field__img\" :class=\"{ success: item.success }\">\n            <use :xlink:href=\"`${memorySprite}#card-cover`\"></use>\n          </svg>\n          <svg v-else alt=\"card\" class=\"game-field__img\" :class=\"{ success: item.success }\">\n            <use :xlink:href=\"getImage(index)\"></use>\n          </svg>\n        </transition>\n      </div>\n    </transition-group>\n    <modal-window v-show=\"modalVisible\" @close=\"closeModal\">\n      <template v-slot:header> {{ $t('memory.congrats') }} </template>\n\n      <template v-slot:body>\n        <img :src=\"cardWinner\" alt=\"winner\" class=\"modal-body__img\" />\n        <p>{{ $t('memory.win') }}</p>\n        <p>{{ steps }} {{ $t('memory.steps', steps) }}</p>\n        <p>{{ gameTime / millisecondsInSecond }} {{ $t('memory.time') }}</p>\n      </template>\n    </modal-window>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { mapWritableState } from 'pinia';\nimport {\n  MEMORY_GAME_SPRITE,\n  MEMORY_GAME_TIMEOUT,\n  MEMORY_GAME_WINNER,\n  MEMORY_LEVELS,\n  MILLISECONDS_IN_SECOND,\n} from '@/common/const';\nimport type { MemoryLevel, GameResult } from '@/common/types';\nimport { defineComponent, type PropType } from 'vue';\nimport ModalWindow from '@/components/modal/ModalWindow.vue';\nimport ImageBtn from '@/components/buttons/ImageBtn.vue';\nimport { playAudio, audioSlide, audioFlip, audioFail, audioSuccess, audioWin } from '@/utils/audio';\nimport useThemeProp from '../../stores/theme';\n\ntype Card = {\n  img: string;\n  index: number;\n  id: number;\n  open: boolean;\n  success: boolean;\n};\n\nexport default defineComponent({\n  name: 'GameField',\n\n  components: {\n    ImageBtn,\n    ModalWindow,\n  },\n\n  data() {\n    return {\n      memorySprite: MEMORY_GAME_SPRITE,\n      cardWinner: MEMORY_GAME_WINNER,\n      images: [] as string[],\n      cards: [] as Card[],\n      activeCard: Infinity,\n      steps: 0,\n      startTime: 0,\n      endTime: 0,\n      grid: '1000px',\n      isHandled: false,\n      isAnimated: false,\n      isModalVisible: false,\n    };\n  },\n\n  props: {\n    level: {\n      type: Object as PropType<MemoryLevel>,\n      default: MEMORY_LEVELS[1],\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n\n    gameTime(): number {\n      return this.endTime - this.startTime;\n    },\n\n    modalVisible(): boolean {\n      return this.isModalVisible && !this.isAnimated && !this.isHandled;\n    },\n\n    millisecondsInSecond(): number {\n      return MILLISECONDS_IN_SECOND;\n    },\n  },\n\n  mounted() {\n    this.setGrid();\n    this.getImages();\n    this.startGame();\n  },\n\n  watch: {\n    level() {\n      this.setGrid();\n      this.getCards();\n      this.startGame();\n    },\n  },\n\n  methods: {\n    getImages() {\n      this.images = [\n        `${MEMORY_GAME_SPRITE}#memory01`,\n        `${MEMORY_GAME_SPRITE}#memory02`,\n        `${MEMORY_GAME_SPRITE}#memory03`,\n        `${MEMORY_GAME_SPRITE}#memory04`,\n        `${MEMORY_GAME_SPRITE}#memory05`,\n        `${MEMORY_GAME_SPRITE}#memory06`,\n        `${MEMORY_GAME_SPRITE}#memory07`,\n        `${MEMORY_GAME_SPRITE}#memory08`,\n        `${MEMORY_GAME_SPRITE}#memory09`,\n        `${MEMORY_GAME_SPRITE}#memory10`,\n        `${MEMORY_GAME_SPRITE}#memory11`,\n        `${MEMORY_GAME_SPRITE}#memory12`,\n      ];\n      this.images = Array.from({ length: 12 }, (x, i) => `${MEMORY_GAME_SPRITE}#memory${i + 1}`);\n\n      this.getCards();\n    },\n\n    getCards() {\n      this.changeScrollHidden('hidden');\n      this.cards = [];\n      let index = 0;\n\n      const images = this.images.sort(() => Math.random() - 0.5).filter((el, i) => i < this.level.n);\n\n      images.forEach((el, i) => {\n        this.pushTwoCards(el, i, index);\n        index += 2;\n      });\n      setTimeout(() => this.changeScrollHidden(''), MEMORY_GAME_TIMEOUT);\n    },\n\n    changeScrollHidden(val = '') {\n      const mainEl: HTMLElement | null = document.querySelector('.main');\n      if (mainEl instanceof HTMLElement) {\n        mainEl.style.overflowY = val;\n      }\n    },\n\n    startGame() {\n      const isAllClosed = this.cards.every(el => !el.open);\n\n      if (!isAllClosed) {\n        playAudio(audioFlip);\n        this.cards.forEach((el, i) => this.closeCard(i));\n      }\n\n      this.activeCard = Infinity;\n      this.steps = 0;\n      this.startTime = 0;\n      this.endTime = 0;\n\n      if (isAllClosed) {\n        this.shuffleCards();\n      } else {\n        setTimeout(() => this.shuffleCards(), MEMORY_GAME_TIMEOUT);\n      }\n      // shuffle 2 times\n      setTimeout(() => this.shuffleCards(), MEMORY_GAME_TIMEOUT / 2);\n    },\n\n    getImage(i: number): string {\n      return this.cards[i].img;\n    },\n\n    getIsOpen(i: number): boolean {\n      return this.cards[i].open;\n    },\n\n    checkGameHandler(i: number): boolean {\n      return !(this.cards[i].open || this.isHandled || this.isAnimated);\n    },\n\n    cardsNotMatched(i1: number, i2: number) {\n      this.isHandled = true;\n\n      setTimeout(() => {\n        playAudio(audioFail);\n\n        this.closeCard(i1);\n        this.closeCard(i2);\n\n        this.isHandled = false;\n      }, MEMORY_GAME_TIMEOUT);\n    },\n\n    cardsMatched(i1: number, i2: number) {\n      this.isHandled = true;\n\n      setTimeout(() => {\n        playAudio(audioSuccess);\n\n        this.cards[i1].success = true;\n        this.cards[i2].success = true;\n\n        this.isHandled = false;\n      }, MEMORY_GAME_TIMEOUT);\n    },\n\n    gameHandler(i: number) {\n      if (this.startTime === 0) this.startTime = Date.now();\n\n      if (!this.checkGameHandler(i)) return;\n\n      playAudio(audioFlip);\n      this.openCard(i);\n      this.steps += 1;\n\n      if (this.activeCard === Infinity) {\n        this.activeCard = i;\n      } else if (this.cards[i].id === this.cards[this.activeCard].id) {\n        if (!this.isWin()) this.cardsMatched(i, this.changeActiveCard());\n      } else {\n        this.cardsNotMatched(i, this.changeActiveCard());\n      }\n\n      if (this.isWin()) {\n        this.endTime = Date.now();\n\n        setTimeout(() => {\n          playAudio(audioWin);\n          this.isModalVisible = true;\n          this.saveResult();\n        }, 0);\n      }\n    },\n\n    isWin(): boolean {\n      return this.cards.every(el => el.open);\n    },\n\n    shuffleCards() {\n      playAudio(audioSlide);\n      this.cards.sort(() => Math.random() - 0.5);\n    },\n\n    pushTwoCards(img: string, id: number, index: number) {\n      this.cards.push({ img, id, index, open: false, success: false });\n      this.cards.push({ img, id, index: index + 1, open: false, success: false });\n    },\n\n    changeActiveCard(): number {\n      const { activeCard } = this;\n      this.activeCard = Infinity;\n      return activeCard;\n    },\n\n    openCard(i: number) {\n      this.cards[i].open = true;\n    },\n\n    closeCard(i: number) {\n      this.cards[i].open = false;\n      this.cards[i].success = false;\n    },\n\n    saveResult() {\n      let currResults: GameResult[] = [];\n      const savedRecords = localStorage.getItem(`rs-sloths-memory-${this.level.level}`);\n\n      if (savedRecords) {\n        currResults = JSON.parse(savedRecords);\n      }\n\n      const gameResult: GameResult = {\n        count: this.steps,\n        time: this.gameTime,\n        createdAt: new Date().getTime(),\n      };\n\n      currResults.unshift(gameResult);\n\n      if (currResults.length > 10) {\n        currResults.pop();\n      }\n\n      localStorage.setItem(`rs-sloths-memory-${this.level.level}`, JSON.stringify(currResults));\n    },\n\n    closeModal() {\n      this.isModalVisible = false;\n    },\n\n    setGrid() {\n      this.grid = this.level.n <= 4 ? '670px' : '1000px';\n    },\n  },\n});\n</script>\n\n<style scoped>\n.game-field {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 1rem;\n}\n\n.game-field__tools {\n  padding: 0.5rem;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 3rem;\n}\n\n.game-field__steps {\n  width: 60px;\n  height: 60px;\n  font-size: 2em;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background-color: var(--color-background-inverse-soft);\n  border-radius: 50%;\n  color: var(--color-text-inverse);\n  transition: 0.5s ease;\n}\n\n.game-field__cards {\n  max-width: v-bind(grid);\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: center;\n  gap: 1em;\n}\n\n.game-field__card {\n  width: 150px;\n  height: 200px;\n  cursor: pointer;\n  perspective: 600px;\n}\n\n.game-field__card_middle {\n  width: 125px;\n  height: 165px;\n}\n\n.game-field__card_senior {\n  width: 100px;\n  height: 133px;\n}\n\n.game-field__img {\n  position: absolute;\n  display: inline-block;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  border-radius: 1em;\n  background-color: lightgray;\n  padding: 0.2rem;\n}\n\n.game-field__img:hover {\n  box-shadow: 0px 0px 5px var(--color-text);\n}\n\n@media (max-width: 1200px) {\n  .game-field__card {\n    width: 100px;\n    height: 133px;\n    cursor: pointer;\n    perspective: 600px;\n  }\n\n  .game-field__card_middle {\n    width: 70px;\n    height: 94px;\n  }\n\n  .game-field__card_senior {\n    width: 70px;\n    height: 94px;\n  }\n}\n\n.success {\n  animation: rainbow 0.5s;\n}\n\n/* Animations */\n.shuffle-list-move {\n  transition: transform 0.6s;\n}\n\n.flip-enter-active {\n  animation: flip-out 0.2s;\n}\n\n.flip-leave-active {\n  animation: flip-in 0.2s;\n}\n\n@keyframes flip-in {\n  0% {\n    transform: rotateY(0deg);\n    transform-style: preserve-3d;\n  }\n  100% {\n    transform: rotateY(90deg);\n    transform-style: preserve-3d;\n  }\n}\n\n@keyframes flip-out {\n  0% {\n    transform: rotateY(270deg);\n    transform-style: preserve-3d;\n  }\n  100% {\n    transform: rotateY(360deg);\n    transform-style: preserve-3d;\n  }\n}\n\n@keyframes rainbow {\n  0% {\n    transform: scale(1);\n    box-shadow: 0 0 5px 5px rgb(255, 255, 0);\n  }\n  33% {\n    transform: scale(1.05);\n    box-shadow: 0 0 5px 5px rgba(0, 0, 255, 0.75);\n  }\n  66% {\n    transform: scale(1.025);\n    box-shadow: 0 0 5px 5px rgba(255, 0, 0, 0.5);\n  }\n  100% {\n    transform: scale(1);\n    box-shadow: 0 0 5px 5px rgba(255, 0, 0, 0);\n  }\n}\n\n.modal-body__img {\n  width: 100%;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/memory/MemoryInfo.vue",
    "content": "<template>\n  <div class=\"game-info\">\n    <div class=\"game-info__title\">{{ $t('results.user') }}</div>\n    <div class=\"game-info__btns\">\n      <custom-btn\n        v-for=\"(indexAll, index) in sortingOptions\"\n        :key=\"`${index}_${sortingOptionsAll[indexAll].text}`\"\n        :text=\"$t(sortingOptionsAll[indexAll].text)\"\n        className=\"btn btn-primary\"\n        @click=\"setSorting(index)\"\n      ></custom-btn>\n    </div>\n\n    <div class=\"game-info__wrap\">\n      <div\n        class=\"game-info__level game-info__level_admin\"\n        v-for=\"(res, index) in gameResults\"\n        :key=\"`${index}_${res.level}`\"\n      >\n        <h4 class=\"result__level__title\">{{ $t(`memory.${res.level}`) }}</h4>\n        <div class=\"game-info__result\" v-for=\"(r, i) in res.results\" :key=\"r.id\">\n          <span class=\"result__index\">{{ `${i + 1}.` }}</span>\n          <span class=\"result__steps\">{{ `${r.count} ${$t('memory.steps', r.count)}` }}</span>\n          <span class=\"result__time\">{{ `${r.time / millisecondsInSecond} s` }}</span>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapWritableState } from 'pinia';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\nimport { GAME_RESULT_SORTING, MEMORY_LEVELS, MILLISECONDS_IN_SECOND } from '@/common/const';\nimport type { GameResult, MemoryLevel } from '@/common/types';\nimport useLoader from '@/stores/loader';\nimport isEven from '@/utils/game-utils';\nimport sortMixins from '@/components/mixins/sort-mixin';\n\ntype MemoryLevelResult = MemoryLevel & { count: number; results: GameResult[] };\n\nexport default defineComponent({\n  name: 'MemoryInfo',\n\n  mixins: [sortMixins],\n\n  components: {\n    CustomBtn,\n  },\n\n  data() {\n    return {\n      gameResults: [] as MemoryLevelResult[],\n      sortingOptionsAll: GAME_RESULT_SORTING,\n      sortingOptions: [] as number[],\n      sorting: 0,\n    };\n  },\n\n  props: {\n    isVisible: {\n      type: Boolean,\n      default: false,\n    },\n  },\n\n  computed: {\n    ...mapWritableState(useLoader, ['isLoad']),\n\n    millisecondsInSecond(): number {\n      return MILLISECONDS_IN_SECOND;\n    },\n  },\n\n  watch: {\n    isVisible() {\n      this.getGameInfo();\n      this.takeSort();\n    },\n  },\n\n  async mounted() {\n    this.getGameInfo();\n\n    this.sortingOptions = this.sortingOptionsAll.map((el, i) => i).filter(el => isEven(el));\n  },\n\n  methods: {\n    getGameInfo() {\n      this.gameResults = [];\n      MEMORY_LEVELS.forEach(item => {\n        let levelRecords: GameResult[] = [];\n        const levelData = localStorage.getItem(`rs-sloths-memory-${item.level}`);\n\n        if (levelData) {\n          levelRecords = JSON.parse(levelData);\n        }\n\n        this.gameResults.push({\n          level: item.level,\n          n: item.n,\n          count: levelRecords.length,\n          results: levelRecords,\n        });\n      });\n    },\n\n    setSorting(i: number) {\n      if (isEven(this.sortingOptions[i])) {\n        this.sortingOptions[i] += 1;\n      } else {\n        this.sortingOptions[i] -= 1;\n      }\n\n      this.sorting = this.sortingOptions[i];\n\n      this.takeSort();\n    },\n\n    takeSort() {\n      this.gameResults.forEach(gameResult => {\n        gameResult.results.sort((a, b) => {\n          const item1: number = this.sortTypes(this.sorting, a);\n          const item2: number = this.sortTypes(this.sorting, b);\n\n          return this.sortElems(item1, item2, this.sorting);\n        });\n      });\n    },\n  },\n});\n</script>\n\n<style scoped>\n.game-info {\n  display: flex;\n  flex-direction: column;\n  gap: 2rem;\n}\n\n.game-info__title {\n  padding-top: 1rem;\n  color: var(--color-text);\n  font-size: 2.4rem;\n  transition: 0.5s ease;\n}\n\n.game-info__btns {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n  gap: var(--gap);\n}\n\n.game-info__wrap {\n  display: flex;\n  gap: 2rem;\n}\n\n.game-info__level {\n  width: 25rem;\n  min-height: 25rem;\n  max-height: 34rem;\n  overflow-y: auto;\n  border-radius: 1rem;\n  border: 0.2rem solid var(--color-border-inverse);\n  background-color: var(--color-background-opacity);\n  color: var(--color-text);\n  transition: 0.5s ease;\n}\n\n.game-info__level_admin {\n  width: 37rem;\n}\n\n.result__level__title {\n  text-align: center;\n  padding: 1rem;\n  font-size: 2.4rem;\n  transition: 0.5s ease;\n}\n\n.game-info__result {\n  padding: 0.5rem 1rem;\n  display: flex;\n  justify-content: space-between;\n  gap: 1rem;\n  color: var(--color-text);\n  transition: 0.5s ease;\n}\n\n.result__index {\n  width: 3rem;\n}\n\n.result__user {\n  width: 11rem;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.result__steps {\n  flex: 1;\n}\n\n.result__time {\n  width: 8rem;\n}\n\n.game-info__again {\n  display: flex;\n  align-items: center;\n  gap: 2rem;\n  transition: 0.5s ease;\n}\n\n@media (max-width: 1200px) {\n  .game-info__title {\n    text-align: center;\n  }\n\n  .game-info__wrap {\n    justify-content: center;\n    flex-wrap: wrap;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/mixins/sort-mixin.ts",
    "content": "import isEven from '@/utils/game-utils';\nimport type { GameResult } from '@/common/types';\n\nconst sortMixins = {\n  methods: {\n    sortElems(a: number, b: number, direct: number): number {\n      if (isEven(direct)) {\n        if (a < b) {\n          return -1;\n        }\n        if (a > b) {\n          return 1;\n        }\n      } else {\n        if (a < b) {\n          return 1;\n        }\n        if (a > b) {\n          return -1;\n        }\n      }\n      return 0;\n    },\n\n    sortTypes(sorting: number, item: GameResult): number {\n      if (sorting < 2) {\n        return item.count;\n      }\n      if (sorting < 4) {\n        return item.time;\n      }\n      return item.createdAt;\n    },\n  },\n};\n\nexport default sortMixins;\n"
  },
  {
    "path": "tools/sloths/src/components/modal/AlertModal.vue",
    "content": "<template>\n  <div class=\"alert-modal\">\n    <modal-window @close=\"closeModal\">\n      <template v-slot:header> {{ $t(header) }} </template>\n\n      <template v-slot:body>\n        <p>\n          {{ message }}\n        </p>\n      </template>\n    </modal-window>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport ModalWindow from '@/components/modal/ModalWindow.vue';\n\nexport default defineComponent({\n  name: 'AlertModal',\n\n  components: {\n    ModalWindow,\n  },\n\n  props: {\n    header: {\n      type: String,\n      required: true,\n    },\n\n    message: {\n      type: String,\n      required: true,\n    },\n  },\n\n  methods: {\n    closeModal() {\n      this.$emit('closeAlertModal');\n    },\n  },\n});\n</script>\n\n.\n<style scoped>\n.alert-modal {\n  z-index: 100;\n  position: absolute;\n}\n\np {\n  white-space: pre-wrap;\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/modal/ModalWindow.vue",
    "content": "<template>\n  <transition name=\"fade\">\n    <div class=\"modal-background\">\n      <div class=\"modal\">\n        <div class=\"btn-close\" @click=\"close\">╳</div>\n        <h3 class=\"modal__header\">\n          <slot name=\"header\"></slot>\n        </h3>\n\n        <section class=\"modal__body\">\n          <slot name=\"body\"> </slot>\n        </section>\n\n        <footer class=\"modal__footer\">\n          <slot name=\"footer\"></slot>\n        </footer>\n        <custom-btn\n          :text=\"$t('btn.close')\"\n          className=\"btn btn-link\"\n          :onClick=\"close\"\n          v-shortkey=\"['esc']\"\n          @shortkey=\"close\"\n        ></custom-btn>\n      </div>\n    </div>\n  </transition>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport CustomBtn from '../buttons/CustomBtn.vue';\n\nexport default defineComponent({\n  name: 'ModalWindow',\n\n  components: {\n    CustomBtn,\n  },\n\n  methods: {\n    close() {\n      this.$emit('close');\n    },\n  },\n});\n</script>\n\n<style scoped>\n.modal-background {\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n\n  display: flex;\n  justify-content: center;\n  align-items: center;\n\n  background-color: var(--dark-opacity);\n\n  z-index: 100;\n}\n\n.modal {\n  padding: 1em 2.5em;\n\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n\n  overflow-x: auto;\n  color: var(--color-text);\n  background-color: var(--color-background-soft);\n  border-radius: 1em;\n  box-shadow: 0px 0px 5px;\n}\n\n.modal__header,\n.modal__footer {\n  position: relative;\n  padding: 0.5em;\n\n  display: flex;\n  justify-content: center;\n}\n.modal__body {\n  position: relative;\n  padding: 1em 0.5em;\n}\n.btn-close {\n  position: absolute;\n  top: 0.8em;\n  right: 0.5em;\n  width: 2em;\n  height: 2em;\n\n  border: none;\n  font-size: 1em;\n  font-weight: bold;\n  text-align: center;\n\n  cursor: pointer;\n\n  background: transparent;\n}\n/* Animation */\n.fade-enter-active {\n  animation: fade-out 0.2s;\n}\n.fade-leave-active {\n  animation: fade-in 0.2s;\n}\n@keyframes fade-in {\n  0% {\n    transform: scale(1);\n    opacity: 1;\n  }\n\n  100% {\n    transform: scale(2);\n    opacity: 0;\n  }\n}\n@keyframes fade-out {\n  0% {\n    transform: scale(2);\n    opacity: 0;\n  }\n\n  100% {\n    transform: scale(1);\n    opacity: 1;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/switchers/SoundSwitcher.vue",
    "content": "<template>\n  <div class=\"sound-switcher\">\n    <input\n      class=\"sound-switcher__input\"\n      type=\"radio\"\n      v-for=\"(sound, i) in sounds\"\n      :key=\"`sound-${i}`\"\n      :value=\"sound\"\n      v-model=\"currSound\"\n      :name=\"`sound-${sound}`\"\n      :id=\"`sound-${sound}`\"\n    />\n\n    <label\n      class=\"sound-switcher__label\"\n      :for=\"`sound-${soundStatus}`\"\n      :class=\"`sound-switcher__label_${currTheme}-${soundStatus}`\"\n      :title=\"$t('header.switchers.sound')\"\n      v-shortkey=\"['ctrl', '1']\"\n      @shortkey=\"setSoundValue(`${soundStatus}`)\"\n    ></label>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapWritableState } from 'pinia';\n\nimport useAudioOn from '../../stores/audio-on';\nimport useThemeProp from '../../stores/theme';\n\nexport default defineComponent({\n  name: 'ThemeSwitcher',\n\n  data(): { currSound: string; sounds: string[] } {\n    return {\n      currSound: '',\n      sounds: ['on', 'off'],\n    };\n  },\n\n  computed: {\n    ...mapWritableState(useAudioOn, ['isAudioOn']),\n    ...mapWritableState(useThemeProp, ['currTheme']),\n    soundStatus(): string {\n      return this.sounds.filter(sound => sound !== this.currSound)[0];\n    },\n  },\n\n  mounted() {\n    this.currSound = this.getSoundValue() || 'on';\n  },\n\n  watch: {\n    currSound(newValue) {\n      this.setSoundValue(newValue);\n    },\n  },\n\n  methods: {\n    getSoundValue(): string | null {\n      return localStorage.getItem('rs-sloths-sound');\n    },\n\n    setSoundValue(sound: string): void {\n      localStorage.setItem('rs-sloths-sound', sound);\n      this.currSound = sound;\n      this.isAudioOn = sound === 'on';\n    },\n  },\n});\n</script>\n\n<style scoped>\n.sound-switcher__input {\n  display: none;\n}\n\n.sound-switcher__label {\n  display: block;\n  width: 3rem;\n  height: 3rem;\n  transition: 0.5s ease;\n  cursor: pointer;\n}\n\n.sound-switcher__label_light-on {\n  background: center center / cover url('@/assets/icons/sounds/sound-light-on.svg') no-repeat;\n}\n\n.sound-switcher__label_light-off {\n  background: center center / cover url('@/assets/icons/sounds/sound-light-off.svg') no-repeat;\n}\n\n.sound-switcher__label_dark-on {\n  background: center center / cover url('@/assets/icons/sounds/sound-dark-on.svg') no-repeat;\n}\n\n.sound-switcher__label_dark-off {\n  background: center center / cover url('@/assets/icons/sounds/sound-dark-off.svg') no-repeat;\n}\n\n.sound-switcher__label:hover {\n  transform: scale(1.1) rotate(-5deg);\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/components/switchers/ThemeSwitcher.vue",
    "content": "<template>\n  <div class=\"theme-switcher\">\n    <input\n      class=\"theme-switcher__input\"\n      type=\"radio\"\n      v-for=\"(theme, i) in themes\"\n      :key=\"`theme-${i}`\"\n      :value=\"theme\"\n      v-model=\"currTheme\"\n      :name=\"theme\"\n      :id=\"`${theme}-theme`\"\n    />\n\n    <label\n      class=\"theme-switcher__label\"\n      :for=\"`${themeStatus}-theme`\"\n      :class=\"`theme-switcher__label_${themeStatus}`\"\n      :title=\"$t('header.switchers.theme')\"\n      v-shortkey=\"['ctrl', '2']\"\n      @shortkey=\"setTheme(`${themeStatus}`)\"\n    ></label>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapWritableState } from 'pinia';\n\nimport useThemeProp from '../../stores/theme';\nimport getUserTheme from '../../utils/userTheme';\n\nexport default defineComponent({\n  name: 'ThemeSwitcher',\n\n  data(): { themes: string[] } {\n    return {\n      themes: ['light', 'dark'],\n    };\n  },\n\n  computed: {\n    ...mapWritableState(useThemeProp, ['currTheme']),\n    themeStatus(): string {\n      return this.themes.filter(theme => theme !== this.currTheme)[0];\n    },\n  },\n\n  mounted() {\n    this.currTheme = this.getLastTheme() || getUserTheme();\n  },\n\n  watch: {\n    currTheme(newTheme) {\n      this.setTheme(newTheme);\n    },\n  },\n\n  methods: {\n    getLastTheme(): string | null {\n      return localStorage.getItem('rs-sloths-theme');\n    },\n\n    setTheme(theme: string): void {\n      localStorage.setItem('rs-sloths-theme', theme);\n      this.currTheme = theme;\n      document.documentElement.className = theme;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.theme-switcher__input {\n  display: none;\n}\n\n.theme-switcher__label {\n  display: block;\n  width: 2rem;\n  height: 3rem;\n  transition: 0.5s ease;\n  cursor: pointer;\n}\n\n.theme-switcher__label_light {\n  background: center center / cover url('@/assets/icons/themes/light.svg') no-repeat;\n}\n\n.theme-switcher__label_dark {\n  background: center center / cover url('@/assets/icons/themes/dark.svg') no-repeat;\n}\n\n.theme-switcher__label:hover {\n  transform: scale(1.1) rotate(-5deg);\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/i18n.ts",
    "content": "import { createI18n } from 'vue-i18n';\nimport en from './assets/locales/en.json';\n\nconst i18n = createI18n({\n  legacy: false,\n  locale: 'en',\n  fallbackLocale: 'en',\n  messages: {\n    en,\n  },\n  globalInjection: true,\n});\n\nexport default i18n;\n"
  },
  {
    "path": "tools/sloths/src/main.ts",
    "content": "import { createApp } from 'vue';\nimport { createPinia } from 'pinia';\n\nimport Shortkey from 'vue-three-shortkey';\nimport i18n from './i18n';\nimport App from './App.vue';\nimport router from './router';\n\nimport 'normalize.css/normalize.css';\nimport './assets/styles/main.css';\n\nconst app = createApp(App);\n\napp.use(createPinia());\napp.use(router);\napp.use(i18n);\napp.use(Shortkey, { prevent: ['input', 'textarea'] });\n\napp.mount('#app');\n"
  },
  {
    "path": "tools/sloths/src/router/index.ts",
    "content": "import { createRouter, createWebHistory } from 'vue-router';\nimport Home from '../views/Home.vue';\n\nconst router = createRouter({\n  history: createWebHistory(import.meta.env.BASE_URL),\n  routes: [\n    {\n      path: '/',\n      name: 'home',\n      component: Home,\n    },\n    {\n      path: '/about',\n      name: 'about',\n      // route level code-splitting\n      // this generates a separate chunk (About.[hash].js) for this route\n      // which is lazy-loaded when the route is visited.\n      component: () => import('../views/About.vue'),\n    },\n    {\n      path: '/catalog',\n      name: 'catalog',\n      component: () => import('../views/Catalog.vue'),\n    },\n    {\n      path: '/create',\n      name: 'create',\n      component: () => import('../views/Create.vue'),\n    },\n    {\n      path: '/merch',\n      name: 'merch',\n      component: () => import('../views/Merch.vue'),\n    },\n    {\n      path: '/guess',\n      name: 'guess',\n      component: () => import('../views/Guess.vue'),\n    },\n    {\n      path: '/memory',\n      name: 'memory',\n      component: () => import('../views/Memory.vue'),\n    },\n    // 404 always last item\n    {\n      path: '/404',\n      name: '404',\n      component: () => import('../views/404.vue'),\n    },\n    {\n      path: '/:pathMatch(.*)*',\n      redirect: '/404',\n    },\n  ],\n});\n\nexport default router;\n"
  },
  {
    "path": "tools/sloths/src/services/error-handler.ts",
    "content": "import useAlertModal from '@/stores/alert-modal';\n\nconst { showAlertModal } = useAlertModal();\n\nexport const errorHandler = (error: unknown) => {\n  showAlertModal('modal.header.error', `${error}`);\n};\n\nexport default errorHandler;\n"
  },
  {
    "path": "tools/sloths/src/services/sloths-service.ts",
    "content": "import { PAGINATION_OPTIONS, SLOTH_SORTING } from '@/common/const';\nimport type { QueryStringOptions, Sloth } from '@/common/types';\n\nexport class SlothsService {\n  constructor(private data: Sloth[]) {}\n\n  public async getAll({\n    page = 1,\n    limit = PAGINATION_OPTIONS[0],\n    order = SLOTH_SORTING[0].value,\n    searchText,\n    filter,\n  }: QueryStringOptions) {\n    let items = this.data;\n    const [orderField, orderDirection] = order.split('-', 2);\n    const orderMultiplier = orderDirection === 'asc' ? 1 : -1;\n\n    items.sort((a: Sloth, b: Sloth) => {\n      if (Number.isNaN(+a[orderField])) {\n        if (a[orderField] === b[orderField]) return 0;\n        return a[orderField] < b[orderField] ? -1 * orderMultiplier : 1 * orderMultiplier;\n      }\n      return (+a[orderField] - +b[orderField]) * orderMultiplier;\n    });\n    if (filter) {\n      const filterTags = filter.split(',');\n      items = items.filter(sloth => {\n        return sloth.tags.some(tag => filterTags.includes(tag));\n      });\n    }\n    if (searchText) {\n      items = items.filter(\n        sloth =>\n          sloth.name.toLowerCase().includes(searchText.toLowerCase()) ||\n          sloth.description.toLowerCase().includes(searchText.toLowerCase()),\n      );\n    }\n\n    const count = items.length;\n\n    const start = (page - 1) * limit;\n    const end = start + limit;\n    items = items.slice(start, end);\n\n    return { data: { items, count }, status: 200 };\n  }\n\n  public getById(id: string) {\n    return this.data.find(sloth => id === sloth.id);\n  }\n\n  public getTags() {\n    const allTags = this.data.flatMap(sloth => sloth.tags);\n    const uniqueTags = [...new Set(allTags)];\n    return uniqueTags.sort();\n  }\n}\n\nexport default SlothsService;\n"
  },
  {
    "path": "tools/sloths/src/shims-vue.d.ts",
    "content": "declare module '*.vue';\ndeclare module 'vue-three-shortkey';\n"
  },
  {
    "path": "tools/sloths/src/stores/alert-modal.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useAlertModal = defineStore({\n  id: 'alertModal',\n\n  state: () => ({\n    isAlert: false,\n    header: '',\n    message: '',\n  }),\n\n  actions: {\n    showAlertModal(header: string, message: string) {\n      this.isAlert = true;\n      this.header = header;\n      this.message = message;\n    },\n  },\n});\n\nexport default useAlertModal;\n"
  },
  {
    "path": "tools/sloths/src/stores/audio-on.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useAudioOn = defineStore({\n  id: 'audioOn',\n\n  state: () => ({\n    isAudioOn: false,\n  }),\n});\n\nexport default useAudioOn;\n"
  },
  {
    "path": "tools/sloths/src/stores/cleaned.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useCleanedStore = defineStore({\n  id: 'cleaned',\n  state: () => ({\n    cleanedFilelist: [] as string[],\n    originalFilelist: [] as string[],\n  }),\n});\n\nexport default useCleanedStore;\n"
  },
  {
    "path": "tools/sloths/src/stores/counter.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useCounterStore = defineStore({\n  id: 'counter',\n  state: () => ({\n    counter: 0,\n  }),\n  getters: {\n    doubleCount: state => state.counter * 2,\n  },\n  actions: {\n    increment() {\n      this.counter += 1;\n    },\n  },\n});\n\nexport default useCounterStore;\n"
  },
  {
    "path": "tools/sloths/src/stores/loader.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useLoader = defineStore({\n  id: 'loader',\n  state: () => ({\n    isLoad: false,\n  }),\n});\n\nexport default useLoader;\n"
  },
  {
    "path": "tools/sloths/src/stores/pages-store.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst usePagesStore = defineStore({\n  id: 'pagesStore',\n  state: () => ({\n    pageCatalog: '',\n    pageCreate: '',\n    pageMerch: '',\n    pageMemory: '',\n  }),\n\n  actions: {\n    getPageCatalogState(): string {\n      return this.pageCatalog;\n    },\n\n    setPageCatalogState(newState: string) {\n      this.pageCatalog = newState;\n    },\n\n    getPageCreateState(): string {\n      return this.pageCreate;\n    },\n\n    setPageCreateState(newState: string) {\n      this.pageCreate = newState;\n    },\n\n    getPageMerchState(): string {\n      return this.pageMerch;\n    },\n\n    setPageMerchState(newState: string) {\n      this.pageMerch = newState;\n    },\n\n    getPageMemoryState(): string {\n      return this.pageMemory;\n    },\n\n    setPageMemoryState(newState: string) {\n      this.pageMemory = newState;\n    },\n  },\n});\n\nexport default usePagesStore;\n"
  },
  {
    "path": "tools/sloths/src/stores/pagination.ts",
    "content": "import { PAGINATION_OPTIONS } from '@/common/const';\nimport { defineStore } from 'pinia';\n\nconst usePagination = defineStore({\n  id: 'pagination',\n\n  state: () => ({\n    perPage: PAGINATION_OPTIONS[0],\n    currPage: 1,\n  }),\n\n  actions: {\n    getPerPage(): number {\n      return this.perPage;\n    },\n\n    setPerPage(n: number) {\n      this.perPage = n;\n    },\n\n    getCurrPage(): number {\n      return this.currPage;\n    },\n\n    setCurrPage(n: number) {\n      this.currPage = n;\n    },\n  },\n});\n\nexport default usePagination;\n"
  },
  {
    "path": "tools/sloths/src/stores/search-text.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useSearchText = defineStore({\n  id: 'searchText',\n\n  state: () => ({\n    searchText: '',\n  }),\n\n  actions: {\n    getSearchText(): string {\n      return this.searchText;\n    },\n\n    setSearchText(s: string) {\n      this.searchText = s;\n    },\n  },\n});\n\nexport default useSearchText;\n"
  },
  {
    "path": "tools/sloths/src/stores/sloth-info.ts",
    "content": "import { defineStore } from 'pinia';\nimport type { Sloth } from '@/common/types';\n\nconst useSlothInfo = defineStore({\n  id: 'slothInfo',\n\n  state: () => ({\n    slothInfo: {} as Sloth,\n    tagsStr: '',\n  }),\n\n  actions: {\n    setEmptySlothInfo() {\n      this.slothInfo = {} as Sloth;\n      this.tagsStr = '';\n    },\n\n    setSlothInfo(newSlothInfo: Sloth) {\n      this.slothInfo = { ...newSlothInfo };\n      this.tagsStr = newSlothInfo.tags.join(' ');\n    },\n  },\n});\n\nexport default useSlothInfo;\n"
  },
  {
    "path": "tools/sloths/src/stores/sloths.ts",
    "content": "import { defineStore } from 'pinia';\nimport type { Sloth } from '../common/types';\n\nconst useSlothsStore = defineStore({\n  id: 'sloths',\n  state: () => ({\n    sloths: [] as Sloth[],\n  }),\n});\n\nexport default useSlothsStore;\n"
  },
  {
    "path": "tools/sloths/src/stores/sorting-list.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useSortingList = defineStore({\n  id: 'sortingList',\n\n  state: () => ({\n    sortingList: '',\n  }),\n\n  actions: {\n    getSortingList(): string {\n      return this.sortingList;\n    },\n\n    setSortingList(s: string) {\n      this.sortingList = s;\n    },\n  },\n});\n\nexport default useSortingList;\n"
  },
  {
    "path": "tools/sloths/src/stores/tag-cloud.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useSelectedTags = defineStore({\n  id: 'tagCloud',\n\n  state: () => ({\n    selected: [] as string[],\n  }),\n\n  actions: {\n    getSelected(): string[] {\n      return [...this.selected];\n    },\n\n    setSelected(s: string[]) {\n      this.selected = [...s];\n    },\n  },\n});\n\nexport default useSelectedTags;\n"
  },
  {
    "path": "tools/sloths/src/stores/theme.ts",
    "content": "import { defineStore } from 'pinia';\n\nconst useThemeProp = defineStore({\n  id: 'theme',\n  state: () => ({\n    currTheme: '',\n  }),\n});\n\nexport default useThemeProp;\n"
  },
  {
    "path": "tools/sloths/src/utils/audio.ts",
    "content": "import fileFlip from '@/assets/sounds/flip.mp3';\nimport fileFail from '@/assets/sounds/fail.mp3';\nimport fileSadTrombone from '@/assets/sounds/sad-trombone.mp3';\nimport fileSuccess from '@/assets/sounds/success.mp3';\nimport fileWin from '@/assets/sounds/ta-da.mp3';\nimport fileSlide from '@/assets/sounds/slide.mp3';\nimport fileOvation from '@/assets/sounds/ovation.mp3';\n\nimport { storeToRefs } from 'pinia';\nimport useAudioOn from '@/stores/audio-on';\n\nconst { isAudioOn } = storeToRefs(useAudioOn());\n\nexport const audioFlip = new Audio(fileFlip);\nexport const audioFail = new Audio(fileFail);\nexport const audioSadTrombone = new Audio(fileSadTrombone);\nexport const audioSuccess = new Audio(fileSuccess);\nexport const audioWin = new Audio(fileWin);\nexport const audioSlide = new Audio(fileSlide);\nexport const audioOvation = new Audio(fileOvation);\n\naudioFlip.volume = 0.2;\naudioFail.volume = 0.1;\naudioSadTrombone.volume = 0.2;\naudioSuccess.volume = 0.3;\naudioWin.volume = 0.1;\naudioSlide.volume = 0.2;\naudioOvation.volume = 0.2;\n\nexport const playAudio = (a: HTMLAudioElement) => {\n  if (isAudioOn.value) {\n    const audio = a;\n    audio.currentTime = 0;\n    const playPromise = audio.play();\n\n    if (playPromise !== undefined) {\n      playPromise.catch(() => {});\n    }\n  }\n};\n"
  },
  {
    "path": "tools/sloths/src/utils/canvas-utils.ts",
    "content": "import type { CanvasElement, CanvasPos, CanvasProperties, CanvasRectXY } from '@/common/types';\n\nexport const canvasSize = 500;\nexport const textMargin = 10;\n\nconst scaleStepDeltaCanvas = 0.02;\nconst scaleStepDeltaElement = 0.05;\n\nconst hexR = 0.299;\nconst hexG = 0.587;\nconst hexB = 0.114;\nconst contrastLimit = 186;\n\nexport const initProperties = (scaleMin: number, scaleTrue: number, scaleMax: number): CanvasProperties => {\n  return {\n    scaleSteps: 1,\n    scaleMin,\n    scaleTrue,\n    scaleMax,\n    backgroundTransparent: false,\n    backgroundColor: '#777777',\n    itemColor: '#222222',\n    textColor: '#ffffff',\n    strokeColor: '#000000',\n  } as CanvasProperties;\n};\n\nexport const initElement = (\n  top: number,\n  bottom: number,\n  scaleSteps: number,\n  scaleMin: number,\n  scaleTrue: number,\n  scaleMax: number,\n  isResizable = true,\n): CanvasElement => {\n  return {\n    isResizable,\n    text: '',\n    left: 0,\n    top,\n    bottom,\n    scaledLeft: 0,\n    scaledTop: top,\n    width: canvasSize * scaleSteps,\n    height: 0,\n    scaledWidth: canvasSize * scaleSteps,\n    scaledHeight: 0,\n    scaleSteps,\n    scaleMin,\n    scaleTrue,\n    scaleMax,\n    isHovered: false,\n    isSelected: false,\n    isBorderHovered: false,\n    isLeftBorderSelected: false,\n    isRightBorderSelected: false,\n    selectedPos: {} as CanvasPos,\n  } as CanvasElement;\n};\n\n// draw\nexport const calcCanvasSizes = (cnv: HTMLCanvasElement, canvasScaleSteps: number) => {\n  const canvas = cnv;\n\n  canvas.width = canvasSize * canvasScaleSteps;\n  canvas.height = canvasSize * canvasScaleSteps;\n};\n\nexport const drawBackground = (\n  canvas: HTMLCanvasElement,\n  ctx: CanvasRenderingContext2D,\n  canvasProps: CanvasProperties,\n) => {\n  if (!canvasProps.backgroundTransparent) {\n    ctx.fillStyle = canvasProps.backgroundColor;\n    ctx.fillRect(0, 0, canvas.width, canvas.height);\n  }\n};\n\nexport const drawMerchImage = (\n  canvas: HTMLCanvasElement,\n  ctx: CanvasRenderingContext2D,\n  img: HTMLImageElement,\n  color: string,\n) => {\n  const tempCanvas = document.createElement('canvas');\n  const tempctx = tempCanvas.getContext('2d');\n  if (!tempctx) return;\n\n  tempCanvas.width = canvas.width;\n  tempCanvas.height = canvas.height;\n  tempctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);\n\n  tempctx.globalCompositeOperation = 'source-atop';\n  tempctx.fillStyle = color;\n  tempctx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);\n\n  ctx.drawImage(tempCanvas, 0, 0);\n  ctx.globalCompositeOperation = 'overlay';\n  ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);\n\n  // always clean up: reset compositing to its default\n  ctx.globalCompositeOperation = 'source-over';\n};\n\nexport const calcElementsSizes = (\n  img: HTMLImageElement,\n  imgCanvasElement: CanvasElement,\n  topCanvasElement: CanvasElement,\n  bottomCanvasElement: CanvasElement,\n  canvasScaleSteps: number,\n) => {\n  let canvasElement = imgCanvasElement;\n  if (img) {\n    canvasElement.width = img.naturalWidth;\n    canvasElement.height = img.naturalHeight;\n\n    canvasElement.scaledWidth = canvasElement.width * canvasElement.scaleSteps * canvasScaleSteps;\n    canvasElement.scaledHeight = canvasElement.scaledWidth * (canvasElement.height / canvasElement.width);\n  } else {\n    canvasElement.width = 0;\n    canvasElement.height = 0;\n    canvasElement.scaledWidth = 0;\n    canvasElement.scaledHeight = 0;\n  }\n\n  canvasElement = topCanvasElement;\n  canvasElement.scaledWidth = canvasElement.width * canvasScaleSteps;\n\n  canvasElement = bottomCanvasElement;\n  canvasElement.scaledWidth = canvasElement.width * canvasScaleSteps;\n};\n\nexport const calcElementsPosition = (layers: CanvasElement[], canvasScaleSteps: number) => {\n  layers.forEach(el => {\n    const canvasElement = el;\n\n    canvasElement.scaledLeft = canvasElement.left * canvasScaleSteps;\n    canvasElement.scaledTop = canvasElement.top * canvasScaleSteps;\n    canvasElement.scaledBottom = canvasElement.bottom * canvasScaleSteps;\n  });\n};\n\nconst getElementRectXY = (el: CanvasElement): CanvasRectXY => {\n  return {\n    x1: el.scaledLeft,\n    x2: el.scaledLeft + el.scaledWidth,\n    y1: el.scaledTop,\n    y2: el.scaledTop + el.scaledHeight,\n  } as CanvasRectXY;\n};\n\nexport const invertHex = (color: string, backgroundTransparent = false): string => {\n  if (backgroundTransparent) return 'gray';\n\n  let hex = color;\n  if (hex.indexOf('#') === 0) {\n    hex = hex.slice(1);\n  }\n  // convert 3-digit hex to 6-digits.\n  if (hex.length === 3) {\n    hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];\n  }\n\n  const r = parseInt(hex.slice(0, 2), 16);\n  const g = parseInt(hex.slice(2, 4), 16);\n  const b = parseInt(hex.slice(4, 6), 16);\n  return r * hexR + g * hexG + b * hexB > contrastLimit ? 'black' : 'white';\n};\n\nexport const drawBorder = (el: CanvasElement, ctx: CanvasRenderingContext2D, color: string) => {\n  if (el.isHovered || el.isSelected) {\n    const elXY = getElementRectXY(el);\n\n    ctx.lineWidth = 2;\n    ctx.strokeStyle = color;\n    ctx.beginPath();\n    ctx.moveTo(elXY.x1, elXY.y1);\n    ctx.lineTo(elXY.x2, elXY.y1);\n    ctx.lineTo(elXY.x2, elXY.y2);\n    ctx.lineTo(elXY.x1, elXY.y2);\n    ctx.closePath();\n    ctx.stroke();\n  }\n};\n\n// mouse handlers\nconst getMousePos = (evt: MouseEvent, canvas: HTMLCanvasElement): CanvasPos => {\n  const rect = canvas.getBoundingClientRect(); // abs. size of element\n  const scaleX = canvas.width / rect.width; // relationship bitmap vs. element for X\n  const scaleY = canvas.height / rect.height; // relationship bitmap vs. element for Y\n\n  return {\n    x: (evt.clientX - rect.left) * scaleX, // scale mouse coordinates after they have\n    y: (evt.clientY - rect.top) * scaleY, // been adjusted to be relative to element\n  } as CanvasPos;\n};\n\nconst moveElement = (mousePos: CanvasPos, el: CanvasElement, canvasScaleSteps: number) => {\n  const canvasElement = el;\n  const dx = mousePos.x - canvasElement.selectedPos.x;\n  const dy = mousePos.y - canvasElement.selectedPos.y;\n  const elXY = getElementRectXY(el);\n\n  let isSelected = false;\n  if (canvasElement.isLeftBorderSelected) {\n    if (mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2) {\n      canvasElement.left += dx / canvasScaleSteps;\n      canvasElement.width -= dx / canvasScaleSteps;\n      isSelected = true;\n    } else {\n      canvasElement.isLeftBorderSelected = false;\n    }\n  } else if (canvasElement.isRightBorderSelected) {\n    if (mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2) {\n      canvasElement.width += dx / canvasScaleSteps;\n      isSelected = true;\n    } else {\n      canvasElement.isRightBorderSelected = false;\n    }\n  } else if (canvasElement.isSelected) {\n    canvasElement.left += dx / canvasScaleSteps;\n    canvasElement.top += dy / canvasScaleSteps;\n    canvasElement.bottom += dy / canvasScaleSteps;\n    isSelected = true;\n  } else {\n    canvasElement.isBorderHovered =\n      (mousePos.x >= elXY.x1 && mousePos.x <= elXY.x1 + 2 && mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2) ||\n      (mousePos.x >= elXY.x2 - 2 && mousePos.x <= elXY.x2 && mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2);\n    canvasElement.isHovered =\n      mousePos.x >= elXY.x1 && mousePos.x <= elXY.x2 && mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2;\n  }\n\n  if (isSelected) {\n    canvasElement.selectedPos.x = mousePos.x;\n    canvasElement.selectedPos.y = mousePos.y;\n  }\n};\n\nexport const moveElements = (\n  e: MouseEvent,\n  canvas: HTMLCanvasElement,\n  layers: CanvasElement[],\n  canvasScaleSteps: number,\n) => {\n  const mousePos = getMousePos(e, canvas);\n\n  const selected = layers.filter(el => el.isSelected);\n\n  if (selected.length) {\n    // move selected only\n    moveElement(mousePos, selected[0], canvasScaleSteps);\n  } else {\n    // move top of layers\n    let isHovered = false;\n\n    for (let i = layers.length - 1; i >= 0; i -= 1) {\n      const el = layers[i];\n\n      if (isHovered) {\n        el.isHovered = false;\n      } else {\n        moveElement(mousePos, el, canvasScaleSteps);\n        isHovered = el.isHovered;\n      }\n    }\n  }\n};\n\nconst selectElement = (mousePos: CanvasPos, el: CanvasElement) => {\n  const canvasElement = el;\n\n  const elXY = getElementRectXY(el);\n\n  const isLeftBorderSelected =\n    mousePos.x >= elXY.x1 && mousePos.x <= elXY.x1 + 2 && mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2;\n\n  const isRightBorderSelected =\n    mousePos.x >= elXY.x2 - 2 && mousePos.x <= elXY.x2 && mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2;\n\n  const isSelected = mousePos.x >= elXY.x1 && mousePos.x <= elXY.x2 && mousePos.y >= elXY.y1 && mousePos.y <= elXY.y2;\n\n  canvasElement.isLeftBorderSelected = isLeftBorderSelected;\n  canvasElement.isRightBorderSelected = isRightBorderSelected && !isLeftBorderSelected;\n  canvasElement.isSelected = isSelected && !isLeftBorderSelected && !isRightBorderSelected;\n\n  if (isSelected || isLeftBorderSelected || isRightBorderSelected) {\n    canvasElement.selectedPos = { ...mousePos };\n  }\n};\n\nexport const selectElements = (e: MouseEvent, canvas: HTMLCanvasElement, layers: CanvasElement[]) => {\n  const mousePos = getMousePos(e, canvas);\n\n  let isSelected = false;\n\n  for (let i = layers.length - 1; i >= 0; i -= 1) {\n    const el = layers[i];\n\n    if (isSelected) {\n      el.isLeftBorderSelected = false;\n      el.isRightBorderSelected = false;\n      el.isSelected = false;\n    } else {\n      selectElement(mousePos, el);\n      isSelected = el.isSelected || el.isLeftBorderSelected || el.isRightBorderSelected;\n    }\n  }\n};\n\nexport const deselectElements = (layers: CanvasElement[]) => {\n  layers.forEach(el => {\n    const canvasElement = el;\n\n    canvasElement.isSelected = false;\n    canvasElement.isLeftBorderSelected = false;\n    canvasElement.isRightBorderSelected = false;\n    canvasElement.selectedPos = {} as CanvasPos;\n  });\n};\n\nexport const unhoverElements = (layers: CanvasElement[]) => {\n  layers.forEach(el => {\n    const canvasElement = el;\n\n    canvasElement.isHovered = false;\n    canvasElement.isBorderHovered = false;\n  });\n};\n\n// scaling\nexport const scaleUpCanvas = (props: CanvasProperties) => {\n  const canvasProps = props;\n  canvasProps.scaleSteps = Math.min(canvasProps.scaleMax, canvasProps.scaleSteps + scaleStepDeltaCanvas);\n};\n\nexport const scaleTrueCanvas = (props: CanvasProperties, layers: CanvasElement[]) => {\n  const canvasProps = props;\n  canvasProps.scaleSteps = 1;\n\n  layers.forEach(el => {\n    const canvasElement = el;\n    canvasElement.scaleSteps = canvasElement.scaleTrue;\n  });\n};\n\nexport const scaleDownCanvas = (props: CanvasProperties) => {\n  const canvasProps = props;\n  canvasProps.scaleSteps = Math.max(canvasProps.scaleMin, canvasProps.scaleSteps - scaleStepDeltaCanvas);\n};\n\nexport const scalingElements = (e: WheelEvent, layers: CanvasElement[], props: CanvasProperties) => {\n  if (e.deltaY === 0) return;\n\n  const dStep = e.deltaY > 0 ? scaleStepDeltaElement : -scaleStepDeltaElement;\n\n  let isHovered = false;\n\n  layers.forEach(el => {\n    if (el.isHovered) {\n      isHovered = true;\n      const canvasElement = el;\n      canvasElement.scaleSteps = Math.max(canvasElement.scaleMin, canvasElement.scaleSteps + dStep);\n    }\n  });\n\n  if (!isHovered) {\n    if (dStep > 0) {\n      scaleUpCanvas(props);\n    } else if (dStep < 0) scaleDownCanvas(props);\n  }\n};\n\n// text\nconst drawTextLine = (ctx: CanvasRenderingContext2D, line: string, x: number, y: number, isStroked: boolean) => {\n  if (isStroked) ctx.strokeText(line, x, y);\n  ctx.fillText(line, x, y);\n};\n\nconst drawTextMultiLineDown = (\n  ctx: CanvasRenderingContext2D,\n  el: CanvasElement,\n  canvasScaleSteps: number,\n  fontSize: number,\n  isStroked: boolean,\n) => {\n  const { scaledLeft, scaledTop } = el;\n  const width = el.scaledWidth;\n  const x = width / 2 + scaledLeft;\n\n  const words = el.text.split(' ');\n  let line = '';\n  let y = scaledTop + fontSize * 0.1;\n  let textHeight = fontSize;\n\n  words.forEach((word, index) => {\n    const testLine = `${line + word} `;\n    const metrics = ctx.measureText(testLine);\n    const testWidth = metrics.width;\n    if (testWidth > width && index > 0) {\n      drawTextLine(ctx, line.trim(), x, y, isStroked);\n      line = `${word} `;\n      y += fontSize;\n      textHeight += fontSize;\n    } else {\n      line = testLine;\n    }\n  });\n  drawTextLine(ctx, line.trim(), x, y, isStroked);\n\n  const canvasElement = el;\n\n  canvasElement.width = width / canvasScaleSteps;\n  canvasElement.height = textHeight;\n  canvasElement.scaledWidth = width;\n  canvasElement.scaledHeight = textHeight;\n};\n\nexport const drawTextDown = (\n  height: number,\n  ctx: CanvasRenderingContext2D,\n  canvasProps: CanvasProperties,\n  el: CanvasElement,\n  canvasScaleSteps: number,\n  isStroked = false,\n) => {\n  ctx.fillStyle = canvasProps.textColor;\n  if (isStroked) ctx.strokeStyle = canvasProps.strokeColor;\n  ctx.textAlign = 'center';\n  ctx.lineJoin = 'round';\n\n  const fontSize = Math.floor((height * el.scaleSteps) / 10);\n  ctx.lineWidth = Math.floor(fontSize / 5);\n  ctx.font = `${fontSize}px sans-serif`;\n  ctx.textBaseline = 'top';\n  drawTextMultiLineDown(ctx, el, canvasScaleSteps, fontSize, isStroked);\n};\n\nconst drawTextMultiLineUp = (\n  ctx: CanvasRenderingContext2D,\n  el: CanvasElement,\n  canvasScaleSteps: number,\n  fontSize: number,\n  isStroked: boolean,\n) => {\n  const { scaledLeft, scaledBottom } = el;\n  const width = el.scaledWidth;\n  const x = width / 2 + scaledLeft;\n\n  const words = el.text.split(' ').reverse();\n  let line = '';\n  let y = scaledBottom + fontSize * 0.1;\n  let textHeight = fontSize;\n\n  words.forEach((word, index) => {\n    const testLine = ` ${word + line}`;\n    const metrics = ctx.measureText(testLine);\n    const testWidth = metrics.width;\n    if (testWidth > width && index > 0) {\n      drawTextLine(ctx, line.trim(), x, y, isStroked);\n      line = ` ${word}`;\n      y -= fontSize;\n      textHeight += fontSize;\n    } else {\n      line = testLine;\n    }\n  });\n  drawTextLine(ctx, line.trim(), x, y, isStroked);\n\n  const canvasElement = el;\n  canvasElement.top = (scaledBottom - textHeight) / canvasScaleSteps;\n  canvasElement.scaledTop = canvasElement.top * canvasScaleSteps;\n  canvasElement.width = width / canvasScaleSteps;\n  canvasElement.height = textHeight;\n  canvasElement.scaledWidth = width;\n  canvasElement.scaledHeight = textHeight;\n};\n\nexport const drawTextUp = (\n  height: number,\n  ctx: CanvasRenderingContext2D,\n  canvasProps: CanvasProperties,\n  el: CanvasElement,\n  canvasScaleSteps: number,\n  isStroked = false,\n) => {\n  ctx.fillStyle = canvasProps.textColor;\n  if (isStroked) ctx.strokeStyle = canvasProps.strokeColor;\n  ctx.textAlign = 'center';\n  ctx.lineJoin = 'round';\n\n  const fontSize = Math.floor((height * el.scaleSteps) / 10);\n  ctx.lineWidth = Math.floor(fontSize / 5);\n  ctx.font = `${fontSize}px sans-serif`;\n  ctx.textBaseline = 'bottom';\n  drawTextMultiLineUp(ctx, el, canvasScaleSteps, fontSize, isStroked);\n};\n\n// others\nexport const centeringElements = (\n  imgCanvasElement: CanvasElement,\n  topCanvasElement: CanvasElement,\n  bottomCanvasElement: CanvasElement,\n  scaleSteps: number,\n  toImg = false,\n) => {\n  let canvasElement = imgCanvasElement;\n  canvasElement.left = (canvasSize - canvasElement.scaledWidth / scaleSteps) / 2;\n  canvasElement.top = (canvasSize - canvasElement.scaledHeight / scaleSteps) / 2;\n\n  canvasElement = topCanvasElement;\n  canvasElement.left = (canvasSize - canvasElement.scaledWidth / scaleSteps) / 2;\n  if (toImg) {\n    canvasElement.bottom = imgCanvasElement.top;\n  } else {\n    canvasElement.top = textMargin;\n  }\n\n  canvasElement = bottomCanvasElement;\n  canvasElement.left = (canvasSize - canvasElement.scaledWidth / scaleSteps) / 2;\n  if (toImg) {\n    canvasElement.top = imgCanvasElement.top + imgCanvasElement.scaledHeight / scaleSteps;\n  } else {\n    canvasElement.top = canvasSize - textMargin - canvasElement.scaledHeight / scaleSteps;\n    canvasElement.bottom = canvasSize - textMargin;\n  }\n};\n\nexport const loadImage = (url: string): Promise<HTMLImageElement> => {\n  return new Promise(resolve => {\n    const image = new Image();\n    image.setAttribute('crossorigin', 'anonymous');\n    image.addEventListener('load', () => {\n      resolve(image);\n    });\n    image.src = url;\n  });\n};\n\nexport const getCursor = (layers: CanvasElement[]): string => {\n  const borderHovered = layers.filter(el => el.isBorderHovered);\n  if (borderHovered.length > 0 && borderHovered[0].isResizable) return 'w-resize';\n\n  const isHovered = layers.filter(el => el.isHovered).length > 0;\n  if (isHovered) return 'move';\n\n  return 'auto';\n};\n"
  },
  {
    "path": "tools/sloths/src/utils/game-utils.ts",
    "content": "export default function isEven(value: number) {\n  return !(value % 2);\n}\n"
  },
  {
    "path": "tools/sloths/src/utils/userTheme.ts",
    "content": "export default function getUserTheme(): string {\n  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {\n    return 'dark';\n  }\n  return 'light';\n}\n"
  },
  {
    "path": "tools/sloths/src/views/404.vue",
    "content": "<template>\n  <div class=\"page-404\">\n    <div class=\"page-404__image\"></div>\n    <div class=\"page-404__wrap\">\n      <h2 class=\"page-404__title\">{{ $t('404.title') }}</h2>\n      <p class=\"page-404__descr\">{{ $t('404.text') }}</p>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: '404View',\n});\n</script>\n\n<style scoped>\n.page-404 {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 3rem;\n}\n\n.page-404__image {\n  height: 50rem;\n  width: 50rem;\n  background: no-repeat center center / contain url('../assets/icons/404/404.svg');\n}\n\n.page-404__wrap {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 5rem;\n  width: 50rem;\n}\n\n.page-404__title {\n  color: var(--color-heading);\n  font-weight: bold;\n  font-size: 3.6rem;\n}\n\n.page-404__descr {\n  font-size: 2.4rem;\n  line-height: 3.6rem;\n  text-align: center;\n  color: var(--color-text);\n}\n\n@media (max-width: 1200px) {\n  .page-404__image {\n    height: 20rem;\n    width: 20rem;\n  }\n}\n\n@media (max-width: 1200px) {\n  .page-404 {\n    flex-direction: column;\n    gap: 3rem;\n  }\n\n  .page-404__wrap {\n    width: 90%;\n    gap: 3rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/views/About.vue",
    "content": "<template>\n  <div class=\"about\">\n    <img class=\"about__logo\" src=\"/img/logo.svg\" alt=\"logo\" />\n    <about-section\n      v-for=\"(section, i) in sections\"\n      :key=\"`${section}`\"\n      :section=\"section\"\n      :softClass=\"i % 2 ? 'left' : 'right'\"\n      :index=\"i\"\n    ></about-section>\n\n    <section class=\"about__teammates\">\n      <img class=\"about__wwt\" :src=\"`/img/team/wwt.svg`\" alt=\"wwt\" />\n      <div class=\"about__team\">{{ $t(`about.teammates.main`) }}</div>\n      <div class=\"about__teammates__wrap\">\n        <about-teammate v-for=\"teammate in teammates\" :key=\"`${teammate}`\" :teammate=\"teammate\"></about-teammate>\n      </div>\n    </section>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport AboutSection from '@/components/about/AboutSection.vue';\nimport AboutTeammate from '@/components/about/AboutTeammate.vue';\n\nexport default {\n  name: 'AboutView',\n\n  data() {\n    return {\n      sections: ['sloths', 'interactives'],\n      teammates: ['wiijoy', 'ogimly', 'vanet'],\n    };\n  },\n\n  components: {\n    AboutSection,\n    AboutTeammate,\n  },\n};\n</script>\n\n<style scoped>\n.about {\n  padding: 3rem;\n  display: flex;\n  flex-direction: column;\n  gap: 4rem;\n}\n\n.about__logo,\n.about__wwt {\n  display: block;\n  margin: 0 auto;\n}\n\n.about__log {\n  height: 50rem;\n}\n\n.about__wwt {\n  width: 40rem;\n}\n\n.about__team {\n  font-size: 2.2rem;\n  line-height: 3.2rem;\n  color: var(--color-text);\n}\n\n.about__teammates {\n  display: flex;\n  flex-direction: column;\n  gap: 4rem;\n}\n\n.about__teammates__wrap {\n  display: flex;\n  justify-content: space-evenly;\n  gap: 5rem;\n}\n\n@media (max-width: 1200px) {\n  .about {\n    padding: 3rem 0;\n    width: 95%;\n    margin: 0 auto;\n  }\n\n  .about__wwt {\n    width: 32rem;\n  }\n\n  .about__teammates__wrap {\n    flex-direction: column;\n  }\n}\n\n@media (max-width: 768px) {\n  .about {\n    width: 90%;\n  }\n\n  .about__teammates__wrap {\n    gap: 7rem;\n  }\n\n  .about__log {\n    height: 40rem;\n  }\n\n  .about__wwt {\n    width: 28rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/views/Catalog.vue",
    "content": "<template>\n  <div class=\"catalog\">\n    <div class=\"catalog__aside list-aside\">\n      <image-btn\n        :imgPath=\"`./img/catalog/download-${currTheme}.svg`\"\n        :disabled=\"!someChecked\"\n        :text=\"$t('btn.download')\"\n        className=\"btn btn-img btn-download\"\n        @click=\"downloadFiles\"\n      ></image-btn>\n      <controls-list\n        @search=\"getSloths\"\n        @tags=\"getSloths\"\n        @sorting=\"getSloths\"\n        @clearAll=\"getSloths\"\n        :placeholder=\"$t('catalog.search')\"\n        :tags=\"tags\"\n        :title=\"$t('catalog.sorting')\"\n        :options=\"sortingOptions\"\n        :text=\"$t('btn.reset')\"\n      >\n      </controls-list>\n      <image-btn\n        :text=\"$t('merch.title')\"\n        :imgPath=\"'./img/home/merch.svg'\"\n        className=\"btn btn-img btn-merch\"\n        @click=\"$router.push({ name: 'merch' })\"\n      ></image-btn>\n    </div>\n    <div class=\"catalog__main list-main\">\n      <pagination-list ref=\"pagination\" :size=\"count\" @getPage=\"getSloths\"></pagination-list>\n      <div class=\"catalog__list\">\n        <sloth-card\n          v-for=\"sloth in sloths\"\n          :key=\"sloth.id\"\n          :slothInfo=\"sloth\"\n          @showSloth=\"showSlothInfoView\"\n          @checkSloth=\"checkSlothInfoView\"\n        ></sloth-card>\n      </div>\n      <sloth-info\n        :isSlothInfoVisible=\"isSlothInfoVisible\"\n        :headerText=\"$t('catalog.info')\"\n        @closeSlothInfo=\"closeSlothInfo\"\n      ></sloth-info>\n    </div>\n    <modal-window v-show=\"isDownloadShow\" @close=\"closeModal\">\n      <template v-slot:header> {{ $t('modal.body.download') }} </template>\n\n      <template v-slot:body>\n        <div class=\"catalog__download\">\n          <sloth-card\n            v-for=\"sloth in checked\"\n            :key=\"sloth.name\"\n            :slothInfo=\"sloth\"\n            :isDownload=\"true\"\n            @checkSloth=\"checkSlothInfoView\"\n          ></sloth-card>\n        </div>\n      </template>\n\n      <template v-slot:footer>\n        <div class=\"catalog__btn\">\n          <custom-btn\n            :text=\"$t('btn.yes')\"\n            className=\"btn btn-primary\"\n            :onClick=\"approveDownload\"\n            :disabled=\"!someChecked\"\n          ></custom-btn>\n          <custom-btn :text=\"$t('btn.no')\" className=\"btn btn-primary\" :onClick=\"closeModal\"></custom-btn>\n        </div>\n      </template>\n    </modal-window>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapWritableState } from 'pinia';\nimport useThemeProp from '@/stores/theme';\nimport type { PageSettings, Sloth } from '@/common/types';\nimport { errorHandler } from '@/services/error-handler';\nimport { CDN_STICKERS_URL, PAGINATION_OPTIONS, SLOTH_SORTING } from '@/common/const';\nimport { SlothsService } from '@/services/sloths-service';\nimport useLoader from '@/stores/loader';\nimport usePagination from '@/stores/pagination';\nimport useSearchText from '@/stores/search-text';\nimport useSelectedTags from '@/stores/tag-cloud';\nimport useSortingList from '@/stores/sorting-list';\nimport useSlothInfo from '@/stores/sloth-info';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\nimport ImageBtn from '@/components/buttons/ImageBtn.vue';\nimport ControlsList from '@/components/controls-list/ControlsList.vue';\nimport PaginationList, { type PaginationListElement } from '@/components/controls-list/PaginationList.vue';\nimport SlothCard from '@/components/catalog/SlothCard.vue';\nimport SlothInfo from '@/components/catalog/SlothInfo.vue';\nimport ModalWindow from '@/components/modal/ModalWindow.vue';\nimport usePagesStore from '@/stores/pages-store';\nimport useSlothsStore from '@/stores/sloths';\nimport JSZip from 'jszip';\nimport { saveAs } from 'file-saver';\n\nconst { setSlothInfo } = useSlothInfo();\n\nconst { getPerPage, getCurrPage, setPerPage, setCurrPage } = usePagination();\nconst { getSearchText, setSearchText } = useSearchText();\nconst { getSelected, setSelected } = useSelectedTags();\nconst { getSortingList, setSortingList } = useSortingList();\nconst { getPageCatalogState, setPageCatalogState } = usePagesStore();\n\nconst { sloths } = useSlothsStore();\nconst service = new SlothsService(sloths);\n\nexport default defineComponent({\n  name: 'CatalogView',\n\n  components: {\n    CustomBtn,\n    ImageBtn,\n    SlothCard,\n    SlothInfo,\n    ControlsList,\n    PaginationList,\n    ModalWindow,\n  },\n\n  data() {\n    return {\n      sloths: [] as Sloth[],\n      count: 0,\n      isSlothInfoVisible: false,\n      tags: [] as string[],\n      sortingOptions: SLOTH_SORTING,\n      isDownloadShow: false,\n      checked: [] as Sloth[],\n    };\n  },\n\n  computed: {\n    ...mapWritableState(useLoader, ['isLoad']),\n    ...mapWritableState(useThemeProp, ['currTheme']),\n\n    someChecked(): boolean {\n      return this.checked.some(el => el.checked);\n    },\n  },\n\n  created() {\n    this.loadStore();\n  },\n\n  async mounted() {\n    await this.getSloths();\n  },\n\n  beforeUnmount() {\n    this.saveStore();\n  },\n\n  watch: {\n    someChecked(newVal) {\n      if (!newVal) {\n        this.isDownloadShow = false;\n      }\n    },\n  },\n\n  methods: {\n    async getSloths() {\n      this.isLoad = true;\n      try {\n        const page = getCurrPage();\n        const limit = getPerPage();\n        const searchText = getSearchText();\n        const selected = getSelected();\n        const sorting = getSortingList();\n\n        const res = await service.getAll({ page, limit, order: sorting, searchText, filter: selected.join(',') });\n\n        this.sloths = res.data.items;\n        this.count = res.data.count;\n\n        if (!this.sloths.length && page !== 1) {\n          const pagination = this.$refs.pagination as PaginationListElement;\n          if (pagination) pagination.goTop();\n        }\n\n        this.setChecked();\n\n        await this.getTags();\n\n        this.saveStore();\n      } catch (error) {\n        errorHandler(error);\n      } finally {\n        this.isLoad = false;\n      }\n    },\n\n    async getTags() {\n      this.isLoad = true;\n      try {\n        const res = service.getTags();\n\n        if (!res) throw new Error(this.$t('catalog.tagsNotFound'));\n\n        this.tags = res.slice();\n      } catch (error) {\n        errorHandler(error);\n      } finally {\n        this.isLoad = false;\n      }\n    },\n\n    checkSlothInfoView(sloth: Sloth) {\n      let slothIndex = this.sloths.indexOf(sloth);\n      if (slothIndex !== -1) this.sloths[slothIndex].checked = !this.sloths[slothIndex].checked;\n\n      slothIndex = this.checked.findIndex(el => el.id === sloth.id);\n      if (slothIndex !== -1) {\n        this.checked.splice(slothIndex, 1);\n      } else {\n        this.checked.push(sloth);\n      }\n\n      this.saveStore();\n    },\n\n    setChecked() {\n      this.sloths.forEach(sloth => {\n        const slothIndex = this.checked.findIndex(el => el.id === sloth.id);\n        this.sloths[this.sloths.indexOf(sloth)].checked = slothIndex !== -1;\n      });\n    },\n\n    async showSlothInfoView(sloth: Sloth) {\n      this.isLoad = true;\n      try {\n        const res = await service.getById(sloth.id);\n\n        if (!res) throw new Error(`${this.$t('catalog.idNotFound')} (${sloth.id})`);\n\n        const dataSloth = res;\n        const slothIndex = this.sloths.findIndex(el => el.id === sloth.id);\n\n        if (slothIndex !== -1) this.sloths[slothIndex] = dataSloth;\n\n        setSlothInfo(dataSloth);\n        this.showSlothInfo();\n      } catch (error) {\n        errorHandler(error);\n      } finally {\n        this.isLoad = false;\n      }\n    },\n\n    showSlothInfo() {\n      this.isSlothInfoVisible = true;\n    },\n\n    closeSlothInfo() {\n      this.isSlothInfoVisible = false;\n    },\n\n    downloadFiles() {\n      if (this.checked.length) this.isDownloadShow = true;\n    },\n\n    async approveDownload() {\n      const forDownload = this.checked.filter(el => el.checked).map(el => el.id);\n\n      if (!forDownload.length) return;\n\n      // download\n      await this.downloadZip(forDownload);\n\n      this.closeModal();\n    },\n\n    async downloadZip(ids: string[]) {\n      const zip = JSZip();\n      const zipFilename = `sloths_${new Date().toISOString()}.zip`;\n\n      ids.forEach(id => {\n        const blobPromise = fetch(`${CDN_STICKERS_URL}/${id}/image.svg`).then(r => {\n          if (r.status === 200) return r.blob();\n          return Promise.reject(new Error(r.statusText));\n        });\n        zip.file(`${id}.svg`, blobPromise);\n      });\n\n      zip\n        .generateAsync({ type: 'blob' })\n        .then(blob => saveAs(blob, zipFilename))\n        .catch(e => errorHandler(e));\n    },\n\n    closeModal() {\n      this.setChecked();\n      this.isDownloadShow = false;\n    },\n\n    saveStore() {\n      const savedProps = {\n        currPage: getCurrPage(),\n        perPage: getPerPage(),\n        searchText: getSearchText(),\n        selected: getSelected(),\n        sorting: getSortingList(),\n        checked: this.checked.filter(el => el.checked).map(el => el.id),\n      };\n      setPageCatalogState(JSON.stringify(savedProps));\n    },\n\n    loadStore() {\n      const settings: PageSettings = {\n        currPage: 1,\n        perPage: PAGINATION_OPTIONS[0],\n        searchText: '',\n        selected: [] as string[],\n        sorting: SLOTH_SORTING[0].value,\n        checked: [] as string[],\n      };\n\n      const str = getPageCatalogState();\n      if (str) {\n        const data = JSON.parse(str);\n        if (data) {\n          settings.currPage = data.currPage;\n          settings.perPage = data.perPage;\n          settings.searchText = data.searchText;\n          settings.selected = data.selected;\n          settings.sorting = data.sorting;\n          settings.checked = data.checked;\n        }\n      }\n\n      setCurrPage(settings.currPage);\n      setPerPage(settings.perPage);\n      setSearchText(settings.searchText);\n      setSelected(settings.selected);\n      setSortingList(settings.sorting);\n\n      const { checked } = settings;\n      checked?.forEach((id: string) => {\n        const found = this.sloths.find(el => el.id === id);\n        if (found) found.checked = true;\n      });\n    },\n  },\n});\n</script>\n\n<style scoped>\n.catalog {\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  gap: 2rem;\n  color: var(--color-text);\n}\n\n.catalog__main {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  gap: var(--gap-2);\n}\n\n.catalog__aside {\n  padding: 1rem;\n}\n\n.catalog__list {\n  display: flex;\n  flex-direction: row;\n  justify-content: flex-start;\n  flex-wrap: wrap;\n  gap: 1rem;\n}\n\n.catalog__download {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  gap: var(--gap);\n\n  max-height: 50rem;\n  overflow: scroll;\n}\n\n.catalog__btn {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: var(--gap);\n}\n\n@media (max-width: 768px) {\n  .catalog {\n    flex-direction: column;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/views/Create.vue",
    "content": "<template>\n  <div class=\"meme\">\n    <div class=\"meme__list list-aside\">\n      <h3>{{ $t('create.description') }}</h3>\n      <div class=\"meme__memes\">\n        <img\n          ref=\"imgs\"\n          v-for=\"(item, index) in images\"\n          :key=\"`${index}_${item}`\"\n          :src=\"getImg(index)\"\n          alt=\"images\"\n          object-fit=\"contain\"\n          class=\"meme__image\"\n          crossOrigin=\"anonymous\"\n          @click=\"updateImage(index)\"\n        />\n      </div>\n    </div>\n    <div class=\"meme__generator list-main\">\n      <div class=\"meme__settings\">\n        <div class=\"meme__property\">\n          <label class=\"meme__label\" for=\"top\">{{ $t('create.top') }}</label>\n          <input type=\"text\" class=\"meme__text\" id=\"top\" v-model=\"topCanvasElement.text\" @input=\"draw()\" />\n        </div>\n        <div class=\"meme__property\">\n          <label class=\"meme__label\" for=\"bottom\">{{ $t('create.bottom') }}</label>\n          <input type=\"text\" class=\"meme__text\" id=\"bottom\" v-model=\"bottomCanvasElement.text\" @input=\"draw()\" />\n        </div>\n      </div>\n      <div class=\"meme__settings\">\n        <div class=\"meme__property\">\n          <label class=\"meme__label\" for=\"color\">{{ $t('create.color') }}</label>\n          <input type=\"color\" id=\"color\" class=\"meme__color\" v-model=\"canvasProps.textColor\" @input=\"draw()\" />\n        </div>\n        <div class=\"meme__property\">\n          <label class=\"meme__label\" for=\"strokeStyle\">{{ $t('create.stroke') }}</label>\n          <input type=\"color\" id=\"strokeStyle\" class=\"meme__color\" v-model=\"canvasProps.strokeColor\" @input=\"draw()\" />\n        </div>\n        <div class=\"meme__property\">\n          <label class=\"meme__label\" for=\"backgroundTransparent\">{{ $t('create.backgroundTransparent') }}</label>\n          <input\n            type=\"checkbox\"\n            id=\"backgroundTransparent\"\n            class=\"meme__transparent\"\n            v-model=\"canvasProps.backgroundTransparent\"\n            @change=\"draw()\"\n          />\n        </div>\n        <div class=\"meme__property\" :class=\"{ 'meme__property-disabled': canvasProps.backgroundTransparent }\">\n          <label class=\"meme__label\" for=\"backgroundColor\">{{ $t('create.backgroundColor') }}</label>\n          <input\n            type=\"color\"\n            id=\"backgroundColor\"\n            class=\"meme__color\"\n            v-model=\"canvasProps.backgroundColor\"\n            @input=\"draw()\"\n            :disabled=\"canvasProps.backgroundTransparent\"\n          />\n        </div>\n      </div>\n\n      <div class=\"meme__canvas-wrapper\">\n        <div class=\"meme__control-buttons\">\n          <icon-btn\n            :text=\"$t('btn.download')\"\n            imgPath=\"icon\"\n            className=\"btn btn-icon icon-download\"\n            @click=\"saveImage\"\n          ></icon-btn>\n          <div class=\"meme__control-buttons-scale\">\n            <icon-btn\n              :text=\"$t('btn.scaleUp')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-plus\"\n              @click=\"scaleUp()\"\n            ></icon-btn>\n            <icon-btn\n              :text=\"$t('btn.trueSize')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-true\"\n              @click=\"scaleTrue()\"\n            ></icon-btn>\n            <icon-btn\n              :text=\"$t('btn.center')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-center\"\n              @click=\"centering\"\n            ></icon-btn>\n            <icon-btn\n              :text=\"$t('btn.scaleDown')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-minus\"\n              @click=\"scaleDown()\"\n            ></icon-btn>\n          </div>\n          <icon-btn\n            :text=\"$t('btn.copy')\"\n            imgPath=\"icon\"\n            className=\"btn btn-icon icon-copy\"\n            @click=\"copyImage\"\n          ></icon-btn>\n        </div>\n        <canvas\n          class=\"meme__canvas\"\n          ref=\"canvas\"\n          @mousemove=\"handleMouseMove\"\n          @mousedown=\"handleMouseDown\"\n          @mouseup=\"handleMouseUp\"\n          @mouseout=\"handleMouseOut\"\n          @wheel=\"handleWheel\"\n        >\n        </canvas>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport IconBtn from '@/components/buttons/IconBtn.vue';\nimport usePagesStore from '@/stores/pages-store';\nimport useCleanedStore from '@/stores/cleaned';\nimport type { CanvasElement } from '@/common/types';\nimport * as CanvasUtils from '@/utils/canvas-utils';\n\nconst { cleanedFilelist } = useCleanedStore();\nconst { getPageCreateState, setPageCreateState } = usePagesStore();\n\nexport default defineComponent({\n  name: 'CreateView',\n\n  components: {\n    IconBtn,\n  },\n\n  data() {\n    return {\n      images: [] as string[],\n      index: 0,\n      canvas: {} as HTMLCanvasElement,\n      canvasProps: CanvasUtils.initProperties(0.5, 1, 1.5),\n      ctx: {} as CanvasRenderingContext2D,\n      img: {} as HTMLImageElement,\n      imgCanvasElement: CanvasUtils.initElement(0, 0, 1, 0.1, 1, 2, false),\n      topCanvasElement: CanvasUtils.initElement(CanvasUtils.textMargin, 0, 1, 0.1, 1, 2),\n      bottomCanvasElement: CanvasUtils.initElement(0, CanvasUtils.canvasSize - CanvasUtils.textMargin, 1, 0.1, 1, 2),\n      layers: [] as CanvasElement[],\n    };\n  },\n\n  beforeMount() {\n    this.getImages();\n  },\n\n  async mounted() {\n    const loaded = this.loadStore();\n\n    const { canvas } = this.$refs;\n    if (!(canvas instanceof HTMLCanvasElement)) return;\n\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n\n    this.canvas = canvas;\n    this.ctx = ctx;\n\n    this.img = await CanvasUtils.loadImage(this.images[this.index]);\n    CanvasUtils.calcCanvasSizes(this.canvas, this.canvasProps.scaleSteps);\n    CanvasUtils.calcElementsSizes(\n      this.img,\n      this.imgCanvasElement,\n      this.topCanvasElement,\n      this.bottomCanvasElement,\n      this.canvasProps.scaleSteps,\n    );\n    if (!loaded) this.centering();\n\n    this.layers[0] = this.imgCanvasElement;\n    this.layers[1] = this.topCanvasElement;\n    this.layers[2] = this.bottomCanvasElement;\n\n    this.draw();\n  },\n\n  beforeRouteLeave() {\n    setPageCreateState(JSON.stringify(this.$data));\n  },\n\n  computed: {\n    cursorPointer() {\n      return CanvasUtils.getCursor(this.layers);\n    },\n  },\n\n  methods: {\n    async getImages() {\n      this.images = cleanedFilelist;\n    },\n\n    getImg(i: number): string {\n      return this.images[i];\n    },\n\n    scaleUp() {\n      CanvasUtils.scaleUpCanvas(this.canvasProps);\n\n      this.draw();\n    },\n\n    scaleTrue() {\n      CanvasUtils.scaleTrueCanvas(this.canvasProps, this.layers);\n\n      this.draw();\n    },\n\n    scaleDown() {\n      CanvasUtils.scaleDownCanvas(this.canvasProps);\n\n      this.draw();\n    },\n\n    centering() {\n      CanvasUtils.centeringElements(\n        this.imgCanvasElement,\n        this.topCanvasElement,\n        this.bottomCanvasElement,\n        this.canvasProps.scaleSteps,\n      );\n\n      this.draw();\n    },\n\n    draw() {\n      CanvasUtils.calcCanvasSizes(this.canvas, this.canvasProps.scaleSteps);\n      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n      CanvasUtils.drawBackground(this.canvas, this.ctx, this.canvasProps);\n\n      CanvasUtils.calcElementsSizes(\n        this.img,\n        this.imgCanvasElement,\n        this.topCanvasElement,\n        this.bottomCanvasElement,\n        this.canvasProps.scaleSteps,\n      );\n      CanvasUtils.calcElementsPosition(this.layers, this.canvasProps.scaleSteps);\n\n      this.ctx.drawImage(\n        this.img,\n        0,\n        0,\n        this.imgCanvasElement.width,\n        this.imgCanvasElement.height,\n        this.imgCanvasElement.scaledLeft,\n        this.imgCanvasElement.scaledTop,\n        this.imgCanvasElement.scaledWidth,\n        this.imgCanvasElement.scaledHeight,\n      );\n\n      const { scaleSteps } = this.canvasProps;\n      const { height } = this.canvas;\n      CanvasUtils.drawTextDown(height, this.ctx, this.canvasProps, this.topCanvasElement, scaleSteps, true);\n      CanvasUtils.drawTextUp(height, this.ctx, this.canvasProps, this.bottomCanvasElement, scaleSteps, true);\n\n      const color = CanvasUtils.invertHex(this.canvasProps.backgroundColor, this.canvasProps.backgroundTransparent);\n      this.layers.forEach(el => CanvasUtils.drawBorder(el, this.ctx, color));\n    },\n\n    handleMouseMove(e: MouseEvent) {\n      CanvasUtils.moveElements(e, this.canvas, this.layers, this.canvasProps.scaleSteps);\n\n      this.draw();\n    },\n\n    handleMouseDown(e: MouseEvent) {\n      CanvasUtils.selectElements(e, this.canvas, this.layers);\n\n      this.draw();\n    },\n\n    handleMouseUp() {\n      CanvasUtils.deselectElements(this.layers);\n\n      this.draw();\n    },\n\n    handleMouseOut() {\n      CanvasUtils.unhoverElements(this.layers);\n      this.handleMouseUp();\n    },\n\n    handleWheel(e: WheelEvent) {\n      CanvasUtils.scalingElements(e, this.layers, this.canvasProps);\n\n      this.draw();\n    },\n\n    updateImage(i: number) {\n      this.index = i;\n\n      const { imgs } = this.$refs;\n      if (!(imgs instanceof Array)) return;\n\n      const image = imgs[this.index];\n      if (!(image instanceof HTMLImageElement)) return;\n\n      this.img = image;\n      CanvasUtils.calcElementsSizes(\n        this.img,\n        this.imgCanvasElement,\n        this.topCanvasElement,\n        this.bottomCanvasElement,\n        this.canvasProps.scaleSteps,\n      );\n      this.centering();\n\n      this.draw();\n    },\n\n    saveImage() {\n      this.canvas.toDataURL();\n      const link = document.createElement('a');\n      link.download = 'download.png';\n      link.href = this.canvas.toDataURL();\n      link.click();\n    },\n\n    copyImage() {\n      this.canvas.toBlob(blob => {\n        const type = blob?.type;\n        if (!type) return;\n        const item = new ClipboardItem({ [type]: blob });\n        navigator.clipboard.write([item]);\n      });\n    },\n\n    loadStore(): boolean {\n      const str = getPageCreateState();\n      if (!str) return false;\n\n      const data = JSON.parse(str);\n      if (!data) return false;\n\n      Object.assign(this.$data, data);\n      return true;\n    },\n  },\n});\n</script>\n<style scoped>\n.meme,\n.meme__generator {\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  gap: var(--gap);\n  color: var(--color-text);\n}\n\n.meme {\n  padding-left: 3rem;\n  height: 100%;\n}\n\n.meme__list {\n  height: 100%;\n  overflow-y: auto;\n}\n\n.meme__generator {\n  flex-direction: column;\n  align-items: center;\n  height: 100%;\n  padding-right: 3rem;\n  overflow-y: auto;\n}\n\n.meme__memes {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  align-items: flex-start;\n  gap: var(--gap);\n}\n\n.meme__image {\n  width: 14rem;\n  height: 14rem;\n  object-fit: contain;\n}\n\n.meme__settings {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: space-between;\n  gap: 0.5rem;\n}\n\n.meme__property {\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--gap);\n}\n\n.meme__property-disabled {\n  opacity: 0.45;\n}\n\n.meme__text,\n.meme__number {\n  padding: 0.5rem;\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  background-color: var(--color-background);\n  color: inherit;\n  border-radius: 1rem;\n  transition: 0.5s ease;\n}\n\n.meme__color {\n  padding: 0 0;\n  border: none;\n  background: none;\n  width: 6.2rem;\n  height: 3.2rem;\n  transition: 0.5s ease;\n}\n\n.meme__color::-webkit-color-swatch-wrapper {\n  padding: 0;\n}\n\n.meme__color::-webkit-color-swatch {\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  border-radius: 1rem;\n}\n\n.meme__transparent {\n  width: 2rem;\n  height: 2rem;\n  margin: 0.6rem 0;\n  accent-color: var(--color-background-inverse);\n}\n\n.meme__canvas-wrapper {\n  position: relative;\n  padding-top: 2rem;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\n.meme__control-buttons {\n  z-index: 10;\n  display: flex;\n  justify-content: center;\n  gap: 1rem;\n}\n\n.meme__control-buttons-scale button {\n  margin: 0 0.25rem;\n  padding: 0;\n}\n\n.meme__canvas {\n  border: 0.2px solid gray;\n}\n.meme__canvas:hover {\n  cursor: v-bind(cursorPointer);\n}\n\n@media (max-width: 1200px) {\n  .meme {\n    padding-left: 1.5rem;\n  }\n\n  .meme__generator {\n    padding-right: 1.5rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/views/Guess.vue",
    "content": "<template>\n  <div class=\"guess\">\n    <custom-btn\n      :text=\"$t('guess.results')\"\n      className=\"btn btn-primary\"\n      @click=\"isTableResultsVisible = true\"\n    ></custom-btn>\n    <h2 class=\"guess__description\">{{ $t('guess.description') }}</h2>\n    <h3 class=\"guess__description\">{{ $t('guess.rules') }}</h3>\n    <custom-btn\n      v-show=\"step < 0\"\n      :text=\"$t('guess.start')\"\n      className=\"btn btn-primary\"\n      :onClick=\"startGame\"\n    ></custom-btn>\n\n    <div class=\"guess__imgs\" :class=\"step >= 0 ? 'guess__imgs_active' : ''\">\n      <div v-for=\"(item, index) in gameCards\" :key=\"`${index}_guess-card`\" class=\"guess__img-wrapper\">\n        <transition name=\"slider\" mode=\"out-in\">\n          <img v-show=\"index === step\" :src=\"item.question.img\" :alt=\"$t('guess.guess')\" class=\"guess__img\" />\n        </transition>\n      </div>\n    </div>\n    <div v-for=\"(item, index) in gameCards\" :key=\"`${index}_guess-answer`\">\n      <div v-show=\"index === step\" class=\"guess__answers\">\n        <span\n          v-for=\"(answer, i) in item.answers\"\n          :key=\"`${i}_${answer.caption}`\"\n          :class=\"`guess__answer ${getClassStepSelect(i)}`\"\n          @click=\"setAnswer(index, i)\"\n          v-shortkey.once=\"[`${i + 1}`]\"\n          @shortkey=\"setAnswer(step, i)\"\n        >\n          {{ i + 1 }} - {{ answer.caption }}\n        </span>\n      </div>\n    </div>\n\n    <custom-btn\n      v-show=\"step >= 0\"\n      :text=\"$t('guess.next')\"\n      className=\"btn btn-primary\"\n      :disabled=\"stepSelection < 0\"\n      :onClick=\"nextStep\"\n      v-shortkey=\"['enter']\"\n      @shortkey=\"nextStep\"\n    ></custom-btn>\n    <div v-show=\"step >= 0\" class=\"guess__results\">\n      <div\n        v-for=\"(res, index) in result\"\n        :key=\"`${index}_guess-result`\"\n        :class=\"`guess__result ${getClassStepResult(index)}`\"\n      ></div>\n    </div>\n\n    <modal-window v-show=\"isModalVisible\" @close=\"closeModal\">\n      <template v-slot:header> {{ $t('guess.congrats') }} </template>\n\n      <template v-slot:body>\n        <div class=\"guess-modal__wrap\">\n          <img class=\"guess-modal__img\" :src=\"cardWinner\" alt=\"winner\" />\n          <p class=\"guess-modal__points\">{{ Math.round((guesses * 100) / gameCards.length) }} %</p>\n        </div>\n        <p>{{ allGuesses ? `${$t('guess.win')} ` : '' }}{{ $t('guess.result') }}</p>\n        <p>{{ guesses }} / {{ gameCards.length }} {{ $t('guess.guesses') }}</p>\n        <p>{{ gameTime / millisecondsInSecond }} {{ $t('memory.time') }}</p>\n      </template>\n    </modal-window>\n    <modal-window v-show=\"isTableResultsVisible\" @close=\"closeTableResults\">\n      <template v-slot:header> {{ $t('guess.results') }} </template>\n      <template v-slot:body>\n        <guess-info :isVisible=\"isTableResultsVisible\"></guess-info>\n      </template>\n    </modal-window>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport ModalWindow from '@/components/modal/ModalWindow.vue';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\nimport GuessInfo from '@/components/guess/GuessInfo.vue';\nimport { GUESS_GAME_WINNER, GUESS_SLOTHS, MILLISECONDS_IN_SECOND } from '@/common/const';\nimport { playAudio, audioWin, audioSadTrombone, audioOvation } from '@/utils/audio';\nimport type { GameResult } from '@/common/types';\n\ntype Card = {\n  caption: string;\n  img: string;\n};\n\ntype GameCard = {\n  question: Card;\n  answers: Card[];\n};\n\nexport default defineComponent({\n  name: 'GuessView',\n\n  components: {\n    CustomBtn,\n    ModalWindow,\n    GuessInfo,\n  },\n\n  data() {\n    return {\n      questions: [] as Card[],\n      answers: [] as Card[],\n      gameCards: [] as GameCard[],\n      result: [] as boolean[],\n      startTime: 0,\n      endTime: 0,\n      step: -1,\n      stepSelection: -1,\n      stepAnswer: false,\n      cardWinner: GUESS_GAME_WINNER,\n      isAnimated: false,\n      isModalVisible: false,\n      isTableResultsVisible: false,\n    };\n  },\n\n  computed: {\n    guesses(): number {\n      return this.result.filter(el => el).length;\n    },\n\n    allGuesses(): boolean {\n      return this.guesses === this.gameCards.length;\n    },\n\n    gameTime(): number {\n      return this.endTime - this.startTime;\n    },\n\n    millisecondsInSecond(): number {\n      return MILLISECONDS_IN_SECOND;\n    },\n  },\n\n  mounted() {\n    this.initCards();\n    this.startGame();\n  },\n\n  methods: {\n    initCards() {\n      this.questions = GUESS_SLOTHS;\n      this.answers = GUESS_SLOTHS;\n    },\n\n    startGame() {\n      this.getGameCards();\n\n      this.endTime = 0;\n      this.startTime = Date.now();\n      this.step = 0;\n      this.stepSelection = -1;\n      this.stepAnswer = false;\n    },\n\n    getGameCards() {\n      const gameCards = [] as GameCard[];\n      this.result = [];\n\n      this.questions.forEach(question => {\n        const newGameCard = {} as GameCard;\n        newGameCard.question = question;\n\n        const trueCard = this.answers.filter(el => el.caption === question.caption);\n        const answers = this.answers\n          .filter(el => el.caption !== question.caption)\n          .sort(() => Math.random() - 0.5)\n          .filter((el, i) => i < 3);\n        answers.push(trueCard[0]);\n\n        newGameCard.answers = answers.sort(() => Math.random() - 0.5);\n\n        gameCards.push(newGameCard);\n        this.result.push(false);\n      });\n\n      this.gameCards = gameCards.sort(() => Math.random() - 0.5);\n    },\n\n    setAnswer(question: number, answer: number) {\n      this.stepSelection = answer;\n      this.stepAnswer = this.gameCards[question].question.caption === this.gameCards[question].answers[answer].caption;\n    },\n\n    nextStep() {\n      if (this.stepSelection >= 0) {\n        this.result[this.step] = this.stepAnswer;\n\n        if (this.stepAnswer) {\n          playAudio(audioWin);\n        } else {\n          playAudio(audioSadTrombone);\n        }\n\n        this.step += 1;\n        this.stepAnswer = false;\n        this.stepSelection = -1;\n\n        if (this.step === this.gameCards.length) {\n          // end\n          this.endTime = Date.now();\n          playAudio(audioOvation);\n          this.isModalVisible = true;\n          this.saveResult();\n\n          this.step = -1;\n          this.stepSelection = -1;\n        }\n      }\n    },\n\n    getClassStepResult(i: number) {\n      if (i >= this.step) return '';\n      return this.result[i] ? 'is-guess' : 'is-not-guess';\n    },\n\n    getClassStepSelect(i: number) {\n      return i === this.stepSelection ? 'active' : '';\n    },\n\n    closeModal() {\n      this.isModalVisible = false;\n    },\n\n    saveResult() {\n      let currResults: GameResult[] = [];\n      const savedRecords = localStorage.getItem('rs-sloths-guess');\n\n      if (savedRecords) {\n        currResults = JSON.parse(savedRecords);\n      }\n\n      const gameResult: GameResult = {\n        count: this.guesses,\n        time: this.gameTime,\n        createdAt: new Date().getTime(),\n      };\n\n      currResults.unshift(gameResult);\n\n      if (currResults.length > 10) {\n        currResults.pop();\n      }\n\n      localStorage.setItem('rs-sloths-guess', JSON.stringify(currResults));\n    },\n\n    closeTableResults() {\n      this.isTableResultsVisible = false;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.guess {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\n.guess__description {\n  text-align: center;\n  color: var(--color-text);\n}\n\n.guess__imgs {\n  position: relative;\n  width: 40rem;\n  height: 40rem;\n}\n\n.guess__imgs_active {\n  background: no-repeat center center / contain url('../img/guess/bg.svg');\n}\n\n.guess__img-wrapper {\n  position: absolute;\n  width: 30rem;\n  height: 30rem;\n  top: 5rem;\n  left: 5rem;\n  overflow: hidden;\n}\n\n.guess__img {\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  z-index: 1;\n}\n\n.guess__answers {\n  padding: 1rem 0;\n\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: calc(var(--gap) / 2);\n\n  color: var(--color-text);\n}\n\n.guess__answer {\n  padding: 0.5rem 0.7rem;\n  cursor: pointer;\n\n  color: var(--color-text);\n  background-color: var(--color-background-soft);\n  border: 0.2rem solid var(--color-border-theme);\n  border-radius: 0.5rem;\n  text-decoration: none;\n}\n\n.guess__answer:hover {\n  border-color: var(--color-border-inverse);\n}\n\n.active {\n  color: var(--color-text-inverse);\n  background-color: var(--color-background-inverse);\n  border-color: var(--color-border-inverse);\n}\n\n.guess__results {\n  margin-top: 1rem;\n  display: flex;\n  justify-content: center;\n  flex-wrap: wrap;\n  gap: 1rem;\n}\n\n.guess__result {\n  display: inline-block;\n  width: 2rem;\n  height: 2rem;\n  border-radius: 50%;\n  background-color: gray;\n}\n\n.is-guess {\n  background-color: var(--green-active);\n}\n\n.is-not-guess {\n  background-color: var(--red-active);\n}\n\n.guess-modal__wrap {\n  position: relative;\n  width: 35rem;\n  height: 35rem;\n}\n\n.guess-modal__img {\n  width: 100%;\n  height: 100%;\n  object-fit: contain;\n}\n\n.guess-modal__points {\n  position: absolute;\n  top: 110px;\n  left: 7px;\n  text-align: center;\n  width: 10rem;\n  color: black;\n  font-weight: 700;\n  font-size: 28px;\n}\n\n.slider-enter-active {\n  animation: slider-out 1s;\n}\n.slider-leave-active {\n  animation: slider-in 1s;\n}\n\n@keyframes slider-in {\n  0% {\n    opacity: 1;\n    transform: translateX(-50%);\n  }\n  100% {\n    opacity: 0;\n    transform: translateX(-150%);\n  }\n}\n\n@keyframes slider-out {\n  0% {\n    opacity: 0;\n    transform: translateX(50%);\n  }\n  100% {\n    opacity: 1;\n    transform: translateX(-50%);\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/views/Home.vue",
    "content": "<template>\n  <div class=\"home\">\n    <home-about></home-about>\n    <home-catalog @click=\"handleCategoryClick('catalog')\"></home-catalog>\n    <div class=\"home__menu\">\n      <home-category\n        v-for=\"(category, i) in categories\"\n        :key=\"`${i}_${category}`\"\n        :category=\"category\"\n        @click=\"handleCategoryClick(category)\"\n      ></home-category>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport HomeCategory from '../components/home/HomeCategory.vue';\nimport HomeAbout from '../components/home/HomeAbout.vue';\nimport HomeCatalog from '../components/home/HomeCatalog.vue';\n\nexport default defineComponent({\n  name: 'HomeView',\n\n  components: {\n    HomeCategory,\n    HomeAbout,\n    HomeCatalog,\n  },\n\n  data(): { categories: string[] } {\n    return {\n      categories: ['memory', 'create', 'guess', 'merch'],\n    };\n  },\n\n  methods: {\n    handleCategoryClick(category: string): void {\n      this.$router.push({ name: `${category}` });\n    },\n  },\n});\n</script>\n\n<style scoped>\n.home {\n  height: 100%;\n  display: grid;\n  gap: 8rem;\n  justify-items: center;\n  align-items: center;\n  justify-content: center;\n  grid-template-columns: 40rem 40rem 40rem;\n  margin: 0 auto;\n  padding: 2rem 0;\n}\n\n.home__menu {\n  display: grid;\n  justify-items: center;\n  justify-content: center;\n  grid-template-columns: repeat(2, 20rem);\n  grid-template-rows: repeat(2, 22rem);\n  gap: 5rem 1rem;\n}\n\n@media (max-width: 1400px) {\n  .home {\n    grid-template-columns: 30rem 40rem;\n    grid-template-rows: 50rem auto;\n    gap: 5rem 3rem;\n    grid-template-areas:\n      'A B'\n      'C C';\n  }\n\n  .home__menu {\n    grid-area: C;\n    gap: 2rem;\n  }\n}\n\n@media (max-width: 768px) {\n  .home {\n    grid-template-columns: 40rem;\n    grid-template-rows: auto;\n    gap: 5rem;\n    grid-template-areas:\n      'B'\n      'C'\n      'A';\n  }\n\n  .home__menu {\n    gap: 1.5rem;\n  }\n}\n\n@media (max-width: 420px) {\n  .home {\n    grid-template-columns: 30rem;\n    gap: 1rem;\n  }\n\n  .home__menu {\n    grid-template-columns: repeat(2, 15rem);\n    gap: 1rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/views/Memory.vue",
    "content": "<template>\n  <div class=\"memory\">\n    <div class=\"memory__aside list-aside\">\n      <h3 class=\"memory__subtitle\">{{ $t('memory.level') }}</h3>\n      <h2 class=\"memory__title\">{{ $t(level) }}</h2>\n      <div class=\"memory__level\">\n        <div\n          v-for=\"(item, index) in levels\"\n          :key=\"item.level\"\n          class=\"btn btn-img memory__btn\"\n          :class=\"{ active_lvl: activeLevel === index }\"\n          @click=\"setLevel(index)\"\n        >\n          <img :src=\"getImg(index)\" :alt=\"$t(getText(index))\" />\n        </div>\n      </div>\n      <custom-btn\n        :text=\"$t('memory.results')\"\n        className=\"btn btn-primary\"\n        @click=\"isTableResultsVisible = true\"\n      ></custom-btn>\n    </div>\n    <div class=\"memory__main list-main\">\n      <game-field :level=\"levels[activeLevel]\"></game-field>\n    </div>\n    <modal-window v-show=\"isTableResultsVisible\" @close=\"closeTableResults\">\n      <template v-slot:header> {{ $t('memory.results') }} </template>\n      <template v-slot:body>\n        <memory-info :isVisible=\"isTableResultsVisible\"></memory-info>\n      </template>\n    </modal-window>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport GameField from '@/components/memory/GameField.vue';\nimport { MEMORY_LEVELS } from '@/common/const';\nimport usePagesStore from '@/stores/pages-store';\nimport MemoryInfo from '@/components/memory/MemoryInfo.vue';\nimport ModalWindow from '@/components/modal/ModalWindow.vue';\nimport CustomBtn from '@/components/buttons/CustomBtn.vue';\n\nconst { getPageMemoryState, setPageMemoryState } = usePagesStore();\n\nexport default defineComponent({\n  name: 'MemoryView',\n\n  components: {\n    GameField,\n    MemoryInfo,\n    ModalWindow,\n    CustomBtn,\n  },\n\n  data() {\n    return {\n      levels: MEMORY_LEVELS,\n      activeLevel: 1,\n      isTableResultsVisible: false,\n    };\n  },\n\n  computed: {\n    level(): string {\n      return `memory.${this.levels[this.activeLevel].level}`;\n    },\n  },\n\n  mounted() {\n    const str = getPageMemoryState();\n    if (!str) return;\n\n    const data = JSON.parse(str);\n    if (!data) return;\n\n    this.activeLevel = data.activeLevel;\n  },\n\n  beforeRouteLeave() {\n    setPageMemoryState(JSON.stringify(this.$data));\n  },\n\n  methods: {\n    getText(i: number): string {\n      return `memory.${this.levels[i].level}`;\n    },\n\n    getImg(i: number): string {\n      return `./img/memory/memory-level-${this.levels[i].level}.svg`;\n    },\n\n    setLevel(i: number) {\n      this.activeLevel = i;\n    },\n\n    closeTableResults() {\n      this.isTableResultsVisible = false;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.memory {\n  padding: 0 3rem;\n  display: flex;\n  gap: 2rem;\n}\n\n.memory__aside {\n  width: 300px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 1em;\n  color: var(--color-text);\n}\n\n.memory__title,\n.memory__subtitle {\n  transition: 0.5s ease;\n}\n\n.memory__main {\n  flex: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.memory__level {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n}\n.memory__btn img {\n  width: 15rem;\n  height: 15rem;\n  object-fit: contain;\n  border-radius: 50%;\n  background-color: lightgray;\n}\n\n.active_lvl img {\n  box-shadow: 0px 0px 5px;\n  border-color: var(--red-active);\n}\n\n@media (max-width: 1200px) {\n  .memory__aside {\n    width: 15rem;\n  }\n\n  .memory__btn img {\n    width: 10rem;\n    height: 10rem;\n  }\n}\n\n@media (max-width: 360px) {\n  .memory__aside {\n    width: 10rem;\n  }\n\n  .memory__btn img {\n    width: 10rem;\n    height: 10rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/src/views/Merch.vue",
    "content": "<template>\n  <div class=\"merch\">\n    <div class=\"merch__list list-aside\">\n      <div class=\"merch__option\">\n        <div\n          :title=\"$t('btn.change')\"\n          class=\"merch__change\"\n          :class=\"`merch__change_${currItems}`\"\n          @click=\"changeItems\"\n        ></div>\n      </div>\n      <h3>{{ $t('merch.description') }}</h3>\n      <div class=\"merch__images\">\n        <img\n          ref=\"imgs\"\n          v-for=\"(item, index) in images\"\n          :key=\"item.id\"\n          :src=\"getImg(index)\"\n          alt=\"image\"\n          object-fit=\"contain\"\n          class=\"merch__image\"\n          crossOrigin=\"anonymous\"\n          @click=\"updImage(index)\"\n        />\n      </div>\n    </div>\n    <div class=\"merch__generator list-main\">\n      <div class=\"merch__merch\">\n        <img\n          ref=\"merch\"\n          v-for=\"(item, index) in merch\"\n          :key=\"item.id\"\n          :src=\"getMerch(index)\"\n          alt=\"merch\"\n          object-fit=\"contain\"\n          class=\"merch__image\"\n          @click=\"updMerch(index)\"\n        />\n      </div>\n      <div class=\"merch__settings\">\n        <div class=\"merch__property\">\n          <label class=\"merch__label\" for=\"top\">{{ $t('create.top') }}</label>\n          <input type=\"text\" class=\"merch__text\" id=\"top\" v-model=\"topCanvasElement.text\" @input=\"draw()\" />\n        </div>\n        <div class=\"merch__property\">\n          <label class=\"merch__label\" for=\"bottom\">{{ $t('create.bottom') }}</label>\n          <input type=\"text\" class=\"merch__text\" id=\"bottom\" v-model=\"bottomCanvasElement.text\" @input=\"draw()\" />\n        </div>\n      </div>\n      <div class=\"merch__settings-row\">\n        <div class=\"merch__settings\">\n          <div class=\"merch__property\">\n            <label class=\"merch__label\" for=\"color\">{{ $t('merch.color') }}</label>\n            <input type=\"color\" id=\"color\" class=\"merch__color\" v-model=\"canvasProps.textColor\" @input=\"draw()\" />\n          </div>\n          <div class=\"merch__property\">\n            <label class=\"merch__label\" for=\"backgroundColor\">{{ $t('merch.backgroundColor') }}</label>\n            <input type=\"color\" id=\"itemColor\" class=\"merch__color\" v-model=\"canvasProps.itemColor\" @input=\"draw()\" />\n          </div>\n        </div>\n      </div>\n\n      <div class=\"merch__canvas-wrapper\">\n        <div class=\"merch__control-buttons\">\n          <icon-btn\n            :text=\"$t('btn.download')\"\n            imgPath=\"icon\"\n            className=\"btn btn-icon icon-download\"\n            @click=\"saveImage\"\n          ></icon-btn>\n          <div class=\"merch__control-buttons-scale\">\n            <icon-btn\n              :text=\"$t('btn.scaleUp')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-plus\"\n              @click=\"scaleUp()\"\n            ></icon-btn>\n            <icon-btn\n              :text=\"$t('btn.trueSize')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-true\"\n              @click=\"scaleTrue()\"\n            ></icon-btn>\n            <icon-btn\n              :text=\"$t('btn.center')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-center\"\n              @click=\"centering\"\n            ></icon-btn>\n            <icon-btn\n              :text=\"$t('btn.scaleDown')\"\n              imgPath=\"icon\"\n              className=\"btn btn-icon icon-minus\"\n              @click=\"scaleDown()\"\n            ></icon-btn>\n          </div>\n          <icon-btn\n            :text=\"$t('btn.copy')\"\n            imgPath=\"icon\"\n            className=\"btn btn-icon icon-copy\"\n            @click=\"copyImage\"\n          ></icon-btn>\n        </div>\n        <canvas\n          class=\"merch__canvas\"\n          ref=\"canvas\"\n          @mousemove=\"handleMouseMove\"\n          @mousedown=\"handleMouseDown\"\n          @mouseup=\"handleMouseUp\"\n          @mouseout=\"handleMouseOut\"\n          @wheel=\"handleWheel\"\n        >\n        </canvas>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport IconBtn from '@/components/buttons/IconBtn.vue';\nimport usePagesStore from '@/stores/pages-store';\nimport useCleanedStore from '@/stores/cleaned';\nimport type { CanvasElement } from '@/common/types';\nimport * as CanvasUtils from '@/utils/canvas-utils';\n\nconst { cleanedFilelist, originalFilelist } = useCleanedStore();\nconst { getPageMerchState, setPageMerchState } = usePagesStore();\n\ntype ImageItem = {\n  id: number;\n  path: string;\n};\n\nexport default defineComponent({\n  name: 'MerchView',\n\n  components: {\n    IconBtn,\n  },\n\n  data() {\n    return {\n      images: [] as ImageItem[],\n      indexMeme: 0,\n      merch: [] as ImageItem[],\n      indexMerch: 0,\n      canvas: {} as HTMLCanvasElement,\n      canvasProps: CanvasUtils.initProperties(0.5, 1, 1.5),\n      ctx: {} as CanvasRenderingContext2D,\n      imgMeme: {} as HTMLImageElement,\n      imgMerch: {} as HTMLImageElement,\n      imgCanvasElement: CanvasUtils.initElement(0, 0, 0.5, 0.1, 0.5, 2, false),\n      topCanvasElement: CanvasUtils.initElement(0, 0, 0.5, 0.1, 0.5, 2),\n      bottomCanvasElement: CanvasUtils.initElement(0, 0, 0.5, 0.1, 0.5, 2),\n      layers: [] as CanvasElement[],\n      currItems: 'cleaned',\n    };\n  },\n\n  beforeMount() {\n    this.getImages();\n  },\n\n  async mounted() {\n    const loaded = this.loadStore();\n\n    const { canvas } = this.$refs;\n    if (!(canvas instanceof HTMLCanvasElement)) return;\n\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n\n    this.canvas = canvas;\n    this.ctx = ctx;\n\n    const imageMerch = CanvasUtils.loadImage(this.merch[this.indexMerch].path);\n    const imageMeme = CanvasUtils.loadImage(this.images[this.indexMeme].path);\n\n    [this.imgMerch, this.imgMeme] = await Promise.all([imageMerch, imageMeme]);\n\n    CanvasUtils.calcCanvasSizes(this.canvas, this.canvasProps.scaleSteps);\n    CanvasUtils.calcElementsSizes(\n      this.imgMeme,\n      this.imgCanvasElement,\n      this.topCanvasElement,\n      this.bottomCanvasElement,\n      this.canvasProps.scaleSteps,\n    );\n    if (!loaded) this.centering();\n\n    // order of layers, index is z-index\n    this.layers[0] = this.imgCanvasElement;\n    this.layers[1] = this.topCanvasElement;\n    this.layers[2] = this.bottomCanvasElement;\n\n    this.draw();\n  },\n\n  beforeRouteLeave() {\n    setPageMerchState(JSON.stringify(this.$data));\n  },\n\n  computed: {\n    cursorPointer() {\n      return CanvasUtils.getCursor(this.layers);\n    },\n  },\n\n  methods: {\n    getImages() {\n      this.images = this.mappingImages(this.currItems === 'cleaned' ? cleanedFilelist : originalFilelist);\n      this.merch = this.mappingImages([\n        './img/merch/tshirt.png',\n        './img/merch/hoodie.png',\n        './img/merch/mug.png',\n        './img/merch/thermo.png',\n      ]);\n    },\n\n    changeItems() {\n      this.images = this.mappingImages(this.currItems !== 'cleaned' ? cleanedFilelist : originalFilelist);\n      setTimeout(() => {\n        this.updImage(this.indexMeme);\n        this.currItems = this.currItems === 'cleaned' ? 'original' : 'cleaned';\n      }, 100);\n    },\n\n    mappingImages(stringArray: string[]): ImageItem[] {\n      return stringArray.map((path, id) => {\n        return { path, id };\n      });\n    },\n\n    getImg(i: number): string {\n      return this.images[i].path;\n    },\n\n    getMerch(i: number): string {\n      return this.merch[i].path;\n    },\n\n    scaleUp() {\n      CanvasUtils.scaleUpCanvas(this.canvasProps);\n\n      this.draw();\n    },\n\n    scaleTrue() {\n      CanvasUtils.scaleTrueCanvas(this.canvasProps, this.layers);\n\n      this.draw();\n    },\n\n    scaleDown() {\n      CanvasUtils.scaleDownCanvas(this.canvasProps);\n\n      this.draw();\n    },\n\n    centering() {\n      CanvasUtils.centeringElements(\n        this.imgCanvasElement,\n        this.topCanvasElement,\n        this.bottomCanvasElement,\n        this.canvasProps.scaleSteps,\n        true,\n      );\n\n      this.draw();\n    },\n\n    draw() {\n      CanvasUtils.calcCanvasSizes(this.canvas, this.canvasProps.scaleSteps);\n      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n      CanvasUtils.drawMerchImage(this.canvas, this.ctx, this.imgMerch, this.canvasProps.itemColor);\n\n      CanvasUtils.calcElementsSizes(\n        this.imgMeme,\n        this.imgCanvasElement,\n        this.topCanvasElement,\n        this.bottomCanvasElement,\n        this.canvasProps.scaleSteps,\n      );\n\n      CanvasUtils.calcElementsPosition(this.layers, this.canvasProps.scaleSteps);\n\n      this.ctx.drawImage(\n        this.imgMeme,\n        0,\n        0,\n        this.imgCanvasElement.width,\n        this.imgCanvasElement.height,\n        this.imgCanvasElement.scaledLeft,\n        this.imgCanvasElement.scaledTop,\n        this.imgCanvasElement.scaledWidth,\n        this.imgCanvasElement.scaledHeight,\n      );\n\n      const { scaleSteps } = this.canvasProps;\n      const { height } = this.canvas;\n      CanvasUtils.drawTextUp(height, this.ctx, this.canvasProps, this.topCanvasElement, scaleSteps);\n      CanvasUtils.drawTextDown(height, this.ctx, this.canvasProps, this.bottomCanvasElement, scaleSteps);\n\n      const color = CanvasUtils.invertHex(this.canvasProps.itemColor);\n      this.layers.forEach(el => CanvasUtils.drawBorder(el, this.ctx, color));\n    },\n\n    handleMouseMove(e: MouseEvent) {\n      CanvasUtils.moveElements(e, this.canvas, this.layers, this.canvasProps.scaleSteps);\n\n      this.draw();\n    },\n\n    handleMouseDown(e: MouseEvent) {\n      CanvasUtils.selectElements(e, this.canvas, this.layers);\n\n      this.draw();\n    },\n\n    handleMouseUp() {\n      CanvasUtils.deselectElements(this.layers);\n\n      this.draw();\n    },\n\n    handleMouseOut() {\n      CanvasUtils.unhoverElements(this.layers);\n\n      this.handleMouseUp();\n    },\n\n    handleWheel(e: WheelEvent) {\n      CanvasUtils.scalingElements(e, this.layers, this.canvasProps);\n\n      this.draw();\n    },\n\n    updMerch(i: number) {\n      this.indexMerch = i;\n\n      const { merch } = this.$refs;\n      if (!(merch instanceof Array)) return;\n\n      const image = merch[this.indexMerch];\n      if (!(image instanceof HTMLImageElement)) return;\n\n      this.imgMerch = image;\n\n      this.draw();\n    },\n\n    updImage(i: number) {\n      this.indexMeme = i;\n\n      const { imgs } = this.$refs;\n      if (!(imgs instanceof Array)) return;\n\n      const image = imgs[this.indexMeme];\n      if (!(image instanceof HTMLImageElement)) return;\n\n      this.imgMeme = image;\n      CanvasUtils.calcElementsSizes(\n        this.imgMeme,\n        this.imgCanvasElement,\n        this.topCanvasElement,\n        this.bottomCanvasElement,\n        this.canvasProps.scaleSteps,\n      );\n      this.centering();\n\n      this.draw();\n    },\n\n    calcsScaleSteps(tempCanvas: HTMLCanvasElement, tempctx: CanvasRenderingContext2D) {\n      tempctx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);\n      const minLeft =\n        Math.min.apply(\n          null,\n          this.layers.map(el => el.scaledLeft),\n        ) / this.canvasProps.scaleSteps;\n      const maxRight =\n        Math.max.apply(\n          null,\n          this.layers.map(el => el.scaledLeft + el.scaledWidth),\n        ) / this.canvasProps.scaleSteps;\n      const maxWidth = maxRight - minLeft;\n\n      const minTop =\n        Math.min.apply(\n          null,\n          this.layers.map(el => el.scaledTop),\n        ) / this.canvasProps.scaleSteps;\n      const maxBottom =\n        Math.max.apply(\n          null,\n          this.layers.map(el => el.scaledTop + el.scaledHeight),\n        ) / this.canvasProps.scaleSteps;\n      const maxHeight = maxBottom - minTop;\n\n      return {\n        minLeft,\n        minTop,\n        scaleSteps: Math.min(tempCanvas.width / maxWidth, tempCanvas.height / maxHeight),\n      };\n    },\n\n    prepareForSave(tempCanvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D) {\n      const tempctx = ctx;\n      tempctx.clearRect(0, 0, tempCanvas.width, tempCanvas.height);\n      const { minLeft, minTop, scaleSteps } = this.calcsScaleSteps(tempCanvas, tempctx);\n\n      const imgCanvasElement = { ...this.imgCanvasElement };\n      const topCanvasElement = { ...this.topCanvasElement };\n      const bottomCanvasElement = { ...this.bottomCanvasElement };\n      const layers = [imgCanvasElement, topCanvasElement, bottomCanvasElement];\n\n      layers.forEach(el => {\n        const canvasElement = el;\n        canvasElement.left -= minLeft;\n        canvasElement.top -= minTop;\n        canvasElement.bottom -= minTop;\n      });\n\n      CanvasUtils.calcElementsSizes(this.imgMeme, imgCanvasElement, topCanvasElement, bottomCanvasElement, scaleSteps);\n      CanvasUtils.calcElementsPosition(layers, scaleSteps);\n\n      tempctx.drawImage(\n        this.imgMeme,\n        0,\n        0,\n        imgCanvasElement.width,\n        imgCanvasElement.height,\n        imgCanvasElement.scaledLeft,\n        imgCanvasElement.scaledTop,\n        imgCanvasElement.scaledWidth,\n        imgCanvasElement.scaledHeight,\n      );\n\n      const height = (this.canvas.height / this.canvasProps.scaleSteps) * scaleSteps;\n      CanvasUtils.drawTextUp(height, tempctx, this.canvasProps, topCanvasElement, scaleSteps);\n      CanvasUtils.drawTextDown(height, tempctx, this.canvasProps, bottomCanvasElement, scaleSteps);\n    },\n\n    saveImage() {\n      const tempCanvas = document.createElement('canvas');\n      const tempctx = tempCanvas.getContext('2d');\n      if (!tempctx) return;\n\n      tempCanvas.width = 1240;\n      tempCanvas.height = 1754;\n      this.prepareForSave(tempCanvas, tempctx);\n\n      tempCanvas.toDataURL();\n      const link = document.createElement('a');\n      link.download = 'download.png';\n      link.href = tempCanvas.toDataURL();\n      link.click();\n    },\n\n    copyImage() {\n      this.canvas.toBlob(blob => {\n        const type = blob?.type;\n        if (!type) return;\n        const item = new ClipboardItem({ [type]: blob });\n        navigator.clipboard.write([item]);\n      });\n    },\n\n    loadStore(): boolean {\n      const str = getPageMerchState();\n      if (!str) return false;\n\n      const data = JSON.parse(str);\n      if (!data) return false;\n\n      Object.assign(this.$data, data);\n      return true;\n    },\n  },\n});\n</script>\n<style scoped>\n.merch,\n.merch__generator {\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  gap: var(--gap);\n  color: var(--color-text);\n}\n\n.merch {\n  padding-left: 3rem;\n  height: 100%;\n}\n\n.merch__generator {\n  flex-direction: column;\n  align-items: center;\n  height: 100%;\n  padding-right: 3rem;\n  overflow-y: auto;\n}\n\n.merch__list {\n  height: 100%;\n  overflow-y: auto;\n}\n\n.merch__option {\n  margin: 0 auto;\n}\n\n.merch__change {\n  width: 24rem;\n  height: 16.7rem;\n  transition: 0.5s ease;\n}\n\n.merch__change_cleaned {\n  background: no-repeat center center / contain url('/img/merch/merch-cleaned.svg');\n}\n\n.merch__change_original {\n  background: no-repeat center center / contain url('/img/merch/merch-original.svg');\n}\n\n.merch__images,\n.merch__merch {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  align-items: flex-start;\n  gap: var(--gap);\n}\n\n.merch__image {\n  width: 14rem;\n  height: 14rem;\n  object-fit: contain;\n}\n\n.merch__settings,\n.merch__settings-row {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.merch__settings {\n  flex-direction: column;\n  gap: 0.5rem;\n}\n\n.merch__settings-row {\n  gap: var(--gap);\n}\n\n.merch__property {\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--gap);\n}\n\n.merch__text,\n.merch__number {\n  padding: 0.5rem;\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  background-color: var(--color-background);\n  color: inherit;\n  border-radius: 1rem;\n  transition: 0.5s ease;\n}\n\n.merch__text {\n  width: 30rem;\n}\n\n.merch__color {\n  padding: 0 0;\n  border: none;\n  background: none;\n  width: 6.2rem;\n  height: 3.2rem;\n  transition: 0.5s ease;\n}\n\n.merch__color::-webkit-color-swatch-wrapper {\n  padding: 0;\n}\n\n.merch__color::-webkit-color-swatch {\n  border: 0.2rem solid var(--color-border-inverse-soft);\n  border-radius: 1rem;\n}\n\n.merch__canvas-wrapper {\n  position: relative;\n  padding-top: 2rem;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\n.merch__control-buttons {\n  z-index: 10;\n  display: flex;\n  justify-content: center;\n  gap: 1rem;\n}\n\n.merch__control-buttons-scale button {\n  margin: 0 0.25rem;\n  padding: 0;\n}\n\n.merch__canvas {\n  border: 0.2px solid gray;\n}\n.merch__canvas:hover {\n  cursor: v-bind(cursorPointer);\n}\n\n@media (max-width: 1200px) {\n  .merch {\n    padding-left: 1.5rem;\n  }\n\n  .merch__generator {\n    padding-right: 1.5rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "tools/sloths/tsconfig.config.json",
    "content": "{\n  \"extends\": \"./node_modules/@vue/tsconfig/tsconfig.node.json\",\n  \"include\": [\"vite.config.*\", \"vitest.config.*\", \"cypress.config.*\"],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "tools/sloths/tsconfig.json",
    "content": "{\n  \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n  \"include\": [\"env.d.ts\", \"src/**/*\", \"src/**/*.vue\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"target\": \"es2020\",\n    \"module\": \"es2020\",\n    \"lib\": [\"es2019\", \"dom\"],\n    \"moduleResolution\": \"node\",\n    \"outDir\": \"./build/\",\n    \"esModuleInterop\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"strictNullChecks\": true,\n    \"skipLibCheck\": true\n  },\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.config.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tools/sloths/vite.config.ts",
    "content": "import { fileURLToPath, URL } from 'node:url';\nimport path from 'node:path';\n\nimport { defineConfig } from 'vite';\nimport vue from '@vitejs/plugin-vue';\nimport vueI18n from '@intlify/vite-plugin-vue-i18n';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [\n    vue(),\n    vueI18n({\n      include: path.resolve(__dirname, './src/locales/**'),\n    }),\n  ],\n  resolve: {\n    alias: {\n      '@': fileURLToPath(new URL('./src', import.meta.url)),\n    },\n  },\n  base: '',\n  build: {\n    commonjsOptions: {\n      esmExternals: true,\n    },\n  },\n});\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"noUpdateNotifier\": true,\n  \"globalDependencies\": [\"**/.env\"],\n  \"tasks\": {\n    \"build\": {\n      \"outputs\": [\".next/**\", \"!.next/cache/**\", \"dist/**\"],\n      \"env\": [\n        \"ACTIVITY_WEBHOOK_SECRET\",\n        \"APP_VERSION\",\n        \"AWS_LAMBDA\",\n        \"BUILD_VERSION\",\n        \"CDN_HOST\",\n        \"CONSENT_SECRET\",\n        \"NESTJS_HOST\",\n        \"NODE_ENV\",\n        \"NODE_PORT\",\n        \"NODE_TLS_REJECT_UNAUTHORIZED\",\n        \"RS_ENV\",\n        \"RS_HOST\",\n        \"RSSCHOOL_ADMIN_TEAMS\",\n        \"RSSHCOOL_API_ADMIN_PASSWORD\",\n        \"RSSHCOOL_API_ADMIN_USERNAME\",\n        \"RSSHCOOL_API_AUTH_CALLBACK\",\n        \"RSSHCOOL_API_AUTH_CLIENT_ID\",\n        \"RSSHCOOL_API_AUTH_CLIENT_SECRET\",\n        \"RSSHCOOL_API_AUTH_SUCCESS_REDIRECT\",\n        \"RSSHCOOL_API_AWS_ACCESS_KEY_ID\",\n        \"RSSHCOOL_API_AWS_REGION\",\n        \"RSSHCOOL_API_AWS_REST_API_KEY\",\n        \"RSSHCOOL_API_AWS_REST_API_URL\",\n        \"RSSHCOOL_API_AWS_SECRET_ACCESS_KEY\",\n        \"RSSHCOOL_API_GITHUB_APP_ID\",\n        \"RSSHCOOL_API_GITHUB_APP_INSTALL_ID\",\n        \"RSSHCOOL_API_GITHUB_HOOKS_SECRET\",\n        \"RSSHCOOL_API_GITHUB_PRIVATE_KEY\",\n        \"RSSHCOOL_API_SESSION_KEY\",\n        \"RSSHCOOL_API_USERS_CLOUD_PASSWORD\",\n        \"RSSHCOOL_API_USERS_CLOUD_USERNAME\",\n        \"RSSHCOOL_AWS_ACCESS_KEY_ID\",\n        \"RSSHCOOL_AWS_REGION\",\n        \"RSSHCOOL_AWS_REST_API_KEY\",\n        \"RSSHCOOL_AWS_REST_API_URL\",\n        \"RSSHCOOL_AWS_SECRET_ACCESS_KEY\",\n        \"RSSHCOOL_HOST\",\n        \"RSSHCOOL_OPENAI_API_KEY\",\n        \"RSSHCOOL_PG_DATABASE\",\n        \"RSSHCOOL_PG_HOST\",\n        \"RSSHCOOL_PG_PASSWORD\",\n        \"RSSHCOOL_PG_USERNAME\",\n        \"RSSHCOOL_UI_GCP_MAPS_API_KEY\",\n        \"RSSHCOOL_USERS_ADMINS\",\n        \"RSSHCOOL_USERS_HIRERS\",\n        \"SENTRY_DSN\",\n        \"SERVER_HOST\",\n        \"RSSCHOOL_DEV_TOOLS\"\n      ]\n    },\n    \"lint\": {\n      \"env\": [\"AGENT\", \"OPENCODE\", \"CLAUDECODE\", \"CURSOR_AGENT\", \"CODEX_THREAD_ID\", \"NO_COLOR\"]\n    },\n    \"test\": {},\n    \"test:ci\": {},\n    \"start\": {\n      \"cache\": false,\n      \"persistent\": true\n    },\n    \"compile\": {},\n    \"openapi\": {}\n  }\n}\n"
  },
  {
    "path": "vitest.shared.mts",
    "content": "import path from 'node:path';\nimport { defineConfig } from 'vitest/config';\n\n/**\n * Shared Vitest base configuration for all workspaces.\n * Each workspace extends this via `mergeConfig`.\n */\nexport default defineConfig({\n  resolve: {\n    alias: {\n      '@common': path.resolve(import.meta.dirname, 'common'),\n    },\n  },\n  test: {\n    globals: true,\n    clearMocks: true,\n  },\n});\n"
  }
]