diff --git a/api-dto/coding/external-coding-import-result.dto.ts b/api-dto/coding/external-coding-import-result.dto.ts new file mode 100644 index 000000000..7d1961d21 --- /dev/null +++ b/api-dto/coding/external-coding-import-result.dto.ts @@ -0,0 +1,20 @@ +export interface ExternalCodingImportResultDto { + message: string; + processedRows: number; + updatedRows: number; + errors: string[]; + affectedRows: Array<{ + unitAlias: string; + variableId: string; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; + originalCodedStatus: string; + originalCode: number | null; + originalScore: number | null; + updatedCodedStatus: string | null; + updatedCode: number | null; + updatedScore: number | null; + }>; +} diff --git a/api-dto/coding/external-coding-import.dto.ts b/api-dto/coding/external-coding-import.dto.ts new file mode 100644 index 000000000..bf248d2b1 --- /dev/null +++ b/api-dto/coding/external-coding-import.dto.ts @@ -0,0 +1,20 @@ +import { IsString, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class ExternalCodingImportDto { + @ApiProperty({ + description: 'Base64 encoded file data (CSV or Excel)', + type: 'string' + }) + @IsString() + file: string; + + @ApiProperty({ + description: 'Optional filename', + type: 'string', + required: false + }) + @IsOptional() + @IsString() + fileName?: string; +} diff --git a/apps/backend/src/app/admin/admin.module.ts b/apps/backend/src/app/admin/admin.module.ts index cf7050eaf..9d7811c3c 100755 --- a/apps/backend/src/app/admin/admin.module.ts +++ b/apps/backend/src/app/admin/admin.module.ts @@ -28,6 +28,9 @@ import FileUpload from '../database/entities/file_upload.entity'; import { ReplayStatisticsController } from './replay-statistics/replay-statistics.controller'; import { VariableBundleModule } from './variable-bundle/variable-bundle.module'; import { VariableBundleController } from './variable-bundle/variable-bundle.controller'; +import { CodingJobsController } from './coding-jobs/coding-jobs.controller'; +import { DatabaseAdminController } from './database/database-admin.controller'; +import { DatabaseExportService } from './database/database-export.service'; @Module({ imports: [ @@ -58,11 +61,14 @@ import { VariableBundleController } from './variable-bundle/variable-bundle.cont UnitInfoController, MissingsProfilesController, ReplayStatisticsController, - VariableBundleController + VariableBundleController, + CodingJobsController, + DatabaseAdminController ], providers: [ BookletInfoService, - UnitInfoService + UnitInfoService, + DatabaseExportService ] }) export class AdminModule {} diff --git a/apps/backend/src/app/admin/code-book/codebook-generator.class.ts b/apps/backend/src/app/admin/code-book/codebook-generator.class.ts index 707c0d35b..faa50090f 100644 --- a/apps/backend/src/app/admin/code-book/codebook-generator.class.ts +++ b/apps/backend/src/app/admin/code-book/codebook-generator.class.ts @@ -1,6 +1,8 @@ import { - ToTextFactory, CodeAsText, CodingScheme, VariableCodingData, CodeData + ToTextFactory, CodeAsText } from '@iqb/responses'; +import { VariableCodingData, CodeData, CodingScheme } from '@iqbspecs/coding-scheme/coding-scheme.interface'; + import { BookVariable, CodeBookContentSetting, diff --git a/apps/backend/src/app/admin/code-book/codebook.interfaces.ts b/apps/backend/src/app/admin/code-book/codebook.interfaces.ts index db21c931d..a80e20146 100644 --- a/apps/backend/src/app/admin/code-book/codebook.interfaces.ts +++ b/apps/backend/src/app/admin/code-book/codebook.interfaces.ts @@ -1,4 +1,4 @@ -import { VariableInfo } from '@iqb/responses'; +import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; /** * Item metadata for codebook diff --git a/apps/backend/src/app/admin/coding-job/coding-job.controller.ts b/apps/backend/src/app/admin/coding-job/coding-job.controller.ts index 6a08bacf4..a599db461 100644 --- a/apps/backend/src/app/admin/coding-job/coding-job.controller.ts +++ b/apps/backend/src/app/admin/coding-job/coding-job.controller.ts @@ -342,4 +342,88 @@ export class CodingJobController { throw new BadRequestException(`Failed to assign coders: ${error.message}`); } } + + @Get('/coder/:coderId') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Get coding jobs by coder', + description: 'Gets all coding jobs assigned to a specific coder' + }) + @ApiParam({ + name: 'coderId', + type: Number, + required: true, + description: 'The ID of the coder' + }) + @ApiOkResponse({ + description: 'The coding jobs assigned to the coder.', + schema: { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/CodingJobDto' } + } + } + } + }) + async getCodingJobsByCoder( + @Param('coderId', ParseIntPipe) coderId: number + ): Promise<{ data: CodingJobDto[] }> { + try { + const codingJobs = await this.codingJobService.getCodingJobsByCoder(coderId); + return { + data: codingJobs.map(job => CodingJobDto.fromEntity(job)) + }; + } catch (error) { + throw new BadRequestException(`Failed to get coding jobs for coder: ${error.message}`); + } + } + + @Get(':jobId/coders') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Get coders by job ID', + description: 'Gets all coders assigned to a specific coding job' + }) + @ApiParam({ + name: 'jobId', + type: Number, + required: true, + description: 'The ID of the coding job' + }) + @ApiOkResponse({ + description: 'The coders assigned to the coding job.', + schema: { + type: 'object', + properties: { + data: { + type: 'array', + items: { + type: 'object', + properties: { + userId: { type: 'number' } + } + } + }, + total: { type: 'number' } + } + } + }) + async getCodersByJobId( + @Param('jobId', ParseIntPipe) jobId: number + ): Promise<{ data: { userId: number }[], total: number }> { + try { + const coderIds = await this.codingJobService.getCodersByJobId(jobId); + const data = coderIds.map(userId => ({ userId })); + return { + data, + total: data.length + }; + } catch (error) { + throw new BadRequestException(`Failed to get coders for job: ${error.message}`); + } + } } diff --git a/apps/backend/src/app/admin/coding-job/dto/coding-job.dto.ts b/apps/backend/src/app/admin/coding-job/dto/coding-job.dto.ts index 3d6f48026..20fda0114 100644 --- a/apps/backend/src/app/admin/coding-job/dto/coding-job.dto.ts +++ b/apps/backend/src/app/admin/coding-job/dto/coding-job.dto.ts @@ -60,20 +60,28 @@ export class CodingJobDto { assigned_coders?: number[]; @ApiProperty({ - description: 'Variable IDs assigned to the coding job', - type: [String], - example: ['var1', 'var2', 'var3'], + description: 'Variables assigned to the coding job with unit and variable IDs', + type: [Object], + example: [{ unitName: 'Unit1', variableId: 'var1' }, { unitName: 'Unit2', variableId: 'var2' }], required: false }) - assigned_variables?: string[]; + assigned_variables?: { unitName: string; variableId: string }[]; @ApiProperty({ - description: 'Variable bundle names assigned to the coding job', - type: [String], - example: ['Bundle A', 'Bundle B'], + description: 'Variable bundles assigned to the coding job with their variables', + type: [Object], + example: [ + { + name: 'Bundle A', + variables: [ + { unitName: 'Unit1', variableId: 'var1' }, + { unitName: 'Unit2', variableId: 'var2' } + ] + } + ], required: false }) - assigned_variable_bundles?: string[]; + assigned_variable_bundles?: { name: string; variables: { unitName: string; variableId: string }[] }[]; @ApiProperty({ description: 'Variables assigned to the coding job', @@ -93,15 +101,15 @@ export class CodingJobDto { * Create a CodingJobDto from a CodingJob entity * @param entity The CodingJob entity * @param assignedCoders Optional array of assigned coder IDs - * @param assignedVariables Optional array of assigned variable IDs - * @param assignedVariableBundles Optional array of assigned variable bundle names + * @param assignedVariables Optional array of assigned variable objects with unit and variable IDs + * @param assignedVariableBundles Optional array of assigned variable bundle objects with name and variables * @returns A CodingJobDto */ static fromEntity( entity: CodingJob, assignedCoders?: number[], - assignedVariables?: string[], - assignedVariableBundles?: string[] + assignedVariables?: { unitName: string; variableId: string }[], + assignedVariableBundles?: { name: string; variables: { unitName: string; variableId: string }[] }[] ): CodingJobDto { const dto = new CodingJobDto(); dto.id = entity.id; diff --git a/apps/backend/src/app/admin/coding-jobs/coding-jobs.controller.ts b/apps/backend/src/app/admin/coding-jobs/coding-jobs.controller.ts new file mode 100644 index 000000000..b0d5bced6 --- /dev/null +++ b/apps/backend/src/app/admin/coding-jobs/coding-jobs.controller.ts @@ -0,0 +1,215 @@ +import { + BadRequestException, + Controller, + Get, + NotFoundException, + Param, + ParseIntPipe, + UseGuards +} from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiTags +} from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; +import { CodingJobService } from '../../database/services/coding-job.service'; +import { CodingJobDto } from '../coding-job/dto/coding-job.dto'; + +@ApiTags('Admin Coding Jobs (Direct)') +@Controller('admin/coding-jobs') +export class CodingJobsController { + constructor(private readonly codingJobService: CodingJobService) {} + + @Get(':id') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Get coding job by ID', + description: 'Retrieves a specific coding job by its ID' + }) + @ApiParam({ + name: 'id', + type: Number, + required: true, + description: 'The ID of the coding job' + }) + @ApiOkResponse({ + description: 'The coding job has been successfully retrieved.', + type: CodingJobDto + }) + @ApiNotFoundResponse({ + description: 'Coding job not found.' + }) + @ApiBadRequestResponse({ + description: 'Failed to retrieve coding job.' + }) + async getCodingJobById( + @Param('id', ParseIntPipe) id: number + ): Promise { + try { + return await this.codingJobService.getCodingJobById(id); + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + throw new BadRequestException(`Failed to retrieve coding job: ${error.message}`); + } + } + + @Get(':coderId/coders') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Get coders by job ID', + description: 'Gets all coders assigned to a specific coding job' + }) + @ApiParam({ + name: 'jobId', + type: Number, + required: true, + description: 'The ID of the coding job' + }) + @ApiOkResponse({ + description: 'The coders assigned to the coding job.', + schema: { + type: 'object', + properties: { + data: { + type: 'array', + items: { + type: 'object', + properties: { + userId: { type: 'number' } + } + } + }, + total: { type: 'number' } + } + } + }) + @ApiBadRequestResponse({ + description: 'Failed to get coders for job.' + }) + @Get('/coder/:coderId') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Get coding jobs by coder', + description: 'Gets all coding jobs assigned to a specific coder' + }) + @ApiParam({ + name: 'coderId', + type: Number, + required: true, + description: 'The ID of the coder' + }) + @ApiOkResponse({ + description: 'The coding jobs assigned to the coder.', + schema: { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/CodingJobDto' } + } + } + } + }) + async getCodingJobsByCoder( + @Param('coderId', ParseIntPipe) coderId: number + ): Promise<{ data: CodingJobDto[] }> { + try { + const codingJobs = await this.codingJobService.getCodingJobsByCoder(coderId); + return { + data: codingJobs.map(job => CodingJobDto.fromEntity(job)) + }; + } catch (error) { + throw new BadRequestException(`Failed to get coding jobs for coder: ${error.message}`); + } + } + + @Get(':id/responses') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Get responses for coding job', + description: 'Gets all responses that match the variable ids in unit names for a specific coding job' + }) + @ApiParam({ + name: 'id', + type: Number, + required: true, + description: 'The ID of the coding job' + }) + @ApiOkResponse({ + description: 'The responses for the coding job.', + schema: { + type: 'object', + properties: { + data: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'number' }, + unitid: { type: 'number' }, + variableid: { type: 'string' }, + status: { type: 'string' }, + value: { type: 'string' }, + subform: { type: 'string' }, + code_v1: { type: 'number' }, + score_v1: { type: 'number' }, + status_v1: { type: 'string' }, + unit: { + type: 'object', + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + alias: { type: 'string' } + } + } + } + } + } + } + } + }) + @ApiNotFoundResponse({ + description: 'Coding job not found.' + }) + @ApiBadRequestResponse({ + description: 'Failed to get responses for coding job.' + }) + async getResponsesForCodingJob( + @Param('id', ParseIntPipe) id: number + ): Promise<{ data: { + id: number; + unitid: number; + variableid: string; + status: string; + value: string; + subform: string; + code_v1: number; + score_v1: number; + status_v1: string; + unit: { + id: number; + name: string; + alias: string; + }; + }[] }> { + try { + const responses = await this.codingJobService.getResponsesForCodingJob(id); + return { + data: responses + }; + } catch (error) { + throw new BadRequestException(`Failed to get responses for coding job: ${error.message}`); + } + } +} diff --git a/apps/backend/src/app/admin/database/database-admin.controller.ts b/apps/backend/src/app/admin/database/database-admin.controller.ts new file mode 100644 index 000000000..a40c8ca9d --- /dev/null +++ b/apps/backend/src/app/admin/database/database-admin.controller.ts @@ -0,0 +1,53 @@ +import { + Controller, + Get, + Header, + Res, + UseGuards, + InternalServerErrorException +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiResponse, + ApiTags +} from '@nestjs/swagger'; +import { Response } from 'express'; +import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; +import { AdminGuard } from '../admin.guard'; +import { DatabaseExportService } from './database-export.service'; + +@Controller('admin/database') +@ApiTags('admin') +export class DatabaseAdminController { + constructor(private readonly databaseExportService: DatabaseExportService) {} + + @Get('export/sqlite') + @ApiOperation({ + summary: 'Export database to SQLite', + description: 'Exports the PostgreSQL database to SQLite format with streaming support for large files' + }) + @ApiResponse({ + status: 200, + description: 'SQLite database file downloaded successfully', + content: { + 'application/x-sqlite3': { + schema: { + type: 'string', + format: 'binary' + } + } + } + }) + @Header('Content-Type', 'application/x-sqlite3') + @Header('Content-Disposition', 'attachment; filename=database-export.sqlite') + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, AdminGuard) + async exportDatabaseToSqlite(@Res() response: Response): Promise { + try { + await this.databaseExportService.exportToSqliteStream(response); + } catch (error) { + throw new InternalServerErrorException('Failed to export database to SQLite'); + } + } +} diff --git a/apps/backend/src/app/admin/database/database-export.service.ts b/apps/backend/src/app/admin/database/database-export.service.ts new file mode 100644 index 000000000..b1978fce6 --- /dev/null +++ b/apps/backend/src/app/admin/database/database-export.service.ts @@ -0,0 +1,497 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; +import { Response } from 'express'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as sqlite3 from 'sqlite3'; +import { promisify } from 'util'; + +@Injectable() +export class DatabaseExportService { + private readonly logger = new Logger(DatabaseExportService.name); + + constructor( + @InjectDataSource() private readonly dataSource: DataSource + ) {} + + async exportToSqliteStream(response: Response): Promise { + const tempDir = path.join(process.cwd(), 'temp'); + const tempFile = path.join(tempDir, `export_${Date.now()}.sqlite`); + + try { + // Ensure temp directory exists + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + // Create SQLite database + const db = new sqlite3.Database(tempFile); + const dbRun = promisify(db.run.bind(db)); + const dbClose = promisify(db.close.bind(db)); + + // Get all table names from PostgreSQL + const tables = await this.dataSource.query(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY table_name + `); + + // Set SQLite to handle large operations better + await dbRun('PRAGMA journal_mode=WAL'); + await dbRun('PRAGMA synchronous=NORMAL'); + await dbRun('PRAGMA cache_size=10000'); + await dbRun('PRAGMA temp_store=memory'); + + let processedTables = 0; + const totalTables = tables.length; + + for (const tableInfo of tables) { + const tableName = tableInfo.table_name; + + // Skip migration tables and other system tables + if (tableName.startsWith('typeorm_') || tableName === 'migrations') { + continue; + } + + try { + this.logger.log(`Processing table: ${tableName}`); + + // Get table structure from PostgreSQL + const columns = await this.dataSource.query(` + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = $1 AND table_schema = 'public' + ORDER BY ordinal_position + `, [tableName]); + + // Create table in SQLite + const columnDefs = columns.map(col => { + const colType = this.mapPostgresTypeToSqlite(col.data_type); + const nullable = col.is_nullable === 'YES' ? '' : ' NOT NULL'; + return `"${col.column_name}" ${colType}${nullable}`; + }).join(', '); + + await dbRun(`CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`); + + // Get row count to handle large tables + const countResult = await this.dataSource.query(`SELECT COUNT(*) as count FROM "${tableName}"`); + const totalRows = parseInt(countResult[0].count, 10); + + if (totalRows === 0) { + continue; + } + + // Process data in batches for memory efficiency + const batchSize = 1000; + let offset = 0; + + while (offset < totalRows) { + const rows = await this.dataSource.query(` + SELECT * FROM "${tableName}" + ORDER BY (SELECT NULL) + LIMIT $1 OFFSET $2 + `, [batchSize, offset]); + + if (rows.length === 0) break; + + // Prepare batch insert + const columnNames = columns.map(col => `"${col.column_name}"`).join(', '); + const placeholders = columns.map(() => '?').join(', '); + const insertSql = `INSERT INTO "${tableName}" (${columnNames}) VALUES (${placeholders})`; + + // Begin transaction for batch + await dbRun('BEGIN TRANSACTION'); + + try { + for (const row of rows) { + const values = columns.map(col => { + const value = row[col.column_name]; + // Handle special data types + if (value === null) return null; + if (typeof value === 'boolean') return value ? 1 : 0; + if (value instanceof Date) return value.toISOString(); + if (typeof value === 'object') return JSON.stringify(value); + return value; + }); + + await dbRun(insertSql, values); + } + + await dbRun('COMMIT'); + } catch (error) { + await dbRun('ROLLBACK'); + throw error; + } + + offset += batchSize; + + // Optional: Send progress info (this would require a different streaming approach) + this.logger.log(`Table ${tableName}: ${Math.min(offset, totalRows)}/${totalRows} rows processed`); + } + + processedTables += 1; + this.logger.log(`Completed table ${tableName} (${processedTables}/${totalTables})`); + } catch (error) { + this.logger.error(`Error processing table ${tableName}: ${error?.message || error}`, error?.stack); + // Continue with next table instead of failing completely + } + } + + // Close database connection + await dbClose(); + + this.logger.log('SQLite export completed, streaming file...'); + + // Stream the file to response + const fileStats = fs.statSync(tempFile); + response.setHeader('Content-Length', fileStats.size); + + // Create read stream and pipe to response + const fileStream = fs.createReadStream(tempFile); + + fileStream.on('error', error => { + this.logger.error(`Stream error: ${error?.message || error}`, error?.stack); + if (!response.headersSent) { + response.status(500).send('Error streaming file'); + } + }); + + fileStream.on('end', () => { + this.logger.log('File streaming completed'); + // Clean up temporary file + setTimeout(() => { + try { + fs.unlinkSync(tempFile); + this.logger.log('Temporary file cleaned up'); + } catch (error) { + this.logger.error(`Error cleaning up temporary file: ${error?.message || error}`, error?.stack); + } + }, 1000); + }); + + fileStream.pipe(response); + } catch (error) { + this.logger.error(`Export error: ${error?.message || error}`, error?.stack); + + // Clean up temporary file on error + try { + if (fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } + } catch (cleanupError) { + this.logger.error(`Error cleaning up temporary file: ${cleanupError?.message || cleanupError}`, cleanupError?.stack); + } + + throw error; + } + } + + async exportWorkspaceToSqliteStream(response: Response, workspaceId: number): Promise { + const tempDir = path.join(process.cwd(), 'temp'); + const tempFile = path.join(tempDir, `workspace_export_${workspaceId}_${Date.now()}.sqlite`); + + try { + // Ensure temp directory exists + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + // Create SQLite database + const db = new sqlite3.Database(tempFile); + const dbRun = promisify(db.run.bind(db)); + const dbClose = promisify(db.close.bind(db)); + + // Set SQLite to handle large operations better + await dbRun('PRAGMA journal_mode=WAL'); + await dbRun('PRAGMA synchronous=NORMAL'); + await dbRun('PRAGMA cache_size=10000'); + await dbRun('PRAGMA temp_store=memory'); + + // Define workspace-specific tables and their filtering queries + const workspaceTables = [ + { + name: 'persons', + query: 'SELECT * FROM persons WHERE workspace_id = $1' + }, + { + name: 'booklet', + query: ` + SELECT b.* FROM booklet b + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'bookletinfo', + query: ` + SELECT bi.* FROM bookletinfo bi + INNER JOIN booklet b ON bi.id = b.infoid + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'bookletlog', + query: ` + SELECT bl.* FROM bookletlog bl + INNER JOIN booklet b ON bl.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'session', + query: ` + SELECT s.* FROM session s + INNER JOIN booklet b ON s.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'unit', + query: ` + SELECT u.* FROM unit u + INNER JOIN booklet b ON u.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'unit_note', + query: ` + SELECT un.* FROM unit_note un + INNER JOIN unit u ON un."unitId" = u.id + INNER JOIN booklet b ON u.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'unit_tag', + query: ` + SELECT ut.* FROM unit_tag ut + INNER JOIN unit u ON ut."unitId" = u.id + INNER JOIN booklet b ON u.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'unitlaststate', + query: ` + SELECT uls.* FROM unitlaststate uls + INNER JOIN unit u ON uls.unitid = u.id + INNER JOIN booklet b ON u.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'unitlog', + query: ` + SELECT ul.* FROM unitlog ul + INNER JOIN unit u ON ul.unitid = u.id + INNER JOIN booklet b ON u.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'response', + query: ` + SELECT r.* FROM response r + INNER JOIN unit u ON r.unitid = u.id + INNER JOIN booklet b ON u.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + }, + { + name: 'chunk', + query: ` + SELECT c.* FROM chunk c + INNER JOIN unit u ON c.unitid = u.id + INNER JOIN booklet b ON u.bookletid = b.id + INNER JOIN persons p ON b.personid = p.id + WHERE p.workspace_id = $1 + ` + } + ]; + + let processedTables = 0; + const totalTables = workspaceTables.length; + + for (const tableConfig of workspaceTables) { + const tableName = tableConfig.name; + + try { + this.logger.log(`Processing workspace table: ${tableName}`); + + // Get table structure from PostgreSQL + const columns = await this.dataSource.query(` + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = $1 AND table_schema = 'public' + ORDER BY ordinal_position + `, [tableName]); + + if (columns.length === 0) { + this.logger.warn(`Table ${tableName} not found, skipping...`); + continue; + } + + // Create table in SQLite + const columnDefs = columns.map(col => { + const colType = this.mapPostgresTypeToSqlite(col.data_type); + const nullable = col.is_nullable === 'YES' ? '' : ' NOT NULL'; + return `"${col.column_name}" ${colType}${nullable}`; + }).join(', '); + + await dbRun(`CREATE TABLE IF NOT EXISTS "${tableName}" (${columnDefs})`); + + // Get workspace-specific data + const rows = await this.dataSource.query(tableConfig.query, [workspaceId]); + + if (rows.length === 0) { + this.logger.log(`No data found for table ${tableName} in workspace ${workspaceId}`); + continue; + } + + // Process data in batches for memory efficiency + const batchSize = 1000; + let offset = 0; + + while (offset < rows.length) { + const batch = rows.slice(offset, offset + batchSize); + + // Prepare batch insert + const columnNames = columns.map(col => `"${col.column_name}"`).join(', '); + const placeholders = columns.map(() => '?').join(', '); + const insertSql = `INSERT INTO "${tableName}" (${columnNames}) VALUES (${placeholders})`; + + // Begin transaction for batch + await dbRun('BEGIN TRANSACTION'); + + try { + for (const row of batch) { + const values = columns.map(col => { + const value = row[col.column_name]; + // Handle special data types + if (value === null) return null; + if (typeof value === 'boolean') return value ? 1 : 0; + if (value instanceof Date) return value.toISOString(); + if (typeof value === 'object') return JSON.stringify(value); + return value; + }); + + await dbRun(insertSql, values); + } + + await dbRun('COMMIT'); + } catch (error) { + await dbRun('ROLLBACK'); + throw error; + } + + offset += batchSize; + this.logger.log(`Table ${tableName}: ${Math.min(offset, rows.length)}/${rows.length} rows processed`); + } + + processedTables += 1; + this.logger.log(`Completed table ${tableName} (${processedTables}/${totalTables})`); + } catch (error) { + this.logger.error(`Error processing table ${tableName}: ${error?.message || error}`, error?.stack); + // Continue with next table instead of failing completely + } + } + + // Close database connection + await dbClose(); + + this.logger.log(`SQLite workspace export completed for workspace ${workspaceId}, streaming file...`); + + // Stream the file to response + const fileStats = fs.statSync(tempFile); + response.setHeader('Content-Length', fileStats.size); + + // Create read stream and pipe to response + const fileStream = fs.createReadStream(tempFile); + + fileStream.on('error', error => { + this.logger.error(`Stream error: ${error?.message || error}`, error?.stack); + if (!response.headersSent) { + response.status(500).send('Error streaming file'); + } + }); + + fileStream.on('end', () => { + this.logger.log('File streaming completed'); + // Clean up temporary file + setTimeout(() => { + try { + fs.unlinkSync(tempFile); + this.logger.log('Temporary file cleaned up'); + } catch (error) { + this.logger.error(`Error cleaning up temporary file: ${error?.message || error}`, error?.stack); + } + }, 1000); + }); + + fileStream.pipe(response); + } catch (error) { + this.logger.error(`Export error: ${error?.message || error}`, error?.stack); + + // Clean up temporary file on error + try { + if (fs.existsSync(tempFile)) { + fs.unlinkSync(tempFile); + } + } catch (cleanupError) { + this.logger.error(`Error cleaning up temporary file: ${cleanupError?.message || cleanupError}`, cleanupError?.stack); + } + + throw error; + } + } + + private mapPostgresTypeToSqlite(postgresType: string): string { + const typeMap: Record = { + integer: 'INTEGER', + bigint: 'INTEGER', + smallint: 'INTEGER', + serial: 'INTEGER', + bigserial: 'INTEGER', + numeric: 'REAL', + decimal: 'REAL', + real: 'REAL', + 'double precision': 'REAL', + money: 'REAL', + 'character varying': 'TEXT', + varchar: 'TEXT', + character: 'TEXT', + char: 'TEXT', + text: 'TEXT', + boolean: 'INTEGER', + date: 'TEXT', + timestamp: 'TEXT', + 'timestamp without time zone': 'TEXT', + 'timestamp with time zone': 'TEXT', + time: 'TEXT', + interval: 'TEXT', + json: 'TEXT', + jsonb: 'TEXT', + uuid: 'TEXT', + bytea: 'BLOB' + }; + + // Handle array types + if (postgresType.includes('[]')) { + return 'TEXT'; + } + + return typeMap[postgresType.toLowerCase()] || 'TEXT'; + } +} diff --git a/apps/backend/src/app/admin/workspace/workspace-coding.controller.ts b/apps/backend/src/app/admin/workspace/workspace-coding.controller.ts index b42bc5413..a5b834161 100644 --- a/apps/backend/src/app/admin/workspace/workspace-coding.controller.ts +++ b/apps/backend/src/app/admin/workspace/workspace-coding.controller.ts @@ -17,6 +17,7 @@ import { VariableAnalysisItemDto } from '../../../../../../api-dto/coding/variab import { ValidateCodingCompletenessRequestDto } from '../../../../../../api-dto/coding/validate-coding-completeness-request.dto'; import { ValidateCodingCompletenessResponseDto } from '../../../../../../api-dto/coding/validate-coding-completeness-response.dto'; import { ExportValidationResultsRequestDto } from '../../../../../../api-dto/coding/export-validation-results-request.dto'; +import { ExternalCodingImportDto } from '../../../../../../api-dto/coding/external-coding-import.dto'; @ApiTags('Admin Workspace Coding') @Controller('admin/workspace') @@ -739,7 +740,6 @@ export class WorkspaceCodingController { page: number; limit: number; }> { - // Validate and sanitize pagination parameters const validPage = Math.max(1, page); const validLimit = Math.min(Math.max(1, limit), 500); // Set maximum limit to 500 @@ -807,7 +807,6 @@ export class WorkspaceCodingController { @Body() request: ExportValidationResultsRequestDto, @Res() res: Response ): Promise { - // Export the complete validation results from cache using cache key const excelData = await this.workspaceCodingService.exportValidationResultsAsExcel( workspace_id, request.cacheKey @@ -853,4 +852,185 @@ export class WorkspaceCodingController { unitName ); } + + @Post(':workspace_id/coding/external-coding-import/stream') + @UseGuards(JwtAuthGuard, WorkspaceGuard) + @ApiTags('coding') + @ApiParam({ name: 'workspace_id', type: Number }) + @ApiBody({ + description: 'External coding file upload (CSV/Excel) with streaming progress', + type: ExternalCodingImportDto + }) + @ApiOkResponse({ + description: 'External coding import with progress streaming', + content: { + 'text/event-stream': { + schema: { + type: 'string' + } + } + } + }) + async importExternalCodingWithProgress( + @WorkspaceId() workspace_id: number, + @Body() body: ExternalCodingImportDto, + @Res() res: Response + ): Promise { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'Cache-Control'); + + try { + const result = await this.workspaceCodingService.importExternalCodingWithProgress( + workspace_id, + body, + (progress: number, message: string) => { + res.write(`data: ${JSON.stringify({ progress, message })}\n\n`); + } + ); + + // Send final result + res.write(`data: ${JSON.stringify({ + progress: 100, + message: 'Import completed', + result + })}\n\n`); + res.end(); + } catch (error) { + res.write(`data: ${JSON.stringify({ + progress: 0, + message: `Import failed: ${error.message}`, + error: true + })}\n\n`); + res.end(); + } + } + + @Post(':workspace_id/coding/external-coding-import') + @UseGuards(JwtAuthGuard, WorkspaceGuard) + @ApiTags('coding') + @ApiParam({ name: 'workspace_id', type: Number }) + @ApiBody({ + description: 'External coding file upload (CSV/Excel)', + type: ExternalCodingImportDto + }) + async importExternalCoding( + @WorkspaceId() workspace_id: number, + @Body() body: ExternalCodingImportDto + ): Promise<{ + message: string; + processedRows: number; + updatedRows: number; + errors: string[]; + affectedRows: Array<{ + unitAlias: string; + variableId: string; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; + originalCodedStatus: string; + originalCode: number | null; + originalScore: number | null; + updatedCodedStatus: string | null; + updatedCode: number | null; + updatedScore: number | null; + }>; + }> { + return this.workspaceCodingService.importExternalCoding(workspace_id, body); + } + + @Post(':workspace_id/coding/coder-training-packages') + @UseGuards(JwtAuthGuard, WorkspaceGuard) + @ApiTags('coding') + @ApiParam({ name: 'workspace_id', type: Number }) + @ApiBody({ + description: 'Generate coder training packages based on CODING_INCOMPLETE responses', + schema: { + type: 'object', + properties: { + selectedCoders: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'number' }, + name: { type: 'string' } + } + } + }, + variableConfigs: { + type: 'array', + items: { + type: 'object', + properties: { + variableName: { type: 'string' }, + sampleCount: { type: 'number' } + } + } + } + } + } + }) + @ApiOkResponse({ + description: 'Coder training packages generated successfully', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + coderId: { type: 'number' }, + coderName: { type: 'string' }, + responses: { + type: 'array', + items: { + type: 'object', + properties: { + responseId: { type: 'number' }, + unitAlias: { type: 'string' }, + variableId: { type: 'string' }, + unitName: { type: 'string' }, + value: { type: 'string' }, + personLogin: { type: 'string' }, + personCode: { type: 'string' }, + personGroup: { type: 'string' }, + bookletName: { type: 'string' }, + variable: { type: 'string' } + } + } + } + } + } + } + }) + async generateCoderTrainingPackages( + @WorkspaceId() workspace_id: number, + @Body() body: { + selectedCoders: { id: number; name: string }[]; + variableConfigs: { variableName: string; sampleCount: number }[]; + } + ): Promise<{ + coderId: number; + coderName: string; + responses: { + responseId: number; + unitAlias: string; + variableId: string; + unitName: string; + value: string; + personLogin: string; + personCode: string; + personGroup: string; + bookletName: string; + variable: string; + }[]; + }[]> { + return this.workspaceCodingService.generateCoderTrainingPackages( + workspace_id, + body.selectedCoders, + body.variableConfigs + ); + } } diff --git a/apps/backend/src/app/admin/workspace/workspace-files.controller.ts b/apps/backend/src/app/admin/workspace/workspace-files.controller.ts index c1b709afe..abb8d891d 100644 --- a/apps/backend/src/app/admin/workspace/workspace-files.controller.ts +++ b/apps/backend/src/app/admin/workspace/workspace-files.controller.ts @@ -12,7 +12,7 @@ import { } from '@nestjs/swagger'; import { FilesInterceptor } from '@nestjs/platform-express'; import { logger } from 'nx/src/utils/logger'; -import { VariableInfo } from '@iqb/responses'; +import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; import { FilesDto } from '../../../../../../api-dto/files/files.dto'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { WorkspaceGuard } from './workspace.guard'; diff --git a/apps/backend/src/app/admin/workspace/workspace-test-center.controller.ts b/apps/backend/src/app/admin/workspace/workspace-test-center.controller.ts index 98666cdc0..54b4698a6 100644 --- a/apps/backend/src/app/admin/workspace/workspace-test-center.controller.ts +++ b/apps/backend/src/app/admin/workspace/workspace-test-center.controller.ts @@ -7,20 +7,11 @@ import { ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { TestcenterService } from '../../database/services/testcenter.service'; +import { TestcenterService, Result } from '../../database/services/testcenter.service'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { WorkspaceGuard } from './workspace.guard'; import { TestGroupsInfoDto } from '../../../../../../api-dto/files/test-groups-info.dto'; -import { - ImportOptions -} from '../../../../../frontend/src/app/ws-admin/components/test-center-import/test-center-import.component'; - -export type Result = { - success: boolean, - testFiles: number, - responses: number, - logs: number -}; +import { ImportOptions } from '../../../../../frontend/src/app/services/import.service'; @ApiTags('Admin Workspace Test Center') @Controller('admin/workspace') diff --git a/apps/backend/src/app/admin/workspace/workspace-test-results.controller.ts b/apps/backend/src/app/admin/workspace/workspace-test-results.controller.ts index 4d5363148..4dea50c83 100644 --- a/apps/backend/src/app/admin/workspace/workspace-test-results.controller.ts +++ b/apps/backend/src/app/admin/workspace/workspace-test-results.controller.ts @@ -2,15 +2,16 @@ import { BadRequestException, Controller, Delete, - Get, Param, Post, Query, UseGuards, UseInterceptors, UploadedFiles + Get, Param, Post, Query, Req, UseGuards, UseInterceptors, UploadedFiles, Res } from '@nestjs/common'; import { ApiBadRequestResponse, ApiBearerAuth, ApiBody, ApiConsumes, ApiOkResponse, ApiOperation, - ApiParam, ApiQuery, ApiTags + ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { FilesInterceptor } from '@nestjs/platform-express'; import { logger } from 'nx/src/utils/logger'; +import { Response } from 'express'; import { JwtAuthGuard } from '../../auth/jwt-auth.guard'; import { WorkspaceGuard } from './workspace.guard'; import { WorkspaceId } from './workspace.decorator'; @@ -18,13 +19,15 @@ import { UploadResultsService } from '../../database/services/upload-results.ser import Persons from '../../database/entities/persons.entity'; import { ResponseEntity } from '../../database/entities/response.entity'; import { WorkspaceTestResultsService } from '../../database/services/workspace-test-results.service'; +import { DatabaseExportService } from '../database/database-export.service'; @ApiTags('Admin Workspace Test Results') @Controller('admin/workspace') export class WorkspaceTestResultsController { constructor( private workspaceTestResultsService: WorkspaceTestResultsService, - private uploadResults: UploadResultsService + private uploadResults: UploadResultsService, + private databaseExportService: DatabaseExportService ) {} @Get(':workspace_id/test-results') @@ -173,14 +176,15 @@ export class WorkspaceTestResultsController { @UseGuards(JwtAuthGuard, WorkspaceGuard) async deleteTestGroups( @Query('testPersons')testPersonIds:string, - @Param('workspace_id')workspaceId:string): Promise<{ + @Param('workspace_id')workspaceId:string, + @Req() req): Promise<{ success: boolean; report: { deletedPersons: string[]; warnings: string[]; }; }> { - return this.workspaceTestResultsService.deleteTestPersons(Number(workspaceId), testPersonIds); + return this.workspaceTestResultsService.deleteTestPersons(Number(workspaceId), testPersonIds, req.user.id); } @Delete(':workspace_id/units/:unitId') @@ -210,7 +214,8 @@ export class WorkspaceTestResultsController { @UseGuards(JwtAuthGuard, WorkspaceGuard) async deleteUnit( @Param('workspace_id') workspaceId: number, - @Param('unitId') unitId: number + @Param('unitId') unitId: number, + @Req() req ): Promise<{ success: boolean; report: { @@ -218,7 +223,7 @@ export class WorkspaceTestResultsController { warnings: string[]; }; }> { - return this.workspaceTestResultsService.deleteUnit(workspaceId, unitId); + return this.workspaceTestResultsService.deleteUnit(workspaceId, unitId, req.user.id); } @Delete(':workspace_id/responses/:responseId') @@ -248,7 +253,8 @@ export class WorkspaceTestResultsController { @UseGuards(JwtAuthGuard, WorkspaceGuard) async deleteResponse( @Param('workspace_id') workspaceId: number, - @Param('responseId') responseId: number + @Param('responseId') responseId: number, + @Req() req ): Promise<{ success: boolean; report: { @@ -256,7 +262,7 @@ export class WorkspaceTestResultsController { warnings: string[]; }; }> { - return this.workspaceTestResultsService.deleteResponse(workspaceId, responseId); + return this.workspaceTestResultsService.deleteResponse(workspaceId, responseId, req.user.id); } @Delete(':workspace_id/booklets/:bookletId') @@ -286,7 +292,8 @@ export class WorkspaceTestResultsController { @UseGuards(JwtAuthGuard, WorkspaceGuard) async deleteBooklet( @Param('workspace_id') workspaceId: number, - @Param('bookletId') bookletId: number + @Param('bookletId') bookletId: number, + @Req() req ): Promise<{ success: boolean; report: { @@ -294,7 +301,7 @@ export class WorkspaceTestResultsController { warnings: string[]; }; }> { - return this.workspaceTestResultsService.deleteBooklet(workspaceId, bookletId); + return this.workspaceTestResultsService.deleteBooklet(workspaceId, bookletId, req.user.id); } @Get(':workspace_id/responses') @@ -816,8 +823,6 @@ export class WorkspaceTestResultsController { if (!files || files.length === 0) { throw new BadRequestException('No files were uploaded.'); } - - // Convert the query parameter to a boolean const shouldOverwrite = overwriteExisting !== 'false'; logger.log(`Uploading test results with overwriteExisting=${shouldOverwrite}`); @@ -829,4 +834,40 @@ export class WorkspaceTestResultsController { throw new BadRequestException('Uploading test results failed. Please try again.'); } } + + @Get(':workspace_id/export/sqlite') + @ApiOperation({ + summary: 'Export workspace test results to SQLite', + description: 'Exports workspace-specific test results data to SQLite format with streaming support' + }) + @ApiParam({ name: 'workspace_id', type: Number, description: 'ID of the workspace' }) + @ApiResponse({ + status: 200, + description: 'SQLite database file downloaded successfully', + content: { + 'application/x-sqlite3': { + schema: { + type: 'string', + format: 'binary' + } + } + } + }) + @ApiBearerAuth() + @UseGuards(JwtAuthGuard, WorkspaceGuard) + async exportWorkspaceToSqlite( + @Param('workspace_id') workspace_id: number, + @Res() response: Response + ): Promise { + try { + response.setHeader('Content-Type', 'application/x-sqlite3'); + response.setHeader('Content-Disposition', `attachment; filename=workspace-${workspace_id}-export-${new Date().toISOString().split('T')[0]}.sqlite`); + + await this.databaseExportService.exportWorkspaceToSqliteStream(response, workspace_id); + } catch (error) { + if (!response.headersSent) { + response.status(500).json({ error: 'Failed to export workspace database to SQLite' }); + } + } + } } diff --git a/apps/backend/src/app/admin/workspace/workspace.decorator.ts b/apps/backend/src/app/admin/workspace/workspace.decorator.ts index 38f9c207b..50f9240f9 100755 --- a/apps/backend/src/app/admin/workspace/workspace.decorator.ts +++ b/apps/backend/src/app/admin/workspace/workspace.decorator.ts @@ -1,9 +1,15 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { createParamDecorator, ExecutionContext, BadRequestException } from '@nestjs/common'; export const WorkspaceId = createParamDecorator( (data: unknown, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); const params = request.params; - return params.workspace_id; + const workspaceId = parseInt(params.workspace_id, 10); + + if (Number.isNaN(workspaceId) || workspaceId <= 0) { + throw new BadRequestException('workspace_id must be a positive number'); + } + + return workspaceId; } ); diff --git a/apps/backend/src/app/database/entities/response.entity.ts b/apps/backend/src/app/database/entities/response.entity.ts index d211a0e03..6cac7c3f2 100644 --- a/apps/backend/src/app/database/entities/response.entity.ts +++ b/apps/backend/src/app/database/entities/response.entity.ts @@ -8,7 +8,7 @@ import { Unit } from './unit.entity'; @Entity('response') @Index(['unitid', 'variableid']) // Composite index for common query patterns @Index(['unitid', 'status']) // Composite index for filtering by status -@Index(['codedstatus']) // Index for filtering by coded status +@Index(['status_v1']) // Index for filtering by coded status @Index(['value']) // Index for searching by value export class ResponseEntity { @PrimaryGeneratedColumn() @@ -31,14 +31,32 @@ export class ResponseEntity { @Column({ type: 'text', nullable: true }) subform: string; + @Column({ type: 'integer', nullable: true }) + status_v1: string | null; + @Column({ type: 'bigint', nullable: true }) - code: number | null; + code_v1: number | null; @Column({ type: 'bigint', nullable: true }) - score: number | null; + score_v1: number | null; - @Column({ type: 'text' }) - codedstatus: string; + @Column({ type: 'text', nullable: true }) + status_v2: string | null; + + @Column({ type: 'bigint', nullable: true }) + code_v2: number | null; + + @Column({ type: 'bigint', nullable: true }) + score_v2: number | null; + + @Column({ type: 'text', nullable: true }) + status_v3: string | null; + + @Column({ type: 'bigint', nullable: true }) + code_v3: number | null; + + @Column({ type: 'bigint', nullable: true }) + score_v3: number | null; @ManyToOne(() => Unit, unit => unit.responses, { onDelete: 'CASCADE' diff --git a/apps/backend/src/app/database/services/coding-job.service.ts b/apps/backend/src/app/database/services/coding-job.service.ts index c5472fd71..1dc76db19 100644 --- a/apps/backend/src/app/database/services/coding-job.service.ts +++ b/apps/backend/src/app/database/services/coding-job.service.ts @@ -8,10 +8,8 @@ import { CodingJobVariableBundle } from '../entities/coding-job-variable-bundle. import { CreateCodingJobDto } from '../../admin/coding-job/dto/create-coding-job.dto'; import { UpdateCodingJobDto } from '../../admin/coding-job/dto/update-coding-job.dto'; import { VariableBundle } from '../entities/variable-bundle.entity'; +import { ResponseEntity } from '../entities/response.entity'; -/** - * Service for managing coding jobs - */ @Injectable() export class CodingJobService { constructor( @@ -24,24 +22,19 @@ export class CodingJobService { @InjectRepository(CodingJobVariableBundle) private codingJobVariableBundleRepository: Repository, @InjectRepository(VariableBundle) - private variableBundleRepository: Repository + private variableBundleRepository: Repository, + @InjectRepository(ResponseEntity) + private responseRepository: Repository ) {} - /** - * Get coding jobs for a workspace with pagination - * @param workspaceId The ID of the workspace - * @param page The page number (1-based) - * @param limit The number of items per page - * @returns Paginated coding jobs with metadata, assigned coders, variables, and variable bundles - */ async getCodingJobs( workspaceId: number, page: number = 1, limit: number = 10 ): Promise<{ data: (CodingJob & { assignedCoders?: number[]; - assignedVariables?: string[]; - assignedVariableBundles?: string[] + assignedVariables?: { unitName: string; variableId: string }[]; + assignedVariableBundles?: { name: string; variables: { unitName: string; variableId: string }[] }[] })[]; total: number; page: number; limit: number }> { const validPage = page > 0 ? page : 1; const validLimit = limit > 0 ? limit : 10; @@ -82,21 +75,27 @@ export class CodingJobService { codersByJobId.get(coder.coding_job_id)!.push(coder.user_id); }); - const variablesByJobId = new Map(); + const variablesByJobId = new Map(); allVariables.forEach(variable => { if (!variablesByJobId.has(variable.coding_job_id)) { variablesByJobId.set(variable.coding_job_id, []); } - variablesByJobId.get(variable.coding_job_id)!.push(variable.variable_id); + variablesByJobId.get(variable.coding_job_id)!.push({ + unitName: variable.unit_name, + variableId: variable.variable_id + }); }); - const variableBundlesByJobId = new Map(); + const variableBundlesByJobId = new Map(); variableBundleEntities.forEach(bundleAssignment => { if (!variableBundlesByJobId.has(bundleAssignment.coding_job_id)) { variableBundlesByJobId.set(bundleAssignment.coding_job_id, []); } if (bundleAssignment.variable_bundle?.name) { - variableBundlesByJobId.get(bundleAssignment.coding_job_id)!.push(bundleAssignment.variable_bundle.name); + variableBundlesByJobId.get(bundleAssignment.coding_job_id)!.push({ + name: bundleAssignment.variable_bundle.name, + variables: bundleAssignment.variable_bundle.variables || [] + }); } }); @@ -106,7 +105,6 @@ export class CodingJobService { assignedVariables: variablesByJobId.get(job.id) || [], assignedVariableBundles: variableBundlesByJobId.get(job.id) || [] })); - console.log(data); return { data, total, @@ -115,13 +113,6 @@ export class CodingJobService { }; } - /** - * Get a coding job by ID - * @param id The ID of the coding job - * @param workspaceId Optional workspace ID to filter by - * @returns The coding job with its relations - * @throws NotFoundException if the coding job is not found - */ async getCodingJob(id: number, workspaceId?: number): Promise<{ codingJob: CodingJob; assignedCoders: number[]; @@ -143,13 +134,11 @@ export class CodingJobService { } } - // Get assigned coders const coders = await this.codingJobCoderRepository.find({ where: { coding_job_id: id } }); const assignedCoders = coders.map(coder => coder.user_id); - // Get variables const codingJobVariables = await this.codingJobVariableRepository.find({ where: { coding_job_id: id } }); @@ -158,7 +147,6 @@ export class CodingJobService { variableId: variable.variable_id })); - // Get variable bundles const codingJobVariableBundles = await this.codingJobVariableBundleRepository.find({ where: { coding_job_id: id } }); @@ -175,17 +163,10 @@ export class CodingJobService { }; } - /** - * Create a new coding job - * @param workspaceId The ID of the workspace - * @param createCodingJobDto The coding job data - * @returns The created coding job - */ async createCodingJob( workspaceId: number, createCodingJobDto: CreateCodingJobDto ): Promise { - // Create the coding job const codingJob = this.codingJobRepository.create({ workspace_id: workspaceId, name: createCodingJobDto.name, @@ -193,26 +174,20 @@ export class CodingJobService { status: createCodingJobDto.status || 'pending' }); - // Save the coding job const savedCodingJob = await this.codingJobRepository.save(codingJob); - // Assign coders if provided if (createCodingJobDto.assignedCoders && createCodingJobDto.assignedCoders.length > 0) { await this.assignCoders(savedCodingJob.id, createCodingJobDto.assignedCoders); } - // Assign variables if provided if (createCodingJobDto.variables && createCodingJobDto.variables.length > 0) { await this.assignVariables(savedCodingJob.id, createCodingJobDto.variables); } - // Assign variable bundles if provided if (createCodingJobDto.variableBundleIds && createCodingJobDto.variableBundleIds.length > 0) { await this.assignVariableBundles(savedCodingJob.id, createCodingJobDto.variableBundleIds); } else if (createCodingJobDto.variableBundles && createCodingJobDto.variableBundles.length > 0) { - // Handle variable bundles without IDs by using their variables directly if (createCodingJobDto.variableBundles[0].id) { - // Extract IDs from variableBundles if they have IDs const bundleIds = createCodingJobDto.variableBundles .filter(bundle => bundle.id) .map(bundle => bundle.id); @@ -221,7 +196,6 @@ export class CodingJobService { await this.assignVariableBundles(savedCodingJob.id, bundleIds); } } else { - // Otherwise, extract variables and assign them directly const variables = createCodingJobDto.variableBundles.flatMap(bundle => bundle.variables || []); if (variables.length > 0) { await this.assignVariables(savedCodingJob.id, variables); @@ -232,14 +206,6 @@ export class CodingJobService { return savedCodingJob; } - /** - * Update a coding job - * @param id The ID of the coding job - * @param workspaceId The ID of the workspace - * @param updateCodingJobDto The coding job data to update - * @returns The updated coding job - * @throws NotFoundException if the coding job is not found - */ async updateCodingJob( id: number, workspaceId: number, @@ -302,33 +268,16 @@ export class CodingJobService { return savedCodingJob; } - /** - * Delete a coding job - * @param id The ID of the coding job - * @param workspaceId The ID of the workspace - * @returns Object with success flag - * @throws NotFoundException if the coding job is not found - */ async deleteCodingJob(id: number, workspaceId: number): Promise<{ success: boolean }> { const codingJob = await this.getCodingJob(id, workspaceId); - // Delete the coding job (cascade will delete related entities) await this.codingJobRepository.remove(codingJob.codingJob); return { success: true }; } - /** - * Assign coders to a coding job - * @param codingJobId The ID of the coding job - * @param userIds The IDs of the users to assign - * @returns The created coding job coder relations - */ async assignCoders(codingJobId: number, userIds: number[]): Promise { - // Remove existing coders first await this.codingJobCoderRepository.delete({ coding_job_id: codingJobId }); - - // Create new coder assignments const coders = userIds.map(userId => this.codingJobCoderRepository.create({ coding_job_id: codingJobId, user_id: userId @@ -337,12 +286,6 @@ export class CodingJobService { return this.codingJobCoderRepository.save(coders); } - /** - * Assign variables to a coding job - * @param codingJobId The ID of the coding job - * @param variables The variables to assign - * @returns The created coding job variable relations - */ private async assignVariables( codingJobId: number, variables: { unitName: string; variableId: string }[] @@ -356,12 +299,6 @@ export class CodingJobService { return this.codingJobVariableRepository.save(codingJobVariables); } - /** - * Assign variable bundles to a coding job - * @param codingJobId The ID of the coding job - * @param variableBundleIds The IDs of the variable bundles to assign - * @returns The created coding job variable bundle relations - */ private async assignVariableBundles( codingJobId: number, variableBundleIds: number[] @@ -373,4 +310,124 @@ export class CodingJobService { return this.codingJobVariableBundleRepository.save(variableBundles); } + + async getCodingJobsByCoder(coderId: number): Promise { + const codingJobCoders = await this.codingJobCoderRepository.find({ + where: { user_id: coderId }, + relations: ['coding_job'] + }); + + return codingJobCoders.map(cjc => cjc.coding_job); + } + + async getCodersByJobId(jobId: number): Promise { + const codingJobCoders = await this.codingJobCoderRepository.find({ + where: { coding_job_id: jobId } + }); + + return codingJobCoders.map(cjc => cjc.user_id); + } + + async getCodingJobById(id: number): Promise { + const codingJob = await this.codingJobRepository.findOne({ + where: { id } + }); + + if (!codingJob) { + throw new NotFoundException(`Coding job with ID ${id} not found`); + } + + const coders = await this.codingJobCoderRepository.find({ + where: { coding_job_id: id } + }); + const assignedCoders = coders.map(coder => coder.user_id); + + const codingJobVariables = await this.codingJobVariableRepository.find({ + where: { coding_job_id: id } + }); + const assignedVariables = codingJobVariables.map(variable => ({ + unitName: variable.unit_name, + variableId: variable.variable_id + })); + + const codingJobVariableBundles = await this.codingJobVariableBundleRepository.find({ + where: { coding_job_id: id }, + relations: ['variable_bundle'] + }); + + const assignedVariableBundles = codingJobVariableBundles + .filter(bundle => bundle.variable_bundle) + .map(bundle => ({ + name: bundle.variable_bundle.name, + variables: bundle.variable_bundle.variables || [] + })); + + return { + ...codingJob, + assignedCoders, + assignedVariables, + assignedVariableBundles + }; + } + + async getResponsesForCodingJob(codingJobId: number): Promise { + const codingJobVariables = await this.codingJobVariableRepository.find({ + where: { coding_job_id: codingJobId } + }); + + const codingJobVariableBundles = await this.codingJobVariableBundleRepository.find({ + where: { coding_job_id: codingJobId }, + relations: ['variable_bundle'] + }); + + const allVariables: { unit_name: string; variable_id: string }[] = codingJobVariables.map(v => ({ + unit_name: v.unit_name, + variable_id: v.variable_id + })); + codingJobVariableBundles.forEach(bundle => { + if (bundle.variable_bundle?.variables) { + bundle.variable_bundle.variables.forEach(variable => { + allVariables.push({ + unit_name: variable.unitName, + variable_id: variable.variableId + }); + }); + } + }); + + if (allVariables.length === 0) { + return []; + } + + const queryBuilder = this.responseRepository.createQueryBuilder('response') + .leftJoinAndSelect('response.unit', 'unit') + .leftJoinAndSelect('unit.booklet', 'booklet') + .leftJoinAndSelect('booklet.bookletinfo', 'bookletinfo') + .leftJoinAndSelect('booklet.person', 'person'); + + const conditions: string[] = []; + const parameters: Record = {}; + + allVariables.forEach((variable, index) => { + const unitParam = `unitName${index}`; + const variableParam = `variableId${index}`; + conditions.push(`(unit.name = :${unitParam} AND response.variableid = :${variableParam})`); + parameters[unitParam] = variable.unit_name; + parameters[variableParam] = variable.variable_id; + }); + + if (conditions.length > 0) { + queryBuilder.where(`(${conditions.join(' OR ')})`, parameters); + } + + return queryBuilder + .orderBy('response.id', 'ASC') + .getMany(); + } } diff --git a/apps/backend/src/app/database/services/person.service.ts b/apps/backend/src/app/database/services/person.service.ts index dfb491f0f..f8e9c7a39 100644 --- a/apps/backend/src/app/database/services/person.service.ts +++ b/apps/backend/src/app/database/services/person.service.ts @@ -174,18 +174,19 @@ export class PersonService { const personMap = new Map(); rows.forEach((row, index) => { try { - if (!row.groupname || !row.loginname || !row.code) { - this.logger.warn(`Skipping incomplete row at index ${index}: ${JSON.stringify(row)}`); - return; - } + // Allow empty values for groupname, loginname, and code + // Use empty string as fallback for missing values + const groupname = row.groupname || ''; + const loginname = row.loginname || ''; + const code = row.code || ''; - const mapKey = `${row.groupname}-${row.loginname}-${row.code}`; + const mapKey = `${groupname}-${loginname}-${code}`; if (!personMap.has(mapKey)) { personMap.set(mapKey, { workspace_id, - group: row.groupname, - login: row.loginname, - code: row.code, + group: groupname, + login: loginname, + code: code, booklets: [] }); } @@ -203,8 +204,8 @@ export class PersonService { async assignBookletsToPerson(person: Person, rows: Response[]): Promise { const logger = new Logger('assignBookletsToPerson'); - const bookletIds = new Set(); // To avoid duplicate booklets - const booklets: TcMergeBooklet[] = []; // List of booklets to be assigned + const bookletIds = new Set(); + const booklets: TcMergeBooklet[] = []; for (const row of rows) { try { @@ -280,21 +281,12 @@ export class PersonService { if (logEntryKeyTrimmed === 'LOADCOMPLETE' && logEntryValue) { const parsedResult = this.parseLoadCompleteLog(logEntryValue); if (parsedResult) { - const { - browserVersion = 'Unknown', - browserName = 'Unknown', - osName = 'Unknown', - screenSizeWidth = '0', - screenSizeHeight = '0', - loadTime = '0' - } = parsedResult; - booklet.sessions.push({ - browser: `${browserName} ${browserVersion}`.trim(), - os: osName.toString(), - screen: `${screenSizeWidth} x ${screenSizeHeight}`, + browser: `${parsedResult.browserName} ${parsedResult.browserVersion}`.trim(), + os: parsedResult.osName, + screen: `${parsedResult.screenSizeWidth} x ${parsedResult.screenSizeHeight}`, ts: timestamp, - loadCompleteMS: Number(loadTime) || 0 + loadCompleteMS: parsedResult.loadTime }); } else { this.logger.warn( @@ -323,17 +315,32 @@ export class PersonService { return person; } - private parseLoadCompleteLog(logEntry: string): { [key: string]: string | number | undefined } | null { + private parseLoadCompleteLog(logEntry: string): { + browserVersion: string, + browserName: string, + osName: string, + device: string, + screenSizeWidth: number, + screenSizeHeight: number, + loadTime: number + } | null { try { const keyValues = logEntry.slice(1, -1).split(','); const parsedResult: { [key: string]: string | number | undefined } = {}; - keyValues.forEach(pair => { - const [key, value] = pair.split(':', 2).map(part => part.trim()); + const [key, value] = pair.split(':', 2).map(part => part.trim().replace(/\\/g, '')); parsedResult[key] = !Number.isNaN(Number(value)) ? Number(value) : value || undefined; }); - return parsedResult; + return { + browserVersion: parsedResult.browserVersion?.toString() || 'Unknown', + browserName: parsedResult.browserName?.toString() || 'Unknown', + osName: parsedResult.osName?.toString() || 'Unknown', + device: parsedResult.device?.toString() || 'Unknown', + screenSizeWidth: Number(parsedResult.screenSizeWidth) || 0, + screenSizeHeight: Number(parsedResult.screenSizeHeight) || 0, + loadTime: Number(parsedResult.loadTime) || 0 + }; } catch (error) { this.logger.error(`Failed to parse LOADCOMPLETE log entry: ${logEntry} - ${error.message}`); return null; @@ -394,8 +401,7 @@ export class PersonService { }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private extractVariablesFromSubforms(subforms: any[]): Set { + private extractVariablesFromSubforms(subforms: TcMergeSubForms[]): Set { const variables = new Set(); subforms.forEach(subform => subform.responses.forEach(response => variables.add(response.id)) ); @@ -487,11 +493,11 @@ export class PersonService { for (const person of persons) { if (!person.booklets || person.booklets.length === 0) { - continue; // Skip silently to reduce log noise + continue; } for (const booklet of person.booklets) { if (!booklet || !booklet.id) { - continue; // Skip silently to reduce log noise + continue; } try { @@ -505,7 +511,6 @@ export class PersonService { if (unit.subforms) { for (const subform of unit.subforms) { if (subform.responses) { - // This is just an estimate as we don't have the actual count of saved vs skipped totalResponsesProcessed += subform.responses.length; } } @@ -544,8 +549,7 @@ export class PersonService { ); } - // Find or create booklet - let savedBooklet = await this.bookletRepository.findOne({ + const existingBooklet = await this.bookletRepository.findOne({ where: { personid: person.id, infoid: bookletInfo.id @@ -557,49 +561,50 @@ export class PersonService { return; } - if (!savedBooklet) { - savedBooklet = await this.bookletRepository.save( - this.bookletRepository.create({ - personid: person.id, - infoid: bookletInfo.id, - lastts: Date.now(), - firstts: Date.now() - }) - ); + // Prevent duplicate entries - return early if booklet already exists + if (existingBooklet) { + this.logger.log(`Booklet ${booklet.id} already exists for person ${person.id}, skipping duplicate`); + return; } + const newBooklet = await this.bookletRepository.save( + this.bookletRepository.create({ + personid: person.id, + infoid: bookletInfo.id, + lastts: Date.now(), + firstts: Date.now() + }) + ); + if (Array.isArray(booklet.units) && booklet.units.length > 0) { - // Process units in batches to improve performance const batchSize = 10; for (let i = 0; i < booklet.units.length; i += batchSize) { const unitBatch = booklet.units.slice(i, i + batchSize); await Promise.all( unitBatch.map(async unit => { if (!unit || !unit.id) { - return; // Skip invalid units silently + return; } - try { - let savedUnit = await this.unitRepository.findOne({ - where: { alias: unit.alias, name: unit.id, bookletid: savedBooklet.id } + const existingUnit = await this.unitRepository.findOne({ + where: { alias: unit.alias, name: unit.id, bookletid: newBooklet.id } }); - if (!savedUnit) { - savedUnit = await this.unitRepository.save( + if (!existingUnit) { + const newUnit = await this.unitRepository.save( this.unitRepository.create({ alias: unit.alias, name: unit.id, - bookletid: savedBooklet.id + bookletid: newBooklet.id }) ); - } - - if (savedUnit) { - await Promise.all([ - this.saveUnitLastState(unit, savedUnit), - this.processSubforms(unit, savedUnit), - this.processChunks(unit, savedUnit, booklet) - ]); + if (newUnit) { + await Promise.all([ + this.saveUnitLastState(unit, newUnit), + this.processSubforms(unit, newUnit), + this.processChunks(unit, newUnit, booklet) + ]); + } } } catch (unitError) { this.logger.error( @@ -618,7 +623,6 @@ export class PersonService { where: { unitid: savedUnit.id } }); - // Only save if no last state exists and we have data to save if (currentLastState.length === 0 && unit.laststate) { const lastStateEntries = Object.entries(unit.laststate).map(([key]) => ({ unitid: savedUnit.id, @@ -626,10 +630,8 @@ export class PersonService { value: unit.laststate[key].value })); - // Only proceed if we have entries to insert if (lastStateEntries.length > 0) { await this.unitLastStateRepository.insert(lastStateEntries); - // Only log if we actually saved something if (lastStateEntries.length > 10) { this.logger.log(`Saved ${lastStateEntries.length} laststate entries for unit ${unit.id}`); } @@ -675,30 +677,30 @@ export class PersonService { } } } catch (error) { - // Include booklet ID in error message for better context this.logger.error(`Failed to save chunks for unit ${unit.id} in booklet ${booklet.id}: ${error.message}`); } } async saveSubformResponsesForUnit( savedUnit: Unit, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - subforms: any[] + subforms: TcMergeSubForms[] ): Promise<{ success: boolean; saved: number; skipped: number }> { try { let totalResponsesSaved = 0; for (const subform of subforms) { if (subform.responses && subform.responses.length > 0) { const responseEntries = subform.responses.map(response => { - let value = response.value; - if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) { - value = `[${value.substring(1, value.length - 1)}]`; + let value: string | null; + if (response.value === null) { + value = null; + } else { + value = JSON.stringify(response.value); } return { unitid: Number(savedUnit.id), variableid: response.id, - status: response.status, + status: response.status as string, value: value, subform: subform.id }; @@ -794,10 +796,8 @@ export class PersonService { async processPersonLogs( persons: Person[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - unitLogs: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - bookletLogs: any, + unitLogs: Log[], + bookletLogs: Log[], overwriteExistingLogs: boolean = true ): Promise<{ success: boolean; @@ -905,8 +905,6 @@ export class PersonService { try { totalBooklets += 1; - - // Store booklet logs with overwrite flag const logsResult = await this.storeBookletLogs( booklet, existingBooklet.id, @@ -965,18 +963,15 @@ export class PersonService { } try { - // Check if logs already exist for this booklet const existingLogsCount = await this.bookletLogRepository.count({ where: { bookletid: bookletId } }); - // If logs exist and we're not supposed to overwrite, skip if (existingLogsCount > 0 && !overwriteExisting) { this.logger.log(`Skipping ${booklet.logs.length} logs for booklet ${booklet.id} (logs already exist)`); return { success: true, saved: 0, skipped: booklet.logs.length }; } - // If logs exist and we're supposed to overwrite, delete existing logs first if (existingLogsCount > 0 && overwriteExisting) { await this.bookletLogRepository.delete({ bookletid: bookletId }); this.logger.log(`Deleted ${existingLogsCount} existing logs for booklet ${booklet.id}`); @@ -1107,7 +1102,6 @@ export class PersonService { ts: Number(log.ts) })); - // Use batch processing for better performance with large datasets const BATCH_SIZE = 1000; for (let i = 0; i < unitLogEntries.length; i += BATCH_SIZE) { const batch = unitLogEntries.slice(i, i + BATCH_SIZE); diff --git a/apps/backend/src/app/database/services/replay-statistics.service.ts b/apps/backend/src/app/database/services/replay-statistics.service.ts index 1c307bc3f..782aa19df 100644 --- a/apps/backend/src/app/database/services/replay-statistics.service.ts +++ b/apps/backend/src/app/database/services/replay-statistics.service.ts @@ -33,7 +33,6 @@ export class ReplayStatisticsService { errorMessage?: string; }): Promise { try { - // Map camelCase properties to snake_case properties to match the entity const mappedData = { workspace_id: data.workspaceId, unit_id: data.unitId, @@ -78,11 +77,21 @@ export class ReplayStatisticsService { */ async getReplayFrequencyByUnit(workspaceId: number): Promise> { try { - const statistics = await this.getReplayStatistics(workspaceId); - return statistics.reduce((acc, stat) => { - acc[stat.unit_id] = (acc[stat.unit_id] || 0) + 1; - return acc; - }, {} as Record); + const result = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'stats.unit_id', + 'COUNT(*) as count' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .groupBy('stats.unit_id') + .getRawMany(); + + const frequency: Record = {}; + result.forEach(row => { + frequency[row.stats_unit_id] = parseInt(row.count, 10); + }); + + return frequency; } catch (error) { this.logger.error(`Error calculating replay frequency: ${error.message}`, error.stack); throw error; @@ -106,16 +115,24 @@ export class ReplayStatisticsService { unitAverages?: Record; }> { try { - const query = this.replayStatisticsRepository.createQueryBuilder('stats') + const baseQuery = this.replayStatisticsRepository.createQueryBuilder('stats') .where('stats.workspace_id = :workspaceId', { workspaceId }); if (unitId) { - query.andWhere('stats.unit_id = :unitId', { unitId }); + baseQuery.andWhere('stats.unit_id = :unitId', { unitId }); } - const statistics = await query.getMany(); + const aggregateQuery = baseQuery.clone() + .select([ + 'MIN(stats.duration_milliseconds) as min', + 'MAX(stats.duration_milliseconds) as max', + 'AVG(stats.duration_milliseconds) as average', + 'COUNT(*) as count' + ]); - if (statistics.length === 0) { + const aggregateResult = await aggregateQuery.getRawOne(); + + if (!aggregateResult || aggregateResult.count === '0') { return { min: 0, max: 0, @@ -125,35 +142,55 @@ export class ReplayStatisticsService { }; } - // Calculate min, max, and average - const durations = statistics.map(stat => stat.duration_milliseconds); - const min = Math.min(...durations); - const max = Math.max(...durations); - const average = durations.reduce((sum, duration) => sum + duration, 0) / durations.length; + const min = parseInt(aggregateResult.min, 10); + const max = parseInt(aggregateResult.max, 10); + const average = parseFloat(aggregateResult.average); - // Create duration distribution (in 10-second buckets) const distribution: Record = {}; - durations.forEach(duration => { - const bucket = Math.floor(duration / 10) * 10; - const bucketKey = `${bucket}-${bucket + 10}`; - distribution[bucketKey] = (distribution[bucketKey] || 0) + 1; - }); + const chunkSize = 10000; // Process 10k records at a time + let offset = 0; + let hasMore = true; + + while (hasMore) { + const chunk = await baseQuery.clone() + .select(['stats.duration_milliseconds']) + .limit(chunkSize) + .offset(offset) + .getRawMany(); + + if (chunk.length === 0) { + hasMore = false; + break; + } + + chunk.forEach(row => { + const duration = parseInt(row.stats_duration_milliseconds, 10); + const bucket = Math.floor(duration / 10000) * 10000; // 10-second buckets in milliseconds + const bucketKey = `${bucket}-${bucket + 10000}`; + distribution[bucketKey] = (distribution[bucketKey] || 0) + 1; + }); + + offset += chunkSize; + if (chunk.length < chunkSize) { + hasMore = false; + } + } - // Calculate average duration per unit if not filtering by unit let unitAverages: Record | undefined; if (!unitId) { - unitAverages = {}; - const unitDurations: Record = {}; + const unitAveragesQuery = this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'stats.unit_id', + 'AVG(stats.duration_milliseconds) as average' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .groupBy('stats.unit_id'); - statistics.forEach(stat => { - if (!unitDurations[stat.unit_id]) { - unitDurations[stat.unit_id] = []; - } - unitDurations[stat.unit_id].push(stat.duration_milliseconds); - }); + const unitAveragesResult = await unitAveragesQuery.getRawMany(); - Object.entries(unitDurations).forEach(([unitKey, durationArray]) => { - unitAverages![unitKey] = durationArray.reduce((sum, duration) => sum + duration, 0) / durationArray.length; + unitAverages = {}; + unitAveragesResult.forEach(row => { + unitAverages![row.stats_unit_id] = parseFloat(row.average); }); } @@ -177,15 +214,22 @@ export class ReplayStatisticsService { */ async getReplayDistributionByDay(workspaceId: number): Promise> { try { - const statistics = await this.getReplayStatistics(workspaceId); - - // Group replays by day (YYYY-MM-DD format) - return statistics.reduce((acc, stat) => { - // Format the date as YYYY-MM-DD - const day = stat.timestamp.toISOString().split('T')[0]; - acc[day] = (acc[day] || 0) + 1; - return acc; - }, {} as Record); + const result = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'DATE(stats.timestamp) as day', + 'COUNT(*) as count' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .groupBy('DATE(stats.timestamp)') + .orderBy('day', 'ASC') + .getRawMany(); + + const distribution: Record = {}; + result.forEach(row => { + distribution[row.day] = parseInt(row.count, 10); + }); + + return distribution; } catch (error) { this.logger.error(`Error calculating replay distribution by day: ${error.message}`, error.stack); throw error; @@ -199,18 +243,23 @@ export class ReplayStatisticsService { */ async getReplayDistributionByHour(workspaceId: number): Promise> { try { - const statistics = await this.getReplayStatistics(workspaceId); - // Initialize all hours with 0 count const hourDistribution: Record = {}; for (let i = 0; i < 24; i++) { hourDistribution[i.toString()] = 0; } - // Count replays by hour - statistics.forEach(stat => { - const hour = stat.timestamp.getHours().toString(); - hourDistribution[hour] += 1; + const result = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'EXTRACT(HOUR FROM stats.timestamp) as hour', + 'COUNT(*) as count' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .groupBy('EXTRACT(HOUR FROM stats.timestamp)') + .getRawMany(); + + result.forEach(row => { + hourDistribution[row.hour.toString()] = parseInt(row.count, 10); }); return hourDistribution; @@ -233,9 +282,16 @@ export class ReplayStatisticsService { commonErrors: Array<{ message: string; count: number }>; }> { try { - const statistics = await this.getReplayStatistics(workspaceId); - - if (statistics.length === 0) { + const overallStats = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'COUNT(*) as total', + 'COUNT(CASE WHEN stats.success = true THEN 1 END) as successful', + 'COUNT(CASE WHEN stats.success = false THEN 1 END) as failed' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .getRawOne(); + + if (!overallStats || overallStats.total === '0') { return { successRate: 0, totalReplays: 0, @@ -245,24 +301,29 @@ export class ReplayStatisticsService { }; } - const totalReplays = statistics.length; - const successfulReplays = statistics.filter(stat => stat.success).length; - const failedReplays = totalReplays - successfulReplays; + const totalReplays = parseInt(overallStats.total, 10); + const successfulReplays = parseInt(overallStats.successful, 10); + const failedReplays = parseInt(overallStats.failed, 10); const successRate = (successfulReplays / totalReplays) * 100; - // Count occurrences of each error message - const errorCounts: Record = {}; - statistics.forEach(stat => { - if (!stat.success && stat.error_message) { - errorCounts[stat.error_message] = (errorCounts[stat.error_message] || 0) + 1; - } - }); - - // Convert to array and sort by count (descending) - const commonErrors = Object.entries(errorCounts) - .map(([message, count]) => ({ message, count })) - .sort((a, b) => b.count - a.count) - .slice(0, 10); // Get top 10 most common errors + const errorResult = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'stats.error_message as message', + 'COUNT(*) as count' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .andWhere('stats.success = false') + .andWhere('stats.error_message IS NOT NULL') + .andWhere('stats.error_message != \'\'') + .groupBy('stats.error_message') + .orderBy('count', 'DESC') + .limit(10) + .getRawMany(); + + const commonErrors = errorResult.map(row => ({ + message: row.message, + count: parseInt(row.count, 10) + })); return { successRate, @@ -284,17 +345,22 @@ export class ReplayStatisticsService { */ async getFailureDistributionByUnit(workspaceId: number): Promise> { try { - const statistics = await this.getReplayStatistics(workspaceId); + const result = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'stats.unit_id', + 'COUNT(*) as count' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .andWhere('stats.success = false') + .groupBy('stats.unit_id') + .getRawMany(); - // Filter for failed replays only - const failedReplays = statistics.filter(stat => !stat.success); + const distribution: Record = {}; + result.forEach(row => { + distribution[row.stats_unit_id] = parseInt(row.count, 10); + }); - // Group failures by unit - return failedReplays.reduce((acc, stat) => { - const unitId = stat.unit_id; - acc[unitId] = (acc[unitId] || 0) + 1; - return acc; - }, {} as Record); + return distribution; } catch (error) { this.logger.error(`Error calculating failure distribution by unit: ${error.message}`, error.stack); throw error; @@ -308,18 +374,23 @@ export class ReplayStatisticsService { */ async getFailureDistributionByDay(workspaceId: number): Promise> { try { - const statistics = await this.getReplayStatistics(workspaceId); - - // Filter for failed replays only - const failedReplays = statistics.filter(stat => !stat.success); - - // Group failures by day (YYYY-MM-DD format) - return failedReplays.reduce((acc, stat) => { - // Format the date as YYYY-MM-DD - const day = stat.timestamp.toISOString().split('T')[0]; - acc[day] = (acc[day] || 0) + 1; - return acc; - }, {} as Record); + const result = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'DATE(stats.timestamp) as day', + 'COUNT(*) as count' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .andWhere('stats.success = false') + .groupBy('DATE(stats.timestamp)') + .orderBy('day', 'ASC') + .getRawMany(); + + const distribution: Record = {}; + result.forEach(row => { + distribution[row.day] = parseInt(row.count, 10); + }); + + return distribution; } catch (error) { this.logger.error(`Error calculating failure distribution by day: ${error.message}`, error.stack); throw error; @@ -333,21 +404,24 @@ export class ReplayStatisticsService { */ async getFailureDistributionByHour(workspaceId: number): Promise> { try { - const statistics = await this.getReplayStatistics(workspaceId); - - // Filter for failed replays only - const failedReplays = statistics.filter(stat => !stat.success); - // Initialize all hours with 0 count const hourDistribution: Record = {}; for (let i = 0; i < 24; i++) { hourDistribution[i.toString()] = 0; } - // Count failures by hour - failedReplays.forEach(stat => { - const hour = stat.timestamp.getHours().toString(); - hourDistribution[hour] += 1; + const result = await this.replayStatisticsRepository.createQueryBuilder('stats') + .select([ + 'EXTRACT(HOUR FROM stats.timestamp) as hour', + 'COUNT(*) as count' + ]) + .where('stats.workspace_id = :workspaceId', { workspaceId }) + .andWhere('stats.success = false') + .groupBy('EXTRACT(HOUR FROM stats.timestamp)') + .getRawMany(); + + result.forEach(row => { + hourDistribution[row.hour.toString()] = parseInt(row.count, 10); }); return hourDistribution; diff --git a/apps/backend/src/app/database/services/shared-types.ts b/apps/backend/src/app/database/services/shared-types.ts index da7b2e314..78e93df7a 100644 --- a/apps/backend/src/app/database/services/shared-types.ts +++ b/apps/backend/src/app/database/services/shared-types.ts @@ -1,5 +1,4 @@ -// This file contains shared types used across multiple services -// to prevent circular dependencies +import { ResponseStatusType, ResponseValueType } from '@iqbspecs/response/response.interface'; export type Response = { groupname: string, @@ -7,9 +6,8 @@ export type Response = { code: string, bookletname: string, unitname: string, - originalUnitId: string - // eslint-disable-next-line @typescript-eslint/no-explicit-any - responses: any, + originalUnitId: string, + responses: string | Chunk[], laststate: string, }; @@ -92,9 +90,11 @@ export type TcMergeSubForms = { export type TcMergeResponse = { id: string, - ts: number, - content: string, - responseType: string + value: ResponseValueType, + status: ResponseStatusType + subform?: string; + code?: number; + score?: number; }; export type TcMergeLastState = { diff --git a/apps/backend/src/app/database/services/testcenter.service.ts b/apps/backend/src/app/database/services/testcenter.service.ts index 8a0df63a0..4a2a3d1d8 100755 --- a/apps/backend/src/app/database/services/testcenter.service.ts +++ b/apps/backend/src/app/database/services/testcenter.service.ts @@ -3,13 +3,12 @@ import { HttpService } from '@nestjs/axios'; import * as https from 'https'; import { catchError, firstValueFrom } from 'rxjs'; import { logger } from 'nx/src/utils/logger'; -import { Person, Response } from './shared-types'; -import { - ImportOptions -} from '../../../../../frontend/src/app/ws-admin/components/test-center-import/test-center-import.component'; +import { Person, Response, Log } from './shared-types'; + import { TestGroupsInfoDto } from '../../../../../../api-dto/files/test-groups-info.dto'; import { PersonService } from './person.service'; import { WorkspaceFilesService } from './workspace-files.service'; +import { ImportOptions } from '../../../../../frontend/src/app/services/import.service'; const agent = new https.Agent({ rejectUnauthorized: false @@ -36,16 +35,6 @@ type File = { data: string }; -export type Log = { - groupname:string, - loginname : string, - code : string, - bookletname : string, - unitname : string, - timestamp: number, - logentry : string, -}; - export type Result = { success: boolean, testFiles: number, @@ -54,7 +43,13 @@ export type Result = { booklets: number, units: number, persons: number, - importedGroups: string + importedGroups: string, + filesPlayer?: number, + filesUnits?: number, + filesDefinitions?: number, + filesCodings?: number, + filesBooklets?: number, + filesTestTakers?: number }; @Injectable() @@ -152,8 +147,9 @@ export class TestcenterService { logger.log('Import response data from TC'); const headersRequest = this.createHeaders(authToken); const chunks = this.createChunks(testGroups.split(','), 2); + const allRawResponses: Response[] = []; - return chunks.map(async chunk => { + for (const chunk of chunks) { const endpoint = url ? `${url}/api/workspace/${tc_workspace}/report/response?dataIds=${chunk.join(',')}` : `https://www.iqb-testcenter${server}.de/api/workspace/${tc_workspace}/report/response?dataIds=${chunk.join(',')}`; @@ -163,21 +159,29 @@ export class TestcenterService { httpsAgent: agent, headers: headersRequest }); + allRawResponses.push(...rawResponses); + } catch (error) { + logger.error('Error fetching response chunk:'); + throw error; + } + } - this.persons = await this.personService.createPersonList(rawResponses, Number(workspace_id)); + return [Promise.resolve().then(async () => { + try { + this.persons = await this.personService.createPersonList(allRawResponses, Number(workspace_id)); const personList = await Promise.all( this.persons.map(async person => { - const personWithBooklets = await this.personService.assignBookletsToPerson(person, rawResponses); - return this.personService.assignUnitsToBookletAndPerson(personWithBooklets, rawResponses); + const personWithBooklets = await this.personService.assignBookletsToPerson(person, allRawResponses); + return this.personService.assignUnitsToBookletAndPerson(personWithBooklets, allRawResponses); }) ); await this.personService.processPersonBooklets(personList, Number(workspace_id)); } catch (error) { - logger.error('Error processing response chunk:'); + logger.error('Error processing consolidated response data:'); throw error; } - }); + })]; } private async importLogs( @@ -192,20 +196,30 @@ export class TestcenterService { logger.log('Import logs data from TC'); const headersRequest = this.createHeaders(authToken); const logsChunks = this.createChunks(testGroups.split(','), 2); - const logsPromises = logsChunks.map(async chunk => { + const allLogData: Log[] = []; + for (const chunk of logsChunks) { const logsUrl = url ? `${url}/api/workspace/${tc_workspace}/report/log?dataIds=${chunk.join(',')}` : `https://iqb-testcenter${server}.de/api/workspace/${tc_workspace}/report/log?dataIds=${chunk.join(',')}`; + try { const { data: logData } = await this.httpService.axiosRef.get(logsUrl, { httpsAgent: agent, headers: headersRequest }); - const { bookletLogs, unitLogs } = this.separateLogsByType(logData); + allLogData.push(...logData); + } catch (error) { + logger.error(`Error fetching log chunk: ${error.message}`); + throw error; + } + } + + return [Promise.resolve().then(async () => { + try { + const { bookletLogs, unitLogs } = this.separateLogsByType(allLogData); - const persons = await this.personService.createPersonList(logData, Number(workspace_id)); + const persons = await this.personService.createPersonList(allLogData, Number(workspace_id)); - // Process logs with overwrite flag const result = await this.personService.processPersonLogs( persons, unitLogs, @@ -215,12 +229,10 @@ export class TestcenterService { logger.log(`Logs import result: ${JSON.stringify(result)}`); } catch (error) { - logger.error(`Error processing logs: ${error.message}`); + logger.error(`Error processing consolidated log data: ${error.message}`); throw error; } - }); - - return logsPromises; + })]; } private separateLogsByType(logData: Log[]): { bookletLogs: Log[], unitLogs: Log[] } { @@ -240,7 +252,7 @@ export class TestcenterService { url: string, authToken: string, importOptions: ImportOptions - ): Promise<{ success: boolean, testFiles: number }> { + ): Promise<{ success: boolean, testFiles: number, filesPlayer: number, filesUnits: number, filesDefinitions: number, filesCodings: number, filesBooklets: number, filesTestTakers: number }> { const headersRequest = this.createHeaders(authToken); const filesEndpoint = url ? `${url}/api/workspace/${tc_workspace}/files` : @@ -252,7 +264,30 @@ export class TestcenterService { headers: headersRequest }); - const filePromises = this.createFilePromises(files, importOptions); + const { + units, + definitions, + player, + codings, + testTakers, + booklets + } = importOptions; + + const playerArr: File[] = player === 'true' ? files.Resource.filter(f => f.name.includes('.html')) : []; + const unitsArr: File[] = units === 'true' ? files.Unit : []; + const definitionsArr: File[] = definitions === 'true' ? files.Resource.filter(f => f.name.includes('.voud')) : []; + const codingsArr: File[] = codings === 'true' ? files.Resource.filter(f => f.name.includes('.vocs')) : []; + const bookletsArr: File[] = booklets === 'true' ? files.Booklet as unknown as File[] : []; + const testTakersArr: File[] = testTakers === 'true' ? files.Testtakers as unknown as File[] : []; + + const filePromises: Promise[] = [ + ...playerArr.map(file => Promise.resolve(file)), + ...unitsArr.map(file => Promise.resolve(file)), + ...definitionsArr.map(file => Promise.resolve(file)), + ...codingsArr.map(file => Promise.resolve(file)), + ...bookletsArr.map(file => Promise.resolve(file)), + ...testTakersArr.map(file => Promise.resolve(file)) + ]; const fetchedFiles = await Promise.all(filePromises.map(async filePromise => { const file = await filePromise; @@ -264,45 +299,27 @@ export class TestcenterService { await this.workspaceFilesService.testCenterImport(dbEntries); return { success: fetchedFiles.length > 0, - testFiles: fetchedFiles.length + testFiles: fetchedFiles.length, + filesPlayer: playerArr.length, + filesUnits: unitsArr.length, + filesDefinitions: definitionsArr.length, + filesCodings: codingsArr.length, + filesBooklets: bookletsArr.length, + filesTestTakers: testTakersArr.length }; } catch (error) { logger.error('Error fetching files:'); - return { success: false, testFiles: 0 }; - } - } - - private createFilePromises(files: ServerFilesResponse, importOptions: ImportOptions): Promise[] { - const { - units, - definitions, - player, - codings, - testTakers, - booklets - } = importOptions; - const filePromises: Promise[] = []; - - if (player === 'true') { - filePromises.push(...files.Resource.filter(f => f.name.includes('.html')).map(file => Promise.resolve(file))); - } - if (units === 'true') { - filePromises.push(...files.Unit.map(file => Promise.resolve(file))); - } - if (definitions === 'true') { - filePromises.push(...files.Resource.filter(f => f.name.includes('.voud')).map(file => Promise.resolve(file))); - } - if (codings === 'true') { - filePromises.push(...files.Resource.filter(f => f.name.includes('.vocs')).map(file => Promise.resolve(file))); - } - if (booklets === 'true') { - filePromises.push(...files.Booklet.map(file => Promise.resolve(file))); - } - if (testTakers === 'true') { - filePromises.push(...files.Testtakers.map(file => Promise.resolve(file))); + return { + success: false, + testFiles: 0, + filesPlayer: 0, + filesUnits: 0, + filesDefinitions: 0, + filesCodings: 0, + filesBooklets: 0, + filesTestTakers: 0 + }; } - - return filePromises; } private createDatabaseEntries( @@ -382,9 +399,14 @@ export class TestcenterService { ); result.testFiles = filesResult.testFiles; result.success = filesResult.success; + result.filesPlayer = filesResult.filesPlayer; + result.filesUnits = filesResult.filesUnits; + result.filesDefinitions = filesResult.filesDefinitions; + result.filesCodings = filesResult.filesCodings; + result.filesBooklets = filesResult.filesBooklets; + result.filesTestTakers = filesResult.filesTestTakers; } - // Wait for all promises to complete await Promise.all(promises); result.success = true; return result; @@ -426,7 +448,7 @@ export class TestcenterService { try { const response = await this.httpService.axiosRef.get(requestUrl, { - httpsAgent: agent, // Disable SSL validation for HTTPS requests + httpsAgent: agent, headers: headersRequest }); diff --git a/apps/backend/src/app/database/services/workspace-coding.service.ts b/apps/backend/src/app/database/services/workspace-coding.service.ts index 54d582235..c1ea6dd0b 100644 --- a/apps/backend/src/app/database/services/workspace-coding.service.ts +++ b/apps/backend/src/app/database/services/workspace-coding.service.ts @@ -1,12 +1,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Like, Repository } from 'typeorm'; +import { CodingScheme } from '@iqbspecs/coding-scheme/coding-scheme.interface'; import * as Autocoder from '@iqb/responses'; import * as cheerio from 'cheerio'; import * as fastCsv from 'fast-csv'; import * as ExcelJS from 'exceljs'; import * as crypto from 'crypto'; -import { ResponseStatusType } from '@iqb/responses'; +import { ResponseStatusType } from '@iqbspecs/response/response.interface'; import { CacheService } from '../../cache/cache.service'; import FileUpload from '../entities/file_upload.entity'; import Persons from '../entities/persons.entity'; @@ -25,11 +26,52 @@ import { ValidationResultDto } from '../../../../../../api-dto/coding/validation import { ValidateCodingCompletenessResponseDto } from '../../../../../../api-dto/coding/validate-coding-completeness-response.dto'; import { JobQueueService } from '../../job-queue/job-queue.service'; +interface CoderTrainingResponse { + responseId: number; + unitAlias: string; + variableId: string; + unitName: string; + value: string; + personLogin: string; + personCode: string; + personGroup: string; + bookletName: string; + variable: string; // combination of variableId + unitAlias +} + +interface ExternalCodingRow { + unit_alias?: string; + variable_id?: string; + status?: string; + score?: string | number; + code?: string | number; + person_code?: string; + person_login?: string; + person_group?: string; + booklet_name?: string; + [key: string]: string | number | undefined; +} + +interface ExternalCodingImportBody { + file: string; // base64 encoded file data + fileName?: string; +} + +interface QueryParameters { + unitAlias: string; + variableId: string; + workspaceId: number; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; +} + interface CodedResponse { id: number; - code?: string; - codedstatus?: string; - score?: number; + code_v1?: string; + status_v1?: string; + score_v1?: number; } @Injectable() @@ -53,17 +95,12 @@ export class WorkspaceCodingService { private cacheService: CacheService ) {} - private codingSchemeCache: Map = new Map(); + private codingSchemeCache: Map = new Map(); private readonly SCHEME_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes cache TTL private testFileCache: Map; timestamp: number }> = new Map(); private readonly TEST_FILE_CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes cache TTL - /** - * Generate a hash for expected combinations to create unique cache keys - * @param expectedCombinations Array of expected combinations - * @returns Hash string for cache key generation - */ private generateExpectedCombinationsHash(expectedCombinations: ExpectedCombinationDto[]): string { const sortedData = expectedCombinations .map(combo => `${combo.unit_key}|${combo.login_name}|${combo.login_code}|${combo.booklet_id}|${combo.variable_id}`) @@ -79,62 +116,49 @@ export class WorkspaceCodingService { if (cacheEntry && (now - cacheEntry.timestamp) < this.TEST_FILE_CACHE_TTL_MS) { this.logger.log(`Using cached test files for workspace ${workspace_id}`); - - // Check if all requested unit aliases are in the cache const missingAliases = unitAliasesArray.filter(alias => !cacheEntry.files.has(alias)); - if (missingAliases.length === 0) { - // All files are in the cache, return the cached files return cacheEntry.files; } - // Some files are missing, fetch only the missing ones this.logger.log(`Fetching ${missingAliases.length} missing test files for workspace ${workspace_id}`); const missingFiles = await this.fileUploadRepository.find({ where: { workspace_id, file_id: In(missingAliases) }, select: ['file_id', 'data', 'filename'] }); - // Add the missing files to the cache missingFiles.forEach(file => { cacheEntry.files.set(file.file_id, file); }); - // Update the timestamp cacheEntry.timestamp = now; return cacheEntry.files; } - // No valid cache entry, fetch all files this.logger.log(`Fetching all test files for workspace ${workspace_id}`); const testFiles = await this.fileUploadRepository.find({ where: { workspace_id, file_id: In(unitAliasesArray) }, select: ['file_id', 'data', 'filename'] }); - // Create a new cache entry const fileMap = new Map(); testFiles.forEach(file => { fileMap.set(file.file_id, file); }); - // Store in cache this.testFileCache.set(workspace_id, { files: fileMap, timestamp: now }); - return fileMap; } - private async getCodingSchemesWithCache(codingSchemeRefs: string[]): Promise> { + private async getCodingSchemesWithCache(codingSchemeRefs: string[]): Promise> { const now = Date.now(); - const result = new Map(); - const emptyScheme = new Autocoder.CodingScheme({}); + const result = new Map(); + const emptyScheme = new CodingScheme({}); - // Check which schemes are in the cache and still valid const missingSchemeRefs = codingSchemeRefs.filter(ref => { const cacheEntry = this.codingSchemeCache.get(ref); if (cacheEntry && (now - cacheEntry.timestamp) < this.SCHEME_CACHE_TTL_MS) { - // Scheme is in cache and still valid result.set(ref, cacheEntry.scheme); return false; } @@ -142,32 +166,24 @@ export class WorkspaceCodingService { }); if (missingSchemeRefs.length === 0) { - // All schemes are in the cache this.logger.log('Using all cached coding schemes'); return result; } - // Fetch missing schemes this.logger.log(`Fetching ${missingSchemeRefs.length} missing coding schemes`); const codingSchemeFiles = await this.fileUploadRepository.find({ where: { file_id: In(missingSchemeRefs) }, select: ['file_id', 'data', 'filename'] }); - // Parse and cache the schemes codingSchemeFiles.forEach(file => { try { const data = typeof file.data === 'string' ? JSON.parse(file.data) : file.data; - const scheme = new Autocoder.CodingScheme(data); - - // Store in result map + const scheme = new CodingScheme(data); result.set(file.file_id, scheme); - - // Store in cache this.codingSchemeCache.set(file.file_id, { scheme, timestamp: now }); } catch (error) { this.logger.error(`--- Fehler beim Verarbeiten des Kodierschemas ${file.filename}: ${error.message}`); - // Use empty scheme for invalid schemes result.set(file.file_id, emptyScheme); } }); @@ -177,15 +193,11 @@ export class WorkspaceCodingService { private cleanupCaches(): void { const now = Date.now(); - - // Clean up coding scheme cache for (const [key, entry] of this.codingSchemeCache.entries()) { if (now - entry.timestamp > this.SCHEME_CACHE_TTL_MS) { this.codingSchemeCache.delete(key); } } - - // Clean up test file cache for (const [key, entry] of this.testFileCache.entries()) { if (now - entry.timestamp > this.TEST_FILE_CACHE_TTL_MS) { this.testFileCache.delete(key); @@ -202,11 +214,9 @@ export class WorkspaceCodingService { } if (bullJob) { - // Get job state and progress const state = await bullJob.getState(); const progress = await bullJob.progress() || 0; - // Map Bull job state to our job status let status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'paused'; switch (state) { case 'active': @@ -229,7 +239,6 @@ export class WorkspaceCodingService { status = 'pending'; } - // Get result from job return value if completed let result: CodingStatistics | undefined; let error: string | undefined; @@ -272,7 +281,6 @@ export class WorkspaceCodingService { return { success: false, message: `Job with ID ${jobId} not found` }; } - // Check if job can be cancelled const state = await bullJob.getState(); if (state === 'completed' || state === 'failed') { return { @@ -281,7 +289,6 @@ export class WorkspaceCodingService { }; } - // Cancel the job const result = await this.jobQueueService.cancelTestPersonCodingJob(jobId); if (result) { this.logger.log(`Job ${jobId} has been cancelled successfully`); @@ -301,7 +308,6 @@ export class WorkspaceCodingService { return { success: false, message: `Job with ID ${jobId} not found` }; } - // Delete the job const result = await this.jobQueueService.deleteTestPersonCodingJob(jobId); if (result) { this.logger.log(`Job ${jobId} has been deleted successfully`); @@ -316,22 +322,18 @@ export class WorkspaceCodingService { private async isJobCancelled(jobId: string | number): Promise { try { - // Check Redis queue const bullJob = await this.jobQueueService.getTestPersonCodingJob(jobId.toString()); if (bullJob) { - // Check if job is paused via our custom isPaused property if (bullJob.data.isPaused) { return true; } - - // Also check Bull's native state const state = await bullJob.getState(); return state === 'paused'; } return false; } catch (error) { this.logger.error(`Error checking job cancellation or pause: ${error.message}`, error.stack); - return false; // Assume not cancelled or paused on error + return false; } } @@ -361,7 +363,6 @@ export class WorkspaceCodingService { const batch = batches[index]; this.logger.log(`Starte Aktualisierung für Batch #${index + 1} (Größe: ${batch.length}).`); - // Check for cancellation or pause before updating batch if (jobId && await this.isJobCancelled(jobId)) { this.logger.log(`Job ${jobId} was cancelled or paused before updating batch #${index + 1}`); await queryRunner.rollbackTransaction(); @@ -375,9 +376,9 @@ export class WorkspaceCodingService { ResponseEntity, response.id, { - code: response.code, - codedstatus: response.codedstatus, - score: response.score + code_v1: response.code_v1, + status_v1: response.status_v1, + score_v1: response.score_v1 } )); @@ -386,21 +387,18 @@ export class WorkspaceCodingService { this.logger.log(`Batch #${index + 1} (Größe: ${batch.length}) erfolgreich aktualisiert.`); - // Update progress during batch updates if (progressCallback) { const batchProgress = 95 + (5 * ((index + 1) / batches.length)); progressCallback(Math.round(Math.min(batchProgress, 99))); // Cap at 99% until fully complete and round to integer } } catch (error) { this.logger.error(`Fehler beim Aktualisieren von Batch #${index + 1} (Größe: ${batch.length}):`, error.message); - // Rollback transaction on error await queryRunner.rollbackTransaction(); await queryRunner.release(); return false; } } - // Commit transaction if all updates were successful await queryRunner.commitTransaction(); this.logger.log(`${allCodedResponses.length} Responses wurden erfolgreich aktualisiert.`); @@ -408,18 +406,15 @@ export class WorkspaceCodingService { metrics.update = Date.now() - updateStart; } - // Always release the query runner await queryRunner.release(); return true; } catch (error) { this.logger.error('Fehler beim Aktualisieren der Responses:', error.message); - // Ensure transaction is rolled back on error try { await queryRunner.rollbackTransaction(); } catch (rollbackError) { this.logger.error('Fehler beim Rollback der Transaktion:', rollbackError.message); } - // Always release the query runner await queryRunner.release(); return false; } @@ -429,7 +424,7 @@ export class WorkspaceCodingService { units: Unit[], unitToResponsesMap: Map, unitToCodingSchemeRefMap: Map, - fileIdToCodingSchemeMap: Map, + fileIdToCodingSchemeMap: Map, allResponses: ResponseEntity[], statistics: CodingStatistics, jobId?: string, @@ -440,7 +435,7 @@ export class WorkspaceCodingService { allCodedResponses.length = allResponses.length; let responseIndex = 0; const batchSize = 50; - const emptyScheme = new Autocoder.CodingScheme({}); + const emptyScheme = new CodingScheme({}); for (let i = 0; i < units.length; i += batchSize) { const unitBatch = units.slice(i, i + batchSize); @@ -457,11 +452,11 @@ export class WorkspaceCodingService { emptyScheme; for (const response of responses) { - const codedResult = scheme.code([{ + const codedResult = Autocoder.CodingFactory.code({ id: response.variableid, value: response.value, status: response.status as ResponseStatusType - }]); + }, scheme.variableCodings[0]); const codedStatus = codedResult[0]?.status; if (!statistics.statusCounts[codedStatus]) { @@ -471,15 +466,14 @@ export class WorkspaceCodingService { allCodedResponses[responseIndex] = { id: response.id, - code: codedResult[0]?.code, - codedstatus: codedStatus, - score: codedResult[0]?.score + code_v1: codedResult?.code, + status_v1: codedStatus, + score_v1: codedResult?.score }; responseIndex += 1; } } - // Check for cancellation or pause during response processing if (jobId && await this.isJobCancelled(jobId)) { this.logger.log(`Job ${jobId} was cancelled or paused during response processing`); if (queryRunner) { @@ -491,7 +485,6 @@ export class WorkspaceCodingService { allCodedResponses.length = responseIndex; - // Report progress after processing if (progressCallback) { progressCallback(95); } @@ -503,11 +496,8 @@ export class WorkspaceCodingService { codingSchemeRefs: Set, jobId?: string, queryRunner?: import('typeorm').QueryRunner - ): Promise> { - // Use cache for coding schemes + ): Promise> { const fileIdToCodingSchemeMap = await this.getCodingSchemesWithCache([...codingSchemeRefs]); - - // Check for cancellation or pause if (jobId && await this.isJobCancelled(jobId)) { this.logger.log(`Job ${jobId} was cancelled or paused after getting coding scheme files`); if (queryRunner) { @@ -547,8 +537,6 @@ export class WorkspaceCodingService { this.logger.error(`--- Fehler beim Verarbeiten der Datei ${testFile.filename}: ${error.message}`); } } - - // Check for cancellation or pause during scheme extraction if (jobId && await this.isJobCancelled(jobId)) { this.logger.log(`Job ${jobId} was cancelled or paused during scheme extraction`); if (queryRunner) { @@ -567,7 +555,6 @@ export class WorkspaceCodingService { progressCallback?: (progress: number) => void, jobId?: string ): Promise { - // Clean up expired cache entries this.cleanupCaches(); const startTime = Date.now(); @@ -582,7 +569,6 @@ export class WorkspaceCodingService { progressCallback(0); } - // Check for cancellation or pause before starting work if (jobId && await this.isJobCancelled(jobId)) { this.logger.log(`Job ${jobId} was cancelled or paused before processing started`); return statistics; @@ -705,7 +691,7 @@ export class WorkspaceCodingService { // Step 5: Get responses - 50% progress const responseQueryStart = Date.now(); const allResponses = await this.responseRepository.find({ - where: { unitid: In(unitIdsArray), status: In(['VALUE_CHANGED']) }, + where: { unitid: In(unitIdsArray), status: In(['VALUE_CHANGED', 'DISPLAYED', 'NOT_REACHED']) }, select: ['id', 'unitid', 'variableid', 'value', 'status'] // Only select needed fields }); metrics.responseQuery = Date.now() - responseQueryStart; @@ -905,9 +891,6 @@ export class WorkspaceCodingService { return { totalResponses: 0, statusCounts: {} }; } - // Check if the input contains groups or person IDs - // If all items can be parsed as numbers, they are person IDs - // Otherwise, they are group names const areAllNumbers = groupsOrIds.every(item => !Number.isNaN(Number(item))); let personIds: string[] = []; @@ -1030,7 +1013,7 @@ export class WorkspaceCodingService { const responses = await this.responseRepository.find({ where: { unitid: In(unitIds), - codedstatus: In(['CODING_INCOMPLETE', 'INTENDED_INCOMPLETE', 'CODE_SELECTION_PENDING']) + status_v1: In(['CODING_INCOMPLETE', 'INTENDED_INCOMPLETE', 'CODE_SELECTION_PENDING']) } }); @@ -1088,7 +1071,7 @@ export class WorkspaceCodingService { .leftJoinAndSelect('unit.booklet', 'booklet') .leftJoinAndSelect('booklet.person', 'person') .leftJoinAndSelect('booklet.bookletinfo', 'bookletinfo') - .where('response.codedStatus = :status', { status: 'CODING_INCOMPLETE' }) + .where('response.status_v1 = :status', { status: 8 }) // CODING_INCOMPLETE = 8 .andWhere('person.workspace_id = :workspace_id', { workspace_id }) .skip((validPage - 1) * validLimit) .take(MAX_LIMIT) // Set a very high limit to fetch all items @@ -1171,7 +1154,7 @@ export class WorkspaceCodingService { .leftJoinAndSelect('unit.booklet', 'booklet') .leftJoinAndSelect('booklet.person', 'person') .leftJoinAndSelect('booklet.bookletinfo', 'bookletinfo') - .where('response.codedStatus = :status', { status: 'CODING_INCOMPLETE' }) + .where('response.status_v1 = :status', { status: 'CODING_INCOMPLETE' }) .andWhere('person.workspace_id = :workspace_id', { workspace_id }) .orderBy('response.id', 'ASC'); @@ -1272,7 +1255,7 @@ export class WorkspaceCodingService { try { const statusCountResults = await this.responseRepository.query(` SELECT - response.codedstatus as "statusValue", + response.status_v1 as "statusValue", COUNT(response.id) as count FROM response INNER JOIN unit ON response.unitid = unit.id @@ -1281,7 +1264,7 @@ export class WorkspaceCodingService { WHERE response.status = $1 AND person.workspace_id = $2 AND person.consider = $3 - GROUP BY response.codedstatus + GROUP BY response.status_v1 `, ['VALUE_CHANGED', workspace_id, true]); let totalResponses = 0; @@ -1366,17 +1349,14 @@ export class WorkspaceCodingService { try { this.logger.log(`Getting missings profiles for workspace ${workspaceId}`); - // Get the setting with key 'missings-profile-iqb-standard' const setting = await this.settingRepository.findOne({ where: { key: 'missings-profile-iqb-standard' } }); if (!setting) { - // If no profiles exist yet, create a default one const defaultProfiles = this.createDefaultMissingsProfiles(); await this.saveMissingsProfiles(defaultProfiles); - // Return just the labels return defaultProfiles.map(profile => ({ label: profile.label })); } @@ -1396,7 +1376,6 @@ export class WorkspaceCodingService { private async getMissingsProfileByLabel(label: string): Promise { try { - // Get the setting with key 'missings-profile-iqb-standard' const setting = await this.settingRepository.findOne({ where: { key: 'missings-profile-iqb-standard' } }); @@ -1405,7 +1384,6 @@ export class WorkspaceCodingService { return null; } - // Parse the profiles from the setting content try { const profiles: MissingsProfilesDto[] = JSON.parse(setting.content); const profile = profiles.find(p => p.label === label); @@ -1488,7 +1466,6 @@ export class WorkspaceCodingService { private async saveMissingsProfiles(profiles: MissingsProfilesDto[]): Promise { try { - // Create or update the setting let setting = await this.settingRepository.findOne({ where: { key: 'missings-profile-iqb-standard' } }); @@ -1510,7 +1487,6 @@ export class WorkspaceCodingService { try { this.logger.log(`Creating missings profile for workspace ${workspaceId}`); - // Get all existing profiles const setting = await this.settingRepository.findOne({ where: { key: 'missings-profile-iqb-standard' } }); @@ -1526,14 +1502,12 @@ export class WorkspaceCodingService { } } - // Check if a profile with the same label already exists const existingProfile = profiles.find(p => p.label === profile.label); if (existingProfile) { this.logger.error(`A missings profile with label '${profile.label}' already exists`); return null; } - // Add the new profile profiles.push(profile); // Save the updated profiles @@ -1966,7 +1940,7 @@ export class WorkspaceCodingService { // Step 2: Count total number of unique unit-variable-code combinations const countQuery = this.responseRepository.createQueryBuilder('response') - .select('COUNT(DISTINCT CONCAT(unit.name, response.variableid, response.code))', 'count') + .select('COUNT(DISTINCT CONCAT(unit.name, response.variableid, response.code_v1))', 'count') .leftJoin('response.unit', 'unit') .leftJoin('unit.booklet', 'booklet') .leftJoin('booklet.person', 'person') @@ -1990,9 +1964,9 @@ export class WorkspaceCodingService { const aggregationQuery = this.responseRepository.createQueryBuilder('response') .select('unit.name', 'unitId') .addSelect('response.variableid', 'variableId') - .addSelect('response.code', 'code') + .addSelect('response.code_v1', 'code_v1') .addSelect('COUNT(response.id)', 'occurrenceCount') - .addSelect('MAX(response.score)', 'score') // Use MAX as a sample score + .addSelect('MAX(response.score_v1)', 'score_V1') // Use MAX as a sample score .leftJoin('response.unit', 'unit') .leftJoin('unit.booklet', 'booklet') .leftJoin('booklet.person', 'person') @@ -2011,10 +1985,10 @@ export class WorkspaceCodingService { aggregationQuery .groupBy('unit.name') .addGroupBy('response.variableid') - .addGroupBy('response.code') + .addGroupBy('response.code_v1') .orderBy('unit.name', 'ASC') .addOrderBy('response.variableid', 'ASC') - .addOrderBy('response.code', 'ASC') + .addOrderBy('response.code_v1', 'ASC') .offset((page - 1) * limit) .limit(limit); @@ -2145,13 +2119,10 @@ export class WorkspaceCodingService { const occurrenceCount = parseInt(item.occurrenceCount, 10); const score = parseFloat(item.score) || 0; - // Get total count for this unit-variable combination const variableTotalCount = unitVariableCounts.get(unitId)?.get(variableId) || 0; - // Calculate relative occurrence const relativeOccurrence = variableTotalCount > 0 ? occurrenceCount / variableTotalCount : 0; - // Get coding scheme information let derivation = ''; let description = ''; const codingScheme = codingSchemeMap.get(unitId); @@ -2163,22 +2134,18 @@ export class WorkspaceCodingService { } } - // Skip items where derivation is BASE_NO_VALUE or empty if (derivation === 'BASE_NO_VALUE' || derivation === '') { continue; } - // Get sample info for replay URL const sampleInfo = sampleInfoMap.get(`${unitId}|${variableId}`); const loginName = sampleInfo?.loginName || ''; const loginCode = sampleInfo?.loginCode || ''; const bookletId = sampleInfo?.bookletId || ''; - // Generate replay URL const variablePage = '0'; const replayUrl = `${serverUrl}/#/replay/${loginName}@${loginCode}@${bookletId}/${unitId}/${variablePage}/${variableId}?auth=${authToken}`; - // Add to result result.push({ replayUrl, unitId, @@ -2193,7 +2160,6 @@ export class WorkspaceCodingService { }); } - // Apply derivation filter if provided if (derivationFilter && derivationFilter.trim() !== '') { const filteredResult = result.filter(item => item.derivation.toLowerCase().includes(derivationFilter.toLowerCase())); @@ -2205,7 +2171,7 @@ export class WorkspaceCodingService { return { data: filteredResult, - total: filteredCount, // Update total count to reflect filtered results + total: filteredCount, page, limit }; @@ -2226,19 +2192,12 @@ export class WorkspaceCodingService { } } - /** - * Export validation results as Excel with complete database content from Redis cache - * @param workspaceId Workspace ID - * @param cacheKey Cache key to retrieve complete validation results - * @returns Excel buffer with complete data - */ async exportValidationResultsAsExcel( workspaceId: number, cacheKey: string ): Promise { this.logger.log(`Exporting validation results as Excel for workspace ${workspaceId} using cache key ${cacheKey}`); - // Validate input parameters if (!cacheKey || typeof cacheKey !== 'string') { const errorMessage = 'Invalid cache key provided'; this.logger.error(`${errorMessage}: ${cacheKey}`); @@ -2246,26 +2205,20 @@ export class WorkspaceCodingService { } try { - // Retrieve complete validation results from cache this.logger.log(`Attempting to retrieve cached data with key: ${cacheKey}`); const cachedData = await this.cacheService.getCompleteValidationResults(cacheKey); if (!cachedData) { - const errorMessage = 'Validation results not found in cache. Please run validation again.'; this.logger.error(`No cached validation results found for cache key ${cacheKey}`); - // Additional logging to help debug cache issues this.logger.error('Cache key format: validation:{workspaceId}:{hash}'); this.logger.error(`Expected pattern: validation:${workspaceId}:*`); - throw new Error(errorMessage); } const validationResults = cachedData.results; this.logger.log(`Successfully retrieved ${validationResults.length} validation results from cache for export`); if (!validationResults || validationResults.length === 0) { - const errorMessage = 'No validation data available for export. Please run validation again.'; this.logger.error('Cached data exists but contains no validation results'); - throw new Error(errorMessage); } const workbook = new ExcelJS.Workbook(); @@ -2286,7 +2239,6 @@ export class WorkspaceCodingService { { header: 'Last Modified', key: 'last_modified', width: 20 } ]; - // Add header style worksheet.getRow(1).font = { bold: true }; worksheet.getRow(1).fill = { type: 'pattern', @@ -2386,14 +2338,6 @@ export class WorkspaceCodingService { } } - /** - * Validate completeness of coding responses with Redis caching and complete backend processing - * @param workspaceId Workspace ID - * @param expectedCombinations Expected combinations from Excel - * @param page Page number (1-based) - * @param pageSize Number of items per page - * @returns Validation results with pagination metadata - */ async validateCodingCompleteness( workspaceId: number, expectedCombinations: ExpectedCombinationDto[], @@ -2404,7 +2348,6 @@ export class WorkspaceCodingService { this.logger.log(`Validating coding completeness for workspace ${workspaceId} with ${expectedCombinations.length} expected combinations`); const startTime = Date.now(); - // Generate cache key based on workspace and combinations hash const combinationsHash = this.generateExpectedCombinationsHash(expectedCombinations); const cacheKey = this.cacheService.generateValidationCacheKey(workspaceId, combinationsHash); @@ -2426,21 +2369,17 @@ export class WorkspaceCodingService { }; } - // No cache found - process ALL combinations and cache the complete results this.logger.log(`No cached results found. Processing all ${expectedCombinations.length} combinations for workspace ${workspaceId}`); const allResults: ValidationResultDto[] = []; let totalMissingCount = 0; - // Process all combinations in batches to avoid overwhelming the database const batchSize = 100; for (let i = 0; i < expectedCombinations.length; i += batchSize) { const batch = expectedCombinations.slice(i, i + batchSize); this.logger.log(`Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(expectedCombinations.length / batchSize)}`); - // Process each combination in the batch for (const expected of batch) { - // Build a query to check if the response exists const responseExists = await this.responseRepository .createQueryBuilder('response') .innerJoin('response.unit', 'unit') @@ -2456,7 +2395,6 @@ export class WorkspaceCodingService { .andWhere('response.value != :empty', { empty: '' }) .getCount(); - // Add the result const status = responseExists > 0 ? 'EXISTS' : 'MISSING'; if (status === 'MISSING') { totalMissingCount += 1; @@ -2469,7 +2407,6 @@ export class WorkspaceCodingService { } } - // Cache the complete results const metadata = { total: expectedCombinations.length, missing: totalMissingCount, @@ -2484,7 +2421,6 @@ export class WorkspaceCodingService { this.logger.warn(`Failed to cache validation results for workspace ${workspaceId}`); } - // Now get the paginated results from the complete data cachedResults = await this.cacheService.getPaginatedValidationResults(cacheKey, page, pageSize); const endTime = Date.now(); @@ -2500,7 +2436,7 @@ export class WorkspaceCodingService { totalPages: cachedResults.metadata.totalPages, hasNextPage: cachedResults.metadata.hasNextPage, hasPreviousPage: cachedResults.metadata.hasPreviousPage, - cacheKey // Include cache key in response for subsequent requests + cacheKey }; } @@ -2537,7 +2473,7 @@ export class WorkspaceCodingService { .leftJoinAndSelect('response.unit', 'unit') .leftJoinAndSelect('unit.booklet', 'booklet') .leftJoinAndSelect('booklet.person', 'person') - .where('response.codedStatus = :status', { status: 'CODING_INCOMPLETE' }) + .where('response.status_v1 = :status', { status: 'CODING_INCOMPLETE' }) .andWhere('person.workspace_id = :workspace_id', { workspace_id: workspaceId }); if (unitName) { @@ -2570,4 +2506,443 @@ export class WorkspaceCodingService { throw new Error('Could not get CODING_INCOMPLETE variables. Please check the database connection.'); } } + + async importExternalCodingWithProgress( + workspaceId: number, + body: ExternalCodingImportBody, + progressCallback: (progress: number, message: string) => void + ): Promise<{ + message: string; + processedRows: number; + updatedRows: number; + errors: string[]; + affectedRows: Array<{ + unitAlias: string; + variableId: string; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; + originalCodedStatus: string; + originalCode: number | null; + originalScore: number | null; + updatedCodedStatus: string | null; + updatedCode: number | null; + updatedScore: number | null; + }>; + }> { + return this.importExternalCoding(workspaceId, body, progressCallback); + } + + async importExternalCoding( + workspaceId: number, + body: ExternalCodingImportBody, + progressCallback?: (progress: number, message: string) => void + ): Promise<{ + message: string; + processedRows: number; + updatedRows: number; + errors: string[]; + affectedRows: Array<{ + unitAlias: string; + variableId: string; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; + originalCodedStatus: string; + originalCode: number | null; + originalScore: number | null; + updatedCodedStatus: string | null; + updatedCode: number | null; + updatedScore: number | null; + }>; + }> { + try { + this.logger.log(`Starting external coding import for workspace ${workspaceId}`); + progressCallback?.(5, 'Starting external coding import...'); + + const fileData = body.file; // Assuming base64 encoded file data + const fileName = body.fileName || 'external-coding.csv'; + + let parsedData: ExternalCodingRow[] = []; + const errors: string[] = []; + + progressCallback?.(10, 'Parsing file...'); + + if (fileName.endsWith('.csv')) { + parsedData = await this.parseCSVFile(fileData); + } else if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) { + parsedData = await this.parseExcelFile(fileData); + } else { + this.logger.error(`Unsupported file format: ${fileName}. Please use CSV or Excel files.`); + return { + message: 'Unsupported file format. Please use CSV or Excel files.', + processedRows: 0, + updatedRows: 0, + errors: ['Unsupported file format. Please use CSV or Excel files.'], + affectedRows: [] + }; + } + + this.logger.log(`Parsed ${parsedData.length} rows from external coding file`); + progressCallback?.(20, `Parsed ${parsedData.length} rows from file`); + + let updatedRows = 0; + const processedRows = parsedData.length; + const affectedRows: Array<{ + unitAlias: string; + variableId: string; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; + originalCodedStatus: string; + originalCode: number | null; + originalScore: number | null; + updatedCodedStatus: string | null; + updatedCode: number | null; + updatedScore: number | null; + }> = []; + + // Process data in batches for better performance + const batchSize = 1000; + const totalBatches = Math.ceil(parsedData.length / batchSize); + + this.logger.log(`Processing ${parsedData.length} rows in ${totalBatches} batches of ${batchSize}`); + progressCallback?.(25, `Starting to process ${parsedData.length} rows in ${totalBatches} batches`); + + for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) { + const batchStart = batchIndex * batchSize; + const batchEnd = Math.min(batchStart + batchSize, parsedData.length); + const batch = parsedData.slice(batchStart, batchEnd); + + this.logger.log(`Processing batch ${batchIndex + 1}/${totalBatches} (rows ${batchStart + 1}-${batchEnd})`); + + // Calculate progress: 25% start + 70% for batch processing + const batchProgress = 25 + Math.floor(((batchIndex) / totalBatches) * 70); + progressCallback?.(batchProgress, `Processing batch ${batchIndex + 1}/${totalBatches} (rows ${batchStart + 1}-${batchEnd})`); + + for (const row of batch) { + try { + const { + unit_alias: unitAlias, variable_id: variableId, status, score, code, + person_code: personCode, person_login: personLogin, person_group: personGroup, booklet_name: bookletName + } = row; + + if (!unitAlias || !variableId) { + errors.push(`Row missing required fields: unit_alias=${unitAlias}, variable_id=${variableId}`); + continue; + } + + const queryBuilder = this.responseRepository + .createQueryBuilder('response') + .select(['response.id', 'response.status_v1', 'response.code_v1', 'response.score_v1', + 'response.status_v2', 'response.code_v2', 'response.score_v2', + 'unit.alias', 'person.code', 'person.login', 'person.group', 'bookletinfo.name']) + .innerJoin('response.unit', 'unit') + .innerJoin('unit.booklet', 'booklet') + .innerJoin('booklet.person', 'person') + .innerJoin('booklet.bookletinfo', 'bookletinfo') + .where('unit.alias = :unitAlias', { unitAlias }) + .andWhere('response.variableid = :variableId', { variableId }) + .andWhere('person.workspace_id = :workspaceId', { workspaceId }); + + if (personCode) { + queryBuilder.andWhere('person.code = :personCode', { personCode }); + } + if (personLogin) { + queryBuilder.andWhere('person.login = :personLogin', { personLogin }); + } + if (personGroup) { + queryBuilder.andWhere('person.group = :personGroup', { personGroup }); + } + if (bookletName) { + queryBuilder.andWhere('bookletinfo.name = :bookletName', { bookletName }); + } + + const queryParameters: QueryParameters = { + unitAlias, + variableId, + workspaceId + }; + + if (personCode) { + queryParameters.personCode = personCode; + } + if (personLogin) { + queryParameters.personLogin = personLogin; + } + if (personGroup) { + queryParameters.personGroup = personGroup; + } + if (bookletName) { + queryParameters.bookletName = bookletName; + } + + const responsesToUpdate = await queryBuilder.setParameters(queryParameters).getMany(); + + if (responsesToUpdate.length > 0) { + const responseIds = responsesToUpdate.map(r => r.id); + const updateResult = await this.responseRepository + .createQueryBuilder() + .update(ResponseEntity) + .set({ + status_v2: status || null, + code_v2: code ? parseInt(code.toString(), 10) : null, + score_v2: score ? parseInt(score.toString(), 10) : null + }) + .where('id IN (:...ids)', { ids: responseIds }) + .execute(); + + if (updateResult.affected && updateResult.affected > 0) { + updatedRows += updateResult.affected; + + // Add comparison data for each affected response + responsesToUpdate.forEach(response => { + affectedRows.push({ + unitAlias: response.unit?.alias || unitAlias, + variableId, + personCode: response.unit?.booklet?.person?.code || undefined, + personLogin: response.unit?.booklet?.person?.login || undefined, + personGroup: response.unit?.booklet?.person?.group || undefined, + bookletName: response.unit?.booklet?.bookletinfo?.name || undefined, + originalCodedStatus: response.status_v1, + originalCode: response.code_v1, + originalScore: response.score_v1, + updatedCodedStatus: status || null, + updatedCode: code ? parseInt(code.toString(), 10) : null, + updatedScore: score ? parseInt(score.toString(), 10) : null + }); + }); + } + } else { + const matchingCriteria = [`unit_alias=${unitAlias}`, `variable_id=${variableId}`]; + if (personCode) matchingCriteria.push(`person_code=${personCode}`); + if (personLogin) matchingCriteria.push(`person_login=${personLogin}`); + if (personGroup) matchingCriteria.push(`person_group=${personGroup}`); + if (bookletName) matchingCriteria.push(`booklet_name=${bookletName}`); + errors.push(`No response found for ${matchingCriteria.join(', ')}`); + } + } catch (rowError) { + errors.push(`Error processing row: ${rowError.message}`); + this.logger.error(`Error processing row: ${rowError.message}`, rowError.stack); + } + } + + // Small delay between batches to prevent overwhelming the database + if (batchIndex < totalBatches - 1) { + await new Promise(resolve => { + setTimeout(resolve, 10); + }); + } + } + + const message = `External coding import completed. Processed ${processedRows} rows, updated ${updatedRows} response records.`; + this.logger.log(message); + progressCallback?.(100, `Import completed: ${updatedRows} of ${processedRows} rows updated`); + + return { + message, + processedRows, + updatedRows, + errors, + affectedRows + }; + } catch (error) { + this.logger.error(`Error importing external coding: ${error.message}`, error.stack); + progressCallback?.(0, `Import failed: ${error.message}`); + throw new Error(`Could not import external coding data: ${error.message}`); + } + } + + private async parseCSVFile(fileData: string): Promise { + return new Promise((resolve, reject) => { + const results: ExternalCodingRow[] = []; + const buffer = Buffer.from(fileData, 'base64'); + let rowCount = 0; + + fastCsv.parseString(buffer.toString(), { headers: true }) + .on('error', error => reject(error)) + .on('data', row => { + if (Object.values(row).some(value => value && value.toString().trim() !== '')) { + results.push(row); + rowCount += 1; + + // Log progress for large files + if (rowCount % 10000 === 0) { + this.logger.log(`Parsed ${rowCount} rows...`); + } + + // Memory protection: limit to 200k rows to prevent memory overflow + if (rowCount > 200000) { + reject(new Error('File too large. Maximum 200,000 rows supported.')); + } + } + }) + .on('end', () => { + this.logger.log(`CSV parsing completed. Total rows: ${results.length}`); + resolve(results); + }); + }); + } + + private async parseExcelFile(fileData: string): Promise { + const buffer = Buffer.from(fileData, 'base64'); + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(buffer); + + const worksheet = workbook.getWorksheet(1); + if (!worksheet) { + throw new Error('No worksheet found in Excel file'); + } + + const results: ExternalCodingRow[] = []; + const headers: string[] = []; + + const headerRow = worksheet.getRow(1); + headerRow.eachCell((cell, colNumber) => { + headers[colNumber] = cell.text || cell.value?.toString() || ''; + }); + + this.logger.log(`Starting Excel parsing. Total rows: ${worksheet.rowCount - 1}`); + + // Memory protection: limit to 200k rows + const maxRows = Math.min(worksheet.rowCount, 200001); // +1 for header row + if (worksheet.rowCount > 200001) { + throw new Error('File too large. Maximum 200,000 rows supported.'); + } + + // Parse data rows + for (let rowNumber = 2; rowNumber <= maxRows; rowNumber++) { + const row = worksheet.getRow(rowNumber); + const rowData: ExternalCodingRow = {}; + + row.eachCell((cell, colNumber) => { + const header = headers[colNumber]; + if (header) { + rowData[header] = cell.text || cell.value?.toString() || ''; + } + }); + + // Only add non-empty rows + if (Object.values(rowData).some(value => value && value.toString().trim() !== '')) { + results.push(rowData); + } + + // Log progress for large files + if ((rowNumber - 1) % 10000 === 0) { + this.logger.log(`Parsed ${rowNumber - 1} rows...`); + } + } + + this.logger.log(`Excel parsing completed. Total rows: ${results.length}`); + return results; + } + + async generateCoderTrainingPackages( + workspaceId: number, + selectedCoders: { id: number; name: string }[], + variableConfigs: { variableName: string; sampleCount: number }[] + ): Promise<{ + coderId: number; + coderName: string; + responses: CoderTrainingResponse[]; + }[]> { + try { + this.logger.log(`Generating coder training packages for workspace ${workspaceId}`); + + const incompleteResponses = await this.responseRepository + .createQueryBuilder('response') + .innerJoin('response.unit', 'unit') + .innerJoin('unit.booklet', 'booklet') + .innerJoin('booklet.person', 'person') + .innerJoin('booklet.bookletinfo', 'bookletinfo') + .select([ + 'response.id', + 'response.variableid', + 'response.value', + 'unit.alias', + 'unit.name', + 'unit.booklet', + 'booklet.person', + 'person.login', + 'person.code', + 'person.group', + 'booklet.bookletinfo', + 'bookletinfo.name' + ]) + .where('response.status_v1 = :status', { status: 'CODING_INCOMPLETE' }) + .andWhere('person.workspace_id = :workspaceId', { workspaceId }) + .getMany(); + + this.logger.log(`Found ${JSON.stringify(incompleteResponses)} responses with CODING_INCOMPLETE status`); + + // Group responses by variable (combination of variableId + unitAlias) + const variableResponsesMap = new Map(); + + incompleteResponses.forEach(response => { + const variable = `${response.variableid}_${response.unit.alias}`; + if (!variableResponsesMap.has(variable)) { + variableResponsesMap.set(variable, []); + } + variableResponsesMap.get(variable)!.push(response); + }); + + this.logger.log(`Grouped responses into ${variableResponsesMap.size} unique variables`); + + const trainingPackages = selectedCoders.map(coder => { + const coderResponses: CoderTrainingResponse[] = []; + variableConfigs.forEach(config => { + const matchingVariables = Array.from(variableResponsesMap.keys()).filter(variable => variable.toLowerCase().includes(config.variableName.toLowerCase()) || + config.variableName.toLowerCase().includes(variable.toLowerCase()) + ); + + matchingVariables.forEach(variable => { + const responses = variableResponsesMap.get(variable) || []; + const sampledResponses = this.sampleResponses(responses, config.sampleCount); + + sampledResponses.forEach(response => { + const person = response.unit?.booklet?.person; + const bookletInfo = response.unit?.booklet?.bookletinfo; + + coderResponses.push({ + responseId: response.id, + unitAlias: response.unit?.alias || '', + variableId: response.variableid, + unitName: response.unit?.name || '', + value: response.value || '', + personLogin: person?.login || '', + personCode: person?.code || '', + personGroup: person?.group || '', + bookletName: bookletInfo?.name || '', + variable: variable + }); + }); + }); + }); + return { + coderId: coder.id, + coderName: coder.name, + responses: coderResponses + }; + }); + + this.logger.log(`Generated training packages for ${selectedCoders.length} coders`); + return trainingPackages; + } catch (error) { + this.logger.error(`Error generating coder training packages: ${error.message}`, error.stack); + throw new Error(`Could not generate coder training packages: ${error.message}`); + } + } + + private sampleResponses(responses: T[], sampleCount: number): T[] { + if (responses.length <= sampleCount) { + return [...responses]; + } + + // Shuffle array and take first sampleCount items + const shuffled = [...responses].sort(() => Math.random() - 0.5); + return shuffled.slice(0, sampleCount); + } } diff --git a/apps/backend/src/app/database/services/workspace-files.service.ts b/apps/backend/src/app/database/services/workspace-files.service.ts index 6a8457ad0..9b8840651 100644 --- a/apps/backend/src/app/database/services/workspace-files.service.ts +++ b/apps/backend/src/app/database/services/workspace-files.service.ts @@ -2,12 +2,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Like, Repository } from 'typeorm'; import * as cheerio from 'cheerio'; +import { Element } from 'domhandler'; import AdmZip = require('adm-zip'); import * as fs from 'fs'; import * as path from 'path'; import * as libxmljs from 'libxmljs2'; import { parseStringPromise } from 'xml2js'; -import { VariableInfo } from '@iqb/responses'; +import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; import FileUpload, { StructuredFileData } from '../entities/file_upload.entity'; import { FilesDto } from '../../../../../../api-dto/files/files.dto'; import { FileIo } from '../../admin/workspace/file-io.interface'; @@ -134,7 +135,6 @@ export class WorkspaceFilesService { } if (fileSize) { - // fileSize-Filter: z.B. '0-10KB', '10KB-100KB', '100KB-1MB', '1MB-10MB', '10MB+' const KB = 1024; const MB = 1024 * KB; // eslint-disable-next-line default-case @@ -146,10 +146,10 @@ export class WorkspaceFilesService { qb = qb.andWhere('file.file_size >= :min AND file.file_size < :max', { min: 10 * KB, max: 100 * KB }); break; case '100KB-1MB': - qb = qb.andWhere('file.file_size >= :min AND file.file_size < :max', { min: 100 * KB, max: 1 * MB }); + qb = qb.andWhere('file.file_size >= :min AND file.file_size < :max', { min: 100 * KB, max: MB }); break; case '1MB-10MB': - qb = qb.andWhere('file.file_size >= :min AND file.file_size < :max', { min: 1 * MB, max: 10 * MB }); + qb = qb.andWhere('file.file_size >= :min AND file.file_size < :max', { min: MB, max: 10 * MB }); break; case '10MB+': qb = qb.andWhere('file.file_size >= :min', { min: 10 * MB }); @@ -180,7 +180,11 @@ export class WorkspaceFilesService { async deleteTestFiles(workspace_id: number, fileIds: string[]): Promise { this.logger.log(`Delete test files for workspace ${workspace_id}`); - const res = await this.fileUploadRepository.delete(fileIds); + const numericIds = fileIds.map(id => parseInt(id, 10)).filter(id => !Number.isNaN(id)); + const res = await this.fileUploadRepository.delete({ + id: In(numericIds), + workspace_id: workspace_id + }); return !!res; } @@ -508,10 +512,7 @@ ${bookletRefs} unitsWithoutPlayer } = await this.checkMissingUnits(Array.from(uniqueBooklets)); - // If booklets are incomplete, all other categories should also be marked as incomplete const bookletComplete = allBookletsExist; - - // If units are incomplete, coding schemes, definitions, and player should also be marked as incomplete const unitComplete = bookletComplete && allUnitsExist; return { @@ -549,10 +550,8 @@ ${bookletRefs} } private extractXmlData( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - bookletTags: cheerio.Cheerio, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - unitTags: cheerio.Cheerio + bookletTags: cheerio.Cheerio, + unitTags: cheerio.Cheerio ): { uniqueBooklets: Set; uniqueUnits: Set; @@ -571,8 +570,9 @@ ${bookletRefs} unitTags.each((_, unit) => { const $ = cheerio.load(unit); + const unitElements = $('unit'); - $('unit').each((__, codingScheme) => { + unitElements.each((__, codingScheme) => { const value = $(codingScheme).text().trim(); if (value) codingSchemeRefs.push(value); }); @@ -582,7 +582,7 @@ ${bookletRefs} if (value) definitionRefs.push(value); }); - const unitId = $('unit').attr('id'); + const unitId = unitElements.attr('id'); if (unitId) { uniqueUnits.add(unitId.trim()); } @@ -985,7 +985,6 @@ ${bookletRefs} this.logger.log(`Extracted information from ${fileType} file: ${JSON.stringify(extractedInfo)}`); } catch (extractError) { this.logger.error(`Error extracting information from ${fileType} file: ${extractError.message}`); - // Continue with upload even if extraction fails } const structuredData: StructuredFileData = { @@ -999,7 +998,7 @@ ${bookletRefs} file_size: file.size, data: file.buffer.toString(), file_id: resolvedFileId, - structured_data: structuredData // Store extracted information in the structured_data column + structured_data: structuredData }, ['file_id', 'workspace_id']); } catch (error) { this.logger.error(`Error processing XML file: ${error.message}`); @@ -1023,10 +1022,9 @@ ${bookletRefs} metadata }; - // Check if this is a schemer HTML file if (metadata['@type'] === 'schemer') { const resourceFileId = WorkspaceFilesService.getSchemerId(file); - const result = await this.fileUploadRepository.upsert({ + return await this.fileUploadRepository.upsert({ filename: file.originalname, workspace_id: workspaceId, file_type: 'Schemer', @@ -1035,12 +1033,10 @@ ${bookletRefs} data: file.buffer.toString(), structured_data: structuredData }, ['file_id', 'workspace_id']); - return result; } - // Handle as player HTML file const resourceFileId = WorkspaceFilesService.getPlayerId(file); - const result = await this.fileUploadRepository.upsert({ + return await this.fileUploadRepository.upsert({ filename: file.originalname, workspace_id: workspaceId, file_type: 'Resource', @@ -1049,11 +1045,9 @@ ${bookletRefs} data: file.buffer.toString(), structured_data: structuredData }, ['file_id', 'workspace_id']); - return result; } catch (error) { - // If there's an error parsing the metadata, handle as a regular resource const resourceFileId = WorkspaceFilesService.getResourceId(file); - const result = await this.fileUploadRepository.upsert({ + return this.fileUploadRepository.upsert({ filename: file.originalname, workspace_id: workspaceId, file_type: 'Resource', @@ -1062,7 +1056,6 @@ ${bookletRefs} data: file.buffer.toString(), structured_data: { metadata: {} } }, ['file_id', 'workspace_id']); - return result; } } diff --git a/apps/backend/src/app/database/services/workspace-test-results.service.ts b/apps/backend/src/app/database/services/workspace-test-results.service.ts index b70a7dff2..4461b58a8 100644 --- a/apps/backend/src/app/database/services/workspace-test-results.service.ts +++ b/apps/backend/src/app/database/services/workspace-test-results.service.ts @@ -103,9 +103,9 @@ export class WorkspaceTestResultsService { 'response.status', 'response.value', 'response.subform', - 'response.code', - 'response.score', - 'response.codedstatus' + 'response.code_v1', + 'response.score_v1', + 'response.status_v1' ]) .getMany(); @@ -140,7 +140,6 @@ export class WorkspaceTestResultsService { .select(['unitLog.id', 'unitLog.unitid', 'unitLog.ts', 'unitLog.key', 'unitLog.parameter']) .getMany(); - // Group logs by unit ID for faster lookup const unitLogsMap = new Map(); unitLogs.forEach(log => { if (!unitLogsMap.has(log.unitid)) { @@ -207,7 +206,7 @@ export class WorkspaceTestResultsService { unitsMap.get(unit.bookletid)?.push(unit); }); - const result = booklets.map(booklet => ({ + return booklets.map(booklet => ({ id: booklet.id, personid: booklet.personid, name: booklet.bookletinfo.name, @@ -224,8 +223,6 @@ export class WorkspaceTestResultsService { tags: unitTagsMap.get(unit.id) || [] })) })); - - return result; } catch (error) { this.logger.error( `Failed to fetch booklets, bookletInfo, units, and results for personId: ${personId} and workspaceId: ${workspaceId}`, @@ -267,16 +264,13 @@ export class WorkspaceTestResultsService { ); } - // Add pagination queryBuilder .skip((validPage - 1) * validLimit) .take(validLimit) .orderBy('person.code', 'ASC'); const [results, total] = await queryBuilder.getManyAndCount(); - const result: [Persons[], number] = [results, total]; - - return result; + return [results, total]; } catch (error) { this.logger.error(`Failed to fetch test results for workspace_id ${workspace_id}: ${error.message}`, error.stack); throw new Error('An error occurred while fetching test results'); @@ -325,7 +319,6 @@ export class WorkspaceTestResultsService { this.logger.log(`Cache miss for responses: workspace=${workspaceId}, testPerson=${connector}, unitId=${unitId}`); - // If not in cache, fetch from database const [login, code, bookletId] = connector.split('@'); const queryBuilder = this.unitRepository.createQueryBuilder('unit') .innerJoinAndSelect('unit.responses', 'response') @@ -342,7 +335,6 @@ export class WorkspaceTestResultsService { const unit = await queryBuilder.getOne(); if (!unit) { - // If no unit found, we need to determine which part of the query failed const person = await this.personsRepository.findOne({ where: { code, login, workspace_id: workspaceId, consider: true @@ -384,7 +376,6 @@ export class WorkspaceTestResultsService { try { value = JSON.parse(value); } catch (e) { - // If parsing fails, keep the original value this.logger.warn(`Failed to parse JSON array: ${value}`); } } else if (value.startsWith('{') && value.endsWith('}')) { @@ -412,7 +403,6 @@ export class WorkspaceTestResultsService { responsesBySubform[subformKey].push(mappedResponse); }); - // Create responses array with unique responses for each subform const responsesArray = Object.keys(responsesBySubform).map(subform => { const uniqueResponses = responsesBySubform[subform].filter( (response, index, self) => index === self.findIndex(r => r.id === response.id) @@ -428,7 +418,6 @@ export class WorkspaceTestResultsService { responses: responsesArray }; - // Cache the result await this.cacheService.set(cacheKey, result); this.logger.log(`Cached responses for: workspace=${workspaceId}, testPerson=${connector}, unitId=${unitId}`); @@ -449,9 +438,9 @@ export class WorkspaceTestResultsService { .orderBy('response.id', 'ASC'); if (status === 'null') { - queryBuilder.andWhere('response.codedStatus IS NULL'); + queryBuilder.andWhere('response.status_v1 IS NULL'); } else { - queryBuilder.andWhere('response.codedStatus = :statusParam', { statusParam: status }); + queryBuilder.andWhere('response.status_v1 = :statusParam', { statusParam: status }); } let result: [ResponseEntity[], number]; @@ -459,8 +448,8 @@ export class WorkspaceTestResultsService { if (options) { const { page, limit } = options; const MAX_LIMIT = 500; - const validPage = Math.max(1, page); // minimum 1 - const validLimit = Math.min(Math.max(1, limit), MAX_LIMIT); // Between 1 and MAX_LIMIT + const validPage = Math.max(1, page); + const validLimit = Math.min(Math.max(1, limit), MAX_LIMIT); queryBuilder .skip((validPage - 1) * validLimit) @@ -469,7 +458,6 @@ export class WorkspaceTestResultsService { result = await queryBuilder.getManyAndCount(); this.logger.log(`Found ${result[0].length} responses with status ${status} (page ${validPage}, limit ${validLimit}, total ${result[1]}) for workspace ${workspace_id}`); } else { - // For non-paginated queries, still use getManyAndCount to avoid multiple queries result = await queryBuilder.getManyAndCount(); this.logger.log(`Found ${result[0].length} responses with status ${status} for workspace ${workspace_id}`); } @@ -483,7 +471,8 @@ export class WorkspaceTestResultsService { async deleteTestPersons( workspaceId: number, - testPersonIds: string + testPersonIds: string, + userId: string ): Promise<{ success: boolean; report: { @@ -533,7 +522,7 @@ export class WorkspaceTestResultsService { for (const person of existingPersons) { try { await this.journalService.createEntry( - 'system', + userId, workspaceId, 'delete', 'test-person', @@ -559,7 +548,8 @@ export class WorkspaceTestResultsService { async deleteUnit( workspaceId: number, - unitId: number + unitId: number, + userId: string ): Promise<{ success: boolean; report: { @@ -599,7 +589,7 @@ export class WorkspaceTestResultsService { try { await this.journalService.createEntry( - 'system', // userId + userId, workspaceId, 'delete', 'unit', @@ -628,7 +618,8 @@ export class WorkspaceTestResultsService { async deleteResponse( workspaceId: number, - responseId: number + responseId: number, + userId: string ): Promise<{ success: boolean; report: { @@ -669,7 +660,7 @@ export class WorkspaceTestResultsService { try { await this.journalService.createEntry( - 'system', // userId + userId, workspaceId, 'delete', 'response', @@ -700,7 +691,8 @@ export class WorkspaceTestResultsService { async deleteBooklet( workspaceId: number, - bookletId: number + bookletId: number, + userId: string ): Promise<{ success: boolean; report: { @@ -717,6 +709,7 @@ export class WorkspaceTestResultsService { const booklet = await manager .createQueryBuilder(Booklet, 'booklet') .leftJoinAndSelect('booklet.person', 'person') + .leftJoinAndSelect('booklet.bookletinfo', 'bookletinfo') .where('booklet.id = :bookletId', { bookletId }) .andWhere('person.workspace_id = :workspaceId', { workspaceId }) .getOne(); @@ -728,7 +721,6 @@ export class WorkspaceTestResultsService { return { success: false, report }; } - // Delete the booklet (cascade will delete associated units and responses) await manager .createQueryBuilder() .delete() @@ -740,13 +732,14 @@ export class WorkspaceTestResultsService { try { await this.journalService.createEntry( - 'system', // userId + userId, workspaceId, 'delete', 'booklet', bookletId, { bookletId, + bookletName: booklet.bookletinfo?.name || 'Unknown', personId: booklet.personid, personLogin: booklet.person?.login || 'Unknown', personCode: booklet.person?.code, @@ -828,7 +821,7 @@ export class WorkspaceTestResultsService { } if (searchParams.codedStatus) { - query.andWhere('response.codedstatus = :codedStatus', { codedStatus: searchParams.codedStatus }); + query.andWhere('response.status_v1 = :codedStatus', { codedStatus: searchParams.codedStatus }); } if (searchParams.group) { @@ -857,9 +850,9 @@ export class WorkspaceTestResultsService { variableId: response.variableid, value: response.value || '', status: response.status, - code: response.code, - score: response.score, - codedStatus: response.codedstatus, + code: response.code_v1, + score: response.score_v1, + codedStatus: response.status_v1, unitId: response.unit.id, unitName: response.unit.name, unitAlias: response.unit.alias, @@ -871,9 +864,7 @@ export class WorkspaceTestResultsService { personGroup: response.unit.booklet.person.group })); - const result = { data, total }; - - return result; + return { data, total }; } catch (error) { this.logger.error( `Failed to search for responses in workspace: ${workspaceId}`, @@ -918,7 +909,6 @@ export class WorkspaceTestResultsService { `Searching for units with name: ${unitName} in workspace: ${workspaceId} (page: ${page}, limit: ${limit})` ); - // Create a query to find all units with the given name const query = this.unitRepository.createQueryBuilder('unit') .innerJoinAndSelect('unit.booklet', 'booklet') .innerJoinAndSelect('booklet.person', 'person') @@ -965,9 +955,9 @@ export class WorkspaceTestResultsService { variableId: response.variableid, value: response.value || '', status: response.status, - code: response.code, - score: response.score, - codedStatus: response.codedstatus + code: response.code_v1, + score: response.score_v1, + codedStatus: response.status_v1 })) : [] })); @@ -980,9 +970,7 @@ export class WorkspaceTestResultsService { }); data = Array.from(uniqueMap.values()); - const result = { data, total: data.length }; - - return result; + return { data, total: data.length }; } catch (error) { this.logger.error( `Failed to search for units with name: ${unitName} in workspace: ${workspaceId}`, @@ -1027,7 +1015,6 @@ export class WorkspaceTestResultsService { `Searching for booklets with name: ${bookletName} in workspace: ${workspaceId} (page: ${page}, limit: ${limit})` ); - // Create a query to find all booklets with the given name const query = this.bookletRepository.createQueryBuilder('booklet') .innerJoinAndSelect('booklet.person', 'person') .innerJoinAndSelect('booklet.bookletinfo', 'bookletinfo') diff --git a/apps/backend/src/app/workspace/workspace-settings.controller.ts b/apps/backend/src/app/workspace/workspace-settings.controller.ts new file mode 100644 index 000000000..68f28a666 --- /dev/null +++ b/apps/backend/src/app/workspace/workspace-settings.controller.ts @@ -0,0 +1,120 @@ +import { + Controller, Get, Post, Put, Delete, Param, Body, ParseIntPipe +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Setting } from '../database/entities/setting.entity'; + +@Controller('api/workspace/:workspaceId/settings') +export class WorkspaceSettingsController { + constructor( + @InjectRepository(Setting) + private settingRepository: Repository + ) {} + + @Get(':key') + async getWorkspaceSetting( + @Param('workspaceId', ParseIntPipe) workspaceId: number, + @Param('key') key: string + ) { + const settingKey = `workspace-${workspaceId}-${key}`; + const setting = await this.settingRepository.findOne({ + where: { key: settingKey } + }); + + if (!setting) { + // Return default for auto-fetch-coding-statistics if not found + if (key === 'auto-fetch-coding-statistics') { + return { + id: 0, + key: settingKey, + value: JSON.stringify({ enabled: true }), + description: 'Controls whether coding statistics are automatically fetched in the coding management component' + }; + } + throw new Error(`Setting ${key} not found for workspace ${workspaceId}`); + } + + return { + id: setting.key, // Using key as id since it's the primary key + key: setting.key, + value: setting.content, + description: `Workspace setting: ${key}` + }; + } + + @Post() + async createWorkspaceSetting( + @Param('workspaceId', ParseIntPipe) workspaceId: number, + @Body() createSettingDto: { key: string; value: string; description?: string } + ) { + const settingKey = `workspace-${workspaceId}-${createSettingDto.key}`; + + // Check if setting already exists, update if it does + const existingSetting = await this.settingRepository.findOne({ + where: { key: settingKey } + }); + + if (existingSetting) { + existingSetting.content = createSettingDto.value; + const updated = await this.settingRepository.save(existingSetting); + return { + id: updated.key, + key: updated.key, + value: updated.content, + description: createSettingDto.description + }; + } + + // Create new setting + const newSetting = this.settingRepository.create({ + key: settingKey, + content: createSettingDto.value + }); + + const saved = await this.settingRepository.save(newSetting); + return { + id: saved.key, + key: saved.key, + value: saved.content, + description: createSettingDto.description + }; + } + + @Put(':settingId') + async updateWorkspaceSetting( + @Param('workspaceId', ParseIntPipe) workspaceId: number, + @Param('settingId') settingId: string, + @Body() updateSettingDto: { value: string } + ) { + const setting = await this.settingRepository.findOne({ + where: { key: settingId } + }); + + if (!setting) { + throw new Error(`Setting ${settingId} not found`); + } + + setting.content = updateSettingDto.value; + const updated = await this.settingRepository.save(setting); + + return { + id: updated.key, + key: updated.key, + value: updated.content, + description: `Workspace setting for workspace ${workspaceId}` + }; + } + + @Delete(':settingId') + async deleteWorkspaceSetting( + @Param('workspaceId', ParseIntPipe) workspaceId: number, + @Param('settingId') settingId: string + ) { + const result = await this.settingRepository.delete({ key: settingId }); + if (result.affected === 0) { + throw new Error(`Setting ${settingId} not found`); + } + return { message: 'Setting deleted successfully' }; + } +} diff --git a/apps/backend/src/app/wsg-admin/coding-job/coding-job.module.ts b/apps/backend/src/app/wsg-admin/coding-job/coding-job.module.ts index 6f62dfa9b..96a1a5f51 100644 --- a/apps/backend/src/app/wsg-admin/coding-job/coding-job.module.ts +++ b/apps/backend/src/app/wsg-admin/coding-job/coding-job.module.ts @@ -7,6 +7,8 @@ import { CodingJobCoder } from '../../database/entities/coding-job-coder.entity' import { CodingJobVariable } from '../../database/entities/coding-job-variable.entity'; import { CodingJobVariableBundle } from '../../database/entities/coding-job-variable-bundle.entity'; import { VariableBundle } from '../../database/entities/variable-bundle.entity'; +import { ResponseEntity } from '../../database/entities/response.entity'; +import { Unit } from '../../database/entities/unit.entity'; import { AuthModule } from '../../auth/auth.module'; @Module({ @@ -16,7 +18,9 @@ import { AuthModule } from '../../auth/auth.module'; CodingJobCoder, CodingJobVariable, CodingJobVariableBundle, - VariableBundle + VariableBundle, + ResponseEntity, + Unit ]), AuthModule ], diff --git a/apps/backend/src/app/wsg-admin/wsg-admin.module.ts b/apps/backend/src/app/wsg-admin/wsg-admin.module.ts index 3f827a7b1..d789a7a26 100644 --- a/apps/backend/src/app/wsg-admin/wsg-admin.module.ts +++ b/apps/backend/src/app/wsg-admin/wsg-admin.module.ts @@ -1,9 +1,15 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { WsgCodingJobModule } from './coding-job/coding-job.module'; +import { WorkspaceSettingsController } from '../workspace/workspace-settings.controller'; +import { Setting } from '../database/entities/setting.entity'; @Module({ - imports: [WsgCodingJobModule], - controllers: [], + imports: [ + WsgCodingJobModule, + TypeOrmModule.forFeature([Setting]) + ], + controllers: [WorkspaceSettingsController], providers: [], exports: [] }) diff --git a/apps/frontend/src/app/coding/components/coder-training/coder-training.component.html b/apps/frontend/src/app/coding/components/coder-training/coder-training.component.html new file mode 100644 index 000000000..1bdbe5fbb --- /dev/null +++ b/apps/frontend/src/app/coding/components/coder-training/coder-training.component.html @@ -0,0 +1,169 @@ +
+
+

+ school + Kodierer-Schulung konfigurieren +

+ +
+ +
+ +
+
+

Kodierer auswählen

+
+ + +
+
+ +
+
+
+ + +
+
{{coder.displayName || coder.name}}
+
{{coder.email}}
+
+ Zugewiesene Jobs: {{coder.assignedJobs.length}} +
+
+
+
+ + +
+ person_off +

Keine Kodierer gefunden

+
+
+
+ +
+

{{selectedCoders.size}} Kodierer ausgewählt

+
+
+ + +
+
+

+ Variablen-Konfiguration + + + Lade verfügbare Variablen... + + + ({{availableVariables.length}} verfügbare Variablen) + +

+ +
+ +
+
+
+ + + Variable auswählen + + + {{variable.unitName}} - {{variable.variableId}} + + + Keine Variablen verfügbar + + + + + + Anzahl Stichproben + + + + +
+
+
+
+ + +
+ + + +
+ + +
+
+

Schulungs-Übersicht

+
    +
  • Ausgewählte Kodierer: {{selectedCoders.size}}
  • +
  • Konfigurierte Variablen: {{variablesFormArray.length}}
  • +
  • + Gesamte Stichproben: + {{getTotalSamples()}} +
  • +
+
+
+
+ + +
+ +

Lade Kodierer...

+
+
+
diff --git a/apps/frontend/src/app/coding/components/coder-training/coder-training.component.scss b/apps/frontend/src/app/coding/components/coder-training/coder-training.component.scss new file mode 100644 index 000000000..7e8483945 --- /dev/null +++ b/apps/frontend/src/app/coding/components/coder-training/coder-training.component.scss @@ -0,0 +1,278 @@ +.coder-training-container { + padding: 24px; + max-width: 1200px; + margin: 0 auto; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.training-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid #e0e0e0; + + .section-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 24px; + font-weight: 500; + color: #333; + + mat-icon { + color: #1976d2; + } + } + + .close-button { + color: #666; + } +} + +.training-content { + display: flex; + flex-direction: column; + gap: 32px; +} + +.section { + .section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + + .section-subtitle { + margin: 0; + font-size: 18px; + font-weight: 500; + color: #333; + } + + .selection-controls { + display: flex; + gap: 8px; + } + } +} + +// Coder Selection Styles +.coder-selection { + .coder-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 12px; + margin-bottom: 16px; + } + + .coder-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 16px; + border: 2px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + border-color: #1976d2; + background-color: #f8f9fa; + } + + &.selected { + border-color: #1976d2; + background-color: #e3f2fd; + } + + .coder-info { + flex: 1; + + .coder-name { + font-weight: 500; + font-size: 16px; + color: #333; + margin-bottom: 4px; + } + + .coder-email { + font-size: 14px; + color: #666; + margin-bottom: 4px; + } + + .coder-jobs { + font-size: 12px; + color: #888; + font-style: italic; + } + } + } + + .no-coders-message { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + color: #666; + text-align: center; + + mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + margin-bottom: 16px; + color: #ccc; + } + } +} + +.selection-summary { + padding: 12px 16px; + background-color: #e8f5e8; + border-radius: 6px; + border-left: 4px solid #4caf50; + + p { + margin: 0; + color: #2e7d32; + } +} + +// Variable Configuration Styles +.variable-config { + .variable-item { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 16px; + padding: 16px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #fafafa; + + .variable-name-field { + flex: 2; + } + + .sample-count-field { + flex: 1; + min-width: 150px; + } + + .remove-variable-button { + margin-top: 8px; + } + } +} + +// Action Buttons +.action-buttons { + display: flex; + gap: 16px; + justify-content: flex-end; + padding-top: 24px; + border-top: 1px solid #e0e0e0; + + .start-training-button { + min-width: 180px; + } +} + +// Training Summary +.training-summary { + .summary-card { + padding: 20px; + background-color: #f5f5f5; + border-radius: 8px; + border-left: 4px solid #1976d2; + + h4 { + margin: 0 0 16px 0; + color: #333; + font-weight: 500; + } + + ul { + margin: 0; + padding-left: 20px; + list-style-type: disc; + + li { + margin-bottom: 8px; + color: #555; + + &:last-child { + margin-bottom: 0; + } + + strong { + color: #1976d2; + } + } + } + } +} + +// Loading State +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px; + color: #666; + + p { + margin-top: 16px; + font-size: 16px; + } +} + +// Responsive Design +@media (max-width: 768px) { + .coder-training-container { + padding: 16px; + } + + .training-header { + .section-title { + font-size: 20px; + } + } + + .section-header { + flex-direction: column; + align-items: flex-start; + gap: 12px; + } + + .coder-selection .coder-list { + grid-template-columns: 1fr; + } + + .variable-config .variable-item { + flex-direction: column; + align-items: stretch; + + .remove-variable-button { + align-self: flex-end; + margin-top: 12px; + } + } + + .action-buttons { + flex-direction: column-reverse; + + .start-training-button, + .cancel-button { + width: 100%; + } + } +} diff --git a/apps/frontend/src/app/coding/components/coder-training/coder-training.component.ts b/apps/frontend/src/app/coding/components/coder-training/coder-training.component.ts new file mode 100644 index 000000000..42b243c68 --- /dev/null +++ b/apps/frontend/src/app/coding/components/coder-training/coder-training.component.ts @@ -0,0 +1,234 @@ +import { + Component, + OnInit, + OnDestroy, + inject, + Output, + EventEmitter +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { MatSelect, MatOption } from '@angular/material/select'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { + FormBuilder, + FormGroup, + ReactiveFormsModule, + Validators, + FormArray +} from '@angular/forms'; +import { Subject, takeUntil } from 'rxjs'; +import { CoderService } from '../../services/coder.service'; +import { Coder } from '../../models/coder.model'; +import { BackendService } from '../../../services/backend.service'; +import { WorkspaceService } from '../../../services/workspace.service'; +import { AppService } from '../../../services/app.service'; + +export interface VariableConfig { + variableName: string; + sampleCount: number; +} + +@Component({ + selector: 'coding-box-coder-training', + standalone: true, + imports: [ + CommonModule, + TranslateModule, + MatButton, + MatIcon, + MatCheckbox, + MatFormField, + MatLabel, + MatInput, + MatSelect, + MatOption, + MatProgressSpinner, + ReactiveFormsModule, + MatIconButton + ], + templateUrl: './coder-training.component.html', + styleUrls: ['./coder-training.component.scss'] +}) +export class CoderTrainingComponent implements OnInit, OnDestroy { + @Output() close = new EventEmitter(); + @Output() startTraining = new EventEmitter<{ selectedCoders: Coder[], variableConfigs: VariableConfig[] }>(); + + private destroy$ = new Subject(); + private snackBar = inject(MatSnackBar); + private coderService = inject(CoderService); + private backendService = inject(BackendService); + private appService = inject(AppService); + private fb = inject(FormBuilder); + + coders: Coder[] = []; + selectedCoders: Set = new Set(); + availableVariables: { unitName: string; variableId: string }[] = []; + isLoading = false; + isLoadingVariables = false; + + trainingForm: FormGroup; + + constructor() { + this.trainingForm = this.fb.group({ + variables: this.fb.array([]) + }); + } + + ngOnInit(): void { + this.loadCoders(); + this.loadAvailableVariables(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + get variablesFormArray(): FormArray { + return this.trainingForm.get('variables') as FormArray; + } + + private loadAvailableVariables(): void { + this.isLoadingVariables = true; + const workspaceId = this.appService.selectedWorkspaceId; + + if (!workspaceId) { + this.showError('Kein Arbeitsbereich ausgewählt'); + this.isLoadingVariables = false; + return; + } + + this.backendService.getCodingIncompleteVariables(workspaceId) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: variables => { + this.availableVariables = variables; + this.isLoadingVariables = false; + + // Add a default variable entry if no variables are available + if (variables.length === 0) { + this.addVariable(); + } + }, + error: error => { + console.error('Error loading available variables:', error); + this.showError('Fehler beim Laden der verfügbaren Variablen'); + this.isLoadingVariables = false; + // Add a default variable entry on error + this.addVariable(); + } + }); + } + + addVariable(variableName: string = ''): void { + const variableGroup = this.fb.group({ + variableName: [variableName, [Validators.required]], + sampleCount: [10, [Validators.required, Validators.min(1), Validators.max(1000)]] + }); + + this.variablesFormArray.push(variableGroup); + } + + removeVariable(index: number): void { + if (this.variablesFormArray.length > 1) { + this.variablesFormArray.removeAt(index); + } else { + this.showError('Mindestens eine Variable ist erforderlich'); + } + } + + loadCoders(): void { + this.isLoading = true; + this.coderService.getCoders() + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (coders: Coder[]) => { + this.coders = coders; + this.isLoading = false; + }, + error: () => { + this.showError('Fehler beim Laden der Kodierer'); + this.isLoading = false; + } + }); + } + + toggleCoderSelection(coder: Coder): void { + if (this.selectedCoders.has(coder.id)) { + this.selectedCoders.delete(coder.id); + } else { + this.selectedCoders.add(coder.id); + } + } + + isCoderSelected(coder: Coder): boolean { + return this.selectedCoders.has(coder.id); + } + + selectAllCoders(): void { + this.coders.forEach(coder => this.selectedCoders.add(coder.id)); + } + + deselectAllCoders(): void { + this.selectedCoders.clear(); + } + + getSelectedCoders(): Coder[] { + return this.coders.filter(coder => this.selectedCoders.has(coder.id)); + } + + canStartTraining(): boolean { + return this.selectedCoders.size > 0 && + this.trainingForm.valid && + this.variablesFormArray.length > 0; + } + + getTotalSamples(): number { + return this.variablesFormArray.controls.reduce((total, control) => total + (control.get('sampleCount')?.value || 0), 0); + } + + trackByCoderId(index: number, coder: Coder): number { + return coder.id; + } + + onStartTraining(): void { + if (!this.canStartTraining()) { + this.showError('Bitte wählen Sie mindestens einen Kodierer aus und konfigurieren Sie die Variablen'); + return; + } + + const selectedCoders = this.getSelectedCoders(); + const variableConfigs: VariableConfig[] = this.variablesFormArray.controls.map(control => ({ + variableName: control.get('variableName')?.value || '', + sampleCount: control.get('sampleCount')?.value || 10 + })); + + this.startTraining.emit({ selectedCoders, variableConfigs }); + this.showSuccess(`Schulung für ${selectedCoders.length} Kodierer gestartet`); + } + + onClose(): void { + this.close.emit(); + } + + private showError(message: string): void { + this.snackBar.open(message, 'Schließen', { + duration: 5000, + panelClass: ['error-snackbar'] + }); + } + + private showSuccess(message: string): void { + this.snackBar.open(message, 'Schließen', { + duration: 3000, + panelClass: ['success-snackbar'] + }); + } +} diff --git a/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.html b/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.html index 2277cc9ae..ba2330f98 100644 --- a/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.html +++ b/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.html @@ -139,7 +139,12 @@
Ausgewählte Variablenbündel
person
-

{{ coder.displayName || coder.name }}

+

+ {{ coder.displayName || coder.name }} + @if (data.isEdit && isCoderOriginallyAssigned(coder)) { + check_circle + } +

{{ coder.name }}

@if (coder.email) {

{{ coder.email }}

@@ -170,7 +175,7 @@

Keine Kodierer verfügbar

Aufgaben-ID Filter - @@ -178,20 +183,21 @@

Keine Kodierer verfügbar

Variablen-ID Filter - - - +
@@ -239,16 +245,6 @@

Keine Kodierer verfügbar

} - - - } @if (!isLoadingVariableAnalysis && variables.length === 0) { @@ -266,23 +262,24 @@

Keine Variablen verfügbar

Name Filter - - - + @if (isLoadingBundles) {
diff --git a/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.ts b/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.ts index 833f9594c..bd4ba45cc 100644 --- a/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.ts +++ b/apps/frontend/src/app/coding/components/coding-job-dialog/coding-job-dialog.component.ts @@ -24,12 +24,13 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { TranslateModule } from '@ngx-translate/core'; import { SelectionModel } from '@angular/cdk/collections'; import { MatTooltip } from '@angular/material/tooltip'; +import { forkJoin } from 'rxjs'; import { CodingJob, VariableBundle, Variable } from '../../models/coding-job.model'; import { Coder } from '../../models/coder.model'; import { BackendService } from '../../../services/backend.service'; import { AppService } from '../../../services/app.service'; import { CoderService } from '../../services/coder.service'; -import { VariableAnalysisItem } from '../../models/variable-analysis-item.model'; +import { CodingJobService } from '../../services/coding-job.service'; export interface CodingJobDialogData { codingJob?: CodingJob; @@ -70,6 +71,7 @@ export class CodingJobDialogComponent implements OnInit { private appService = inject(AppService); private coderService = inject(CoderService); private snackBar = inject(MatSnackBar); + private codingJobService = inject(CodingJobService); codingJobForm!: FormGroup; isLoading = false; @@ -95,7 +97,6 @@ export class CodingJobDialogComponent implements OnInit { isLoadingBundles = false; // Variable analysis items - variableAnalysisItems: VariableAnalysisItem[] = []; isLoadingVariableAnalysis = false; totalVariableAnalysisRecords = 0; variableAnalysisPageIndex = 0; @@ -117,16 +118,22 @@ export class CodingJobDialogComponent implements OnInit { this.loadCodingIncompleteVariables(); this.loadVariableBundles(); this.loadAvailableCoders(); - console.log(this.data); - // Load coders if we're in edit mode and have a job ID if (this.data.isEdit && this.data.codingJob?.id) { this.loadCoders(this.data.codingJob.id); } + + this.dataSource.filterPredicate = (row, filter: string): boolean => { + try { + const { unitName, variableId } = JSON.parse(filter || '{}'); + const unitMatch = unitName ? row.unitName?.toLowerCase().includes(String(unitName).toLowerCase()) : true; + const varMatch = variableId ? row.variableId?.toLowerCase().includes(String(variableId).toLowerCase()) : true; + return unitMatch && varMatch; + } catch { + return true; + } + }; } - /** - * Loads all available coders in the workspace for selection - */ loadAvailableCoders(): void { this.isLoadingAvailableCoders = true; @@ -134,6 +141,19 @@ export class CodingJobDialogComponent implements OnInit { next: coders => { this.availableCoders = coders; this.isLoadingAvailableCoders = false; + let assignedIds: number[] = []; + if (this.data.isEdit && this.data.codingJob?.assignedCoders) { + assignedIds = this.data.codingJob.assignedCoders; + } else if (this.coders && this.coders.length > 0) { + assignedIds = this.coders.map(c => c.id); + } + + if (assignedIds.length > 0) { + const preSelectedCoders = this.availableCoders.filter(c => assignedIds.includes(c.id)); + this.selectedCoders = new SelectionModel(true, preSelectedCoders); + } else { + this.selectedCoders = new SelectionModel(true, []); + } }, error: () => { this.isLoadingAvailableCoders = false; @@ -152,8 +172,9 @@ export class CodingJobDialogComponent implements OnInit { next: coders => { this.coders = coders; this.isLoadingCoders = false; - // Pre-select the assigned coders - this.selectedCoders = new SelectionModel(true, coders); + const assignedIds = coders.map(c => c.id); + const preSelectedCoders = this.availableCoders.filter(c => assignedIds.includes(c.id)); + this.selectedCoders = new SelectionModel(true, preSelectedCoders); }, error: () => { this.isLoadingCoders = false; @@ -168,14 +189,10 @@ export class CodingJobDialogComponent implements OnInit { status: [this.data.codingJob?.status || 'pending', Validators.required] }); - if (this.data.codingJob?.variables) { - this.variables = [...this.data.codingJob.variables]; - this.dataSource.data = this.variables; - this.selectedVariables = new SelectionModel(true, [...this.variables]); - } + const originallyAssigned = this.data.codingJob?.assignedVariables ?? this.data.codingJob?.variables; - if (this.data.codingJob?.variableBundles) { - this.selectedVariableBundles = new SelectionModel(true, [...this.data.codingJob.variableBundles]); + if (originallyAssigned && originallyAssigned.length > 0) { + this.selectedVariables = new SelectionModel(true, [...originallyAssigned]); } } @@ -195,13 +212,29 @@ export class CodingJobDialogComponent implements OnInit { next: variables => { this.variables = variables; this.dataSource.data = this.variables; - if (this.data.codingJob?.variables) { - this.data.codingJob.variables.forEach(variable => { - const foundVariable = this.variables.find( - b => b.unitName === variable.unitName && b.variableId === variable.variableId - ); - if (foundVariable) { - this.selectedVariables.select(foundVariable); + const originallyAssigned = this.data.codingJob?.assignedVariables ?? this.data.codingJob?.variables; + if (originallyAssigned && originallyAssigned.length > 0) { + const makeKey = (u?: string | null, v?: string | null) => `${(u || '').trim().toLowerCase()}::${(v || '').trim().toLowerCase()}`; + + const toKey = (obj: unknown): string => { + if (obj && typeof obj === 'object') { + const rec = obj as Record; + const unitNameVal = rec.unitName; + const varIdCandidate = rec.variableId ?? rec.variableid ?? rec.variableID; + const unitName = typeof unitNameVal === 'string' ? unitNameVal : ''; + const variableId = typeof varIdCandidate === 'string' ? varIdCandidate : ''; + return makeKey(unitName, variableId); + } + return makeKey('', ''); + }; + + const assignedKeySet = new Set(originallyAssigned.map(toKey)); + + this.selectedVariables.clear(); + this.variables.forEach(rowVar => { + const rowKey = makeKey(rowVar.unitName ?? '', rowVar.variableId ?? ''); + if (assignedKeySet.has(rowKey)) { + this.selectedVariables.select(rowVar); } }); } @@ -217,8 +250,6 @@ export class CodingJobDialogComponent implements OnInit { loadVariableBundles(): void { this.isLoadingBundles = true; - - // Get the current workspace ID from the app service const workspaceId = this.appService.selectedWorkspaceId; if (workspaceId) { @@ -237,13 +268,12 @@ export class CodingJobDialogComponent implements OnInit { } } - onPageChange(): void { - // Pagination not needed for CODING_INCOMPLETE variables - // This method can be removed or kept for future use - } - applyFilter(): void { this.loadCodingIncompleteVariables(this.unitNameFilter); + this.dataSource.filter = JSON.stringify({ + unitName: this.unitNameFilter || '', + variableId: this.variableIdFilter || '' + }); } applyBundleFilter(): void { @@ -257,6 +287,7 @@ export class CodingJobDialogComponent implements OnInit { clearFilters(): void { this.unitNameFilter = ''; this.variableIdFilter = ''; + this.dataSource.filter = ''; this.loadCodingIncompleteVariables(); } @@ -265,24 +296,18 @@ export class CodingJobDialogComponent implements OnInit { this.bundlesDataSource.filter = ''; } - /** Whether the number of selected bundle matches the total number of rows. */ - isAllBundlesSelected(): boolean { - const numSelected = this.selectedVariableBundles.selected.length; - const numRows = this.bundlesDataSource.data.length; - return numSelected === numRows; - } - /** * Check if a variable was originally assigned to this coding job * @param variable The variable to check * @returns true if the variable was originally assigned to this job */ isVariableOriginallyAssigned(variable: Variable): boolean { - if (!this.data.codingJob?.variables) { + const originallyAssigned = this.data.codingJob?.assignedVariables ?? this.data.codingJob?.variables; + if (!originallyAssigned) { return false; } - return this.data.codingJob.variables.some( + return originallyAssigned.some( originalVar => originalVar.unitName === variable.unitName && originalVar.variableId === variable.variableId ); } @@ -302,6 +327,14 @@ export class CodingJobDialogComponent implements OnInit { ); } + isCoderOriginallyAssigned(coder: Coder): boolean { + if (!this.data.codingJob?.assignedCoders) { + return false; + } + + return this.data.codingJob.assignedCoders.includes(coder.id); + } + /** Gets the number of variables in a bundle */ getVariableCount(bundle: VariableBundle): number { return bundle.variables.length; @@ -353,37 +386,72 @@ export class CodingJobDialogComponent implements OnInit { return; } + const selectedCoderIds = this.selectedCoders.selected.map(c => c.id); + const codingJob: CodingJob = { id: this.data.codingJob?.id || 0, ...this.codingJobForm.value, createdAt: this.data.codingJob?.createdAt || new Date(), updatedAt: new Date(), - assignedCoders: this.selectedCoders.selected.map(coder => coder.id), + assignedCoders: selectedCoderIds, variables: this.selectedVariables.selected, variableBundles: this.selectedVariableBundles.selected, assignedVariables: this.selectedVariables.selected, assignedVariableBundles: this.selectedVariableBundles.selected }; - // If we're editing an existing coding job if (this.data.isEdit && this.data.codingJob?.id) { this.backendService.updateCodingJob(workspaceId, this.data.codingJob.id, codingJob).subscribe({ next: updatedJob => { - this.isSaving = false; - this.snackBar.open('Coding job updated successfully', 'Close', { duration: 3000 }); - this.dialogRef.close(updatedJob); + if (updatedJob?.id && selectedCoderIds.length > 0) { + const assignCalls = selectedCoderIds.map(id => this.codingJobService.assignCoder(updatedJob.id!, id)); + forkJoin(assignCalls).subscribe({ + next: results => { + const lastJob = results.filter(Boolean).pop() || { ...updatedJob, assignedCoders: selectedCoderIds }; + this.isSaving = false; + this.snackBar.open('Coding job updated successfully', 'Close', { duration: 3000 }); + this.dialogRef.close(lastJob); + }, + error: () => { + this.isSaving = false; + this.snackBar.open('Coding job updated, but assigning coders failed', 'Close', { duration: 5000 }); + this.dialogRef.close({ ...updatedJob, assignedCoders: selectedCoderIds }); + } + }); + } else { + this.isSaving = false; + this.snackBar.open('Coding job updated successfully', 'Close', { duration: 3000 }); + this.dialogRef.close(updatedJob); + } }, error: error => { this.isSaving = false; this.snackBar.open(`Error updating coding job: ${error.message}`, 'Close', { duration: 5000 }); } }); - } else { // If we're creating a new coding job + } else { this.backendService.createCodingJob(workspaceId, codingJob).subscribe({ next: createdJob => { - this.isSaving = false; - this.snackBar.open('Coding job created successfully', 'Close', { duration: 3000 }); - this.dialogRef.close(createdJob); + if (createdJob?.id && selectedCoderIds.length > 0) { + const assignCalls = selectedCoderIds.map(id => this.codingJobService.assignCoder(createdJob.id!, id)); + forkJoin(assignCalls).subscribe({ + next: results => { + const lastJob = results.filter(Boolean).pop() || { ...createdJob, assignedCoders: selectedCoderIds }; + this.isSaving = false; + this.snackBar.open('Coding job created successfully', 'Close', { duration: 3000 }); + this.dialogRef.close(lastJob); + }, + error: () => { + this.isSaving = false; + this.snackBar.open('Job created, but assigning coders failed', 'Close', { duration: 5000 }); + this.dialogRef.close({ ...createdJob, assignedCoders: selectedCoderIds }); + } + }); + } else { + this.isSaving = false; + this.snackBar.open('Coding job created successfully', 'Close', { duration: 3000 }); + this.dialogRef.close(createdJob); + } }, error: error => { this.isSaving = false; diff --git a/apps/frontend/src/app/coding/components/coding-jobs/coding-jobs.component.ts b/apps/frontend/src/app/coding/components/coding-jobs/coding-jobs.component.ts index d66d3a8f3..8b6ba3ef0 100755 --- a/apps/frontend/src/app/coding/components/coding-jobs/coding-jobs.component.ts +++ b/apps/frontend/src/app/coding/components/coding-jobs/coding-jobs.component.ts @@ -27,6 +27,8 @@ import { SearchFilterComponent } from '../../../shared/search-filter/search-filt import { CodingJob, Variable, VariableBundle } from '../../models/coding-job.model'; import { CodingJobDialogComponent } from '../coding-job-dialog/coding-job-dialog.component'; import { ConfirmDialogComponent } from '../../../shared/confirm-dialog/confirm-dialog.component'; +import { Coder } from '../../models/coder.model'; +import { CoderService } from '../../services/coder.service'; @Component({ selector: 'coding-box-coding-jobs', @@ -63,8 +65,10 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { backendService = inject(BackendService); private snackBar = inject(MatSnackBar); private dialog = inject(MatDialog); + private coderService = inject(CoderService); private coderNamesByJobId = new Map(); + private allCoders: Coder[] = []; private jobDetailsCache = new Map(); @@ -76,6 +80,11 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { @ViewChild(MatSort) sort!: MatSort; ngOnInit(): void { + this.coderService.getCoders().subscribe(coders => { + this.allCoders = coders; + this.updateCoderNamesMap(this.dataSource.data); + }); + this.loadCodingJobs(); } @@ -95,21 +104,16 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { this.backendService.getCodingJobs(workspaceId).subscribe({ next: response => { this.coderNamesByJobId.clear(); - const processedData = response.data.map(job => { - if (job.assignedCoders && job.assignedCoders.length > 0) { - this.coderNamesByJobId.set(job.id, `${job.assignedCoders.length} Kodierer`); - } else { - this.coderNamesByJobId.set(job.id, 'Keine'); - } - - return { - ...job, - createdAt: job.createdAt ? new Date(job.createdAt) : new Date(), - updatedAt: job.updatedAt ? new Date(job.updatedAt) : new Date() - }; - }); + const processedData = response.data.map(job => ({ + ...job, + createdAt: job.createdAt ? new Date(job.createdAt) : new Date(), + updatedAt: job.updatedAt ? new Date(job.updatedAt) : new Date() + })); this.dataSource.data = processedData; + // Namen der Codierer für die Liste aktualisieren + this.updateCoderNamesMap(processedData); + this.jobDetailsCache.clear(); this.isLoading = false; }, @@ -159,6 +163,15 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { if (job.assignedVariables && job.assignedVariables.length > 0) { return this.formatAssignedVariables(job.assignedVariables); } + // Fallback: falls Variablen unter "variables" statt "assignedVariables" geliefert werden + if (job.variables && job.variables.length > 0) { + return this.formatAssignedVariables(job.variables); + } + // Letzter Fallback: ggf. aus Cache (z. B. nach Lazy-Load) + const cachedDetails = this.jobDetailsCache.get(job.id); + if (cachedDetails && cachedDetails.variables && cachedDetails.variables.length > 0) { + return this.formatAssignedVariables(cachedDetails.variables); + } return 'Keine Variablen'; } @@ -166,7 +179,7 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { if (job.assignedVariableBundles && job.assignedVariableBundles.length > 0) { const count = job.assignedVariableBundles.length; const maxToShow = 2; - const bundleNames = job.assignedVariableBundles.map(b => b.name); + const bundleNames = job.assignedVariableBundles.map(b => b.name || 'unbekannt'); if (bundleNames.length <= maxToShow) { return `${count} (${bundleNames.join(', ')})`; @@ -185,7 +198,11 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { } const maxToShow = 3; - const variableNames = assignedVariables.map(v => `${v.unitName}_${v.variableId}`); + const variableNames = assignedVariables.map(v => { + const unitName = v.unitName || 'unbekannt'; + const variableId = v.variableId || 'unbekannt'; + return `${unitName}_${variableId}`; + }); if (variableNames.length <= maxToShow) { return variableNames.join(', '); @@ -194,31 +211,29 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { return `${variableNames.slice(0, maxToShow).join(', ')} +${variableNames.length - maxToShow} weitere`; } - private formatVariableBundles(bundles: VariableBundle[]): string { - if (!bundles || bundles.length === 0) { - return 'Keine Variablenbündel'; - } - const maxToShow = 3; - const bundleNames = bundles.map(b => b.name); - - if (bundleNames.length <= maxToShow) { - return bundleNames.join(', '); - } - - return `${bundleNames.slice(0, maxToShow).join(', ')} +${bundleNames.length - maxToShow} weitere`; - } - getFullVariables(job: CodingJob): string { if (job.assignedVariables && job.assignedVariables.length > 0) { - const variableNames = job.assignedVariables.map(v => `${v.unitName}_${v.variableId}`); + const variableNames = job.assignedVariables.map(v => { + const unitName = v.unitName || 'unbekannt'; + const variableId = v.variableId || 'unbekannt'; + return `${unitName}_${variableId}`; + }); return `Variablen (${job.assignedVariables.length}): ${variableNames.join(', ')}`; } if (job.variables && job.variables.length > 0) { - return job.variables.map(v => `${v.unitName}_${v.variableId}`).join(', '); + return job.variables.map(v => { + const unitName = v.unitName || 'unbekannt'; + const variableId = v.variableId || 'unbekannt'; + return `${unitName}_${variableId}`; + }).join(', '); } const cachedDetails = this.jobDetailsCache.get(job.id); if (cachedDetails && cachedDetails.variables && cachedDetails.variables.length > 0) { - return cachedDetails.variables.map(v => `${v.unitName}_${v.variableId}`).join(', '); + return cachedDetails.variables.map(v => { + const unitName = v.unitName || 'unbekannt'; + const variableId = v.variableId || 'unbekannt'; + return `${unitName}_${variableId}`; + }).join(', '); } return 'Keine Variablen zugewiesen'; @@ -226,17 +241,17 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { getFullVariableBundles(job: CodingJob): string { if (job.assignedVariableBundles && job.assignedVariableBundles.length > 0) { - const bundleNames = job.assignedVariableBundles.map(b => b.name); + const bundleNames = job.assignedVariableBundles.map(b => b.name || 'unbekannt'); return `Variablen-Bündel (${job.assignedVariableBundles.length}): ${bundleNames.join(', ')}`; } if (job.variableBundles && job.variableBundles.length > 0) { - return job.variableBundles.map(b => b.name).join(', '); + return job.variableBundles.map(b => b.name || 'unbekannt').join(', '); } const cachedDetails = this.jobDetailsCache.get(job.id); if (cachedDetails && cachedDetails.variableBundles && cachedDetails.variableBundles.length > 0) { - return cachedDetails.variableBundles.map(b => b.name).join(', '); + return cachedDetails.variableBundles.map(b => b.name || 'unbekannt').join(', '); } return 'Keine Variablen-Bündel zugewiesen'; @@ -257,6 +272,8 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { const now = new Date(); const newCodingJob: CodingJob = { ...result, + // Normalisierung: damit die Liste sofort die Variablen anzeigt + assignedVariables: result.assignedVariables ?? result.variables ?? [], id: newId, createdAt: now, updatedAt: now @@ -264,6 +281,7 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { const currentData = this.dataSource.data; this.dataSource.data = [...currentData, newCodingJob]; this.snackBar.open(`Kodierjob "${newCodingJob.name}" wurde erstellt`, 'Schließen', { duration: 3000 }); + this.loadCodingJobs(); } }); } @@ -282,32 +300,20 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { dialogRef.afterClosed().subscribe(result => { if (result) { - const currentData = this.dataSource.data; - const index = currentData.findIndex(job => job.id === result.id); - - if (index !== -1) { - // Preserve the original createdAt and ensure updatedAt is a Date object - const updatedData = [...currentData]; - const now = new Date(); - - // Handle createdAt date properly - let createdAtDate = now; - if (selectedJob.createdAt instanceof Date) { - createdAtDate = selectedJob.createdAt; - } else if (selectedJob.createdAt) { - createdAtDate = new Date(selectedJob.createdAt); - } - - updatedData[index] = { - ...result, - createdAt: createdAtDate, - updatedAt: now - }; - - this.dataSource.data = updatedData; - - this.snackBar.open(`Kodierjob "${result.name}" wurde aktualisiert`, 'Schließen', { duration: 3000 }); + const workspaceId = this.appService.selectedWorkspaceId; + if (!workspaceId) { + this.snackBar.open('Kein Workspace ausgewählt', 'Schließen', { duration: 3000 }); + return; } + this.backendService.updateCodingJob(workspaceId, result.id, result).subscribe({ + next: updatedJob => { + this.loadCodingJobs(); + this.snackBar.open(`Kodierjob "${updatedJob.name}" wurde aktualisiert`, 'Schließen', { duration: 3000 }); + }, + error: () => { + this.snackBar.open(`Fehler beim Aktualisieren von Kodierjob "${result.name}"`, 'Schließen', { duration: 3000 }); + } + }); } }); } @@ -420,32 +426,61 @@ export class CodingJobsComponent implements OnInit, AfterViewInit { getAssignedCoderNames(job: CodingJob): string { if (this.coderNamesByJobId.has(job.id)) { - const coderNames = this.coderNamesByJobId.get(job.id)!; - - if (coderNames !== 'Keine' && coderNames.includes(',') && job.assignedCoders && job.assignedCoders.length > 2) { - const namesList = coderNames.split(', '); - return `${namesList[0]}, ${namesList[1]} +${job.assignedCoders.length - 2} weitere`; + const full = this.coderNamesByJobId.get(job.id)!; + if (full === 'Keine') { + return full; } - - return coderNames; + const names = full.split(', ').filter(n => n && n.trim().length > 0); + if (names.length > 2) { + return `${names[0]}, ${names[1]} +${names.length - 2} weitere`; + } + return names.join(', '); } if (!job.assignedCoders || job.assignedCoders.length === 0) { return 'Keine'; } + // Fallback, falls Map noch nicht gefüllt ist return `${job.assignedCoders.length} Kodierer`; } getFullCoderNames(job: CodingJob): string { if (this.coderNamesByJobId.has(job.id)) { - return this.coderNamesByJobId.get(job.id) || `${job.assignedCoders?.length || 0} Kodierer`; + const full = this.coderNamesByJobId.get(job.id)!; + return full === 'Keine' ? 'Keine Kodierer zugewiesen' : full; } if (!job.assignedCoders || job.assignedCoders.length === 0) { return 'Keine Kodierer zugewiesen'; } + // Fallback return `${job.assignedCoders.length} Kodierer`; } + + private updateCoderNamesMap(jobs: CodingJob[]): void { + if (!jobs || jobs.length === 0) { + return; + } + + const byId = new Map(this.allCoders.map(c => [c.id, c])); + jobs.forEach(job => { + const ids = job.assignedCoders || []; + if (ids.length === 0) { + this.coderNamesByJobId.set(job.id, 'Keine'); + return; + } + const names = ids + .map(id => byId.get(id)) + .filter((c): c is Coder => !!c) + .map(c => c.displayName || c.name || `Coder ${c.id}`); + + if (names.length === 0) { + this.coderNamesByJobId.set(job.id, `${ids.length} Kodierer`); + } else { + this.coderNamesByJobId.set(job.id, names.join(', ')); + } + }); + } } diff --git a/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.html b/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.html index fec18a004..c69efe1ea 100755 --- a/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.html +++ b/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.html @@ -2,11 +2,12 @@ -
@@ -35,7 +35,6 @@

Validierung läuft

-
@@ -56,7 +55,6 @@

Fehler bei der Validierung

-
@@ -78,7 +76,6 @@

Validierungsergebnisse


-
-
+
+
+
+
+

Import-Vergleichstabelle

+

+ {{importResults.message}} | + Betroffene Zeilen: {{importResults.affectedRows.length}} | + Seite {{comparisonCurrentPage}} von {{comparisonTotalPages}} +

+
+
+ + +
+
+
+ + +
+
+ + + Seite {{comparisonCurrentPage}} von {{comparisonTotalPages}} + + +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Unit AliasVariable IDPerson CodeOriginal StatusOriginal CodeOriginal ScoreUpdated StatusUpdated CodeUpdated Score
{{row.unitAlias}}{{row.variableId}}{{row.personCode || '-'}}{{row.originalCodedStatus}}{{row.originalCode || '-'}}{{row.originalScore || '-'}} + {{row.updatedCodedStatus || '-'}} + + {{row.updatedCode || '-'}} + + {{row.updatedScore || '-'}} +
+
+ + +
+ + + Seite {{comparisonCurrentPage}} von {{comparisonTotalPages}} + + +
+ +
+

Fehler und Warnungen

+
    +
  • {{error}}
  • +
+
+
+
+

Manuelle Kodierung planen

@@ -164,4 +273,14 @@

Variablenbündel

+ + +
+
+ + +
+
diff --git a/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.scss b/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.scss index dd2163c79..dab0efc2f 100755 --- a/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.scss +++ b/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.scss @@ -266,6 +266,87 @@ } } +// Comparison results styles +.comparison-results { + width: 100%; + margin-bottom: 24px; + + .comparison-content { + overflow-x: auto; + max-height: 600px; + overflow-y: auto; + } + + .results-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + + th, td { + padding: 10px 12px; + text-align: left; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + } + + th { + background-color: #f5f5f5; + font-weight: 500; + color: #333; + position: sticky; + top: 0; + z-index: 10; + } + + tr:hover { + background-color: rgba(25, 118, 210, 0.05); + } + + td.updated { + background-color: rgba(76, 175, 80, 0.1); + color: #388e3c; + font-weight: 500; + position: relative; + + &::after { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background-color: #4caf50; + } + } + } + + .error-section { + margin-top: 20px; + padding: 15px; + background-color: rgba(255, 193, 7, 0.1); + border-radius: 4px; + border-left: 4px solid #ffc107; + + h3 { + margin-top: 0; + margin-bottom: 10px; + color: #f57f17; + font-size: 16px; + font-weight: 500; + } + + ul { + margin: 0; + padding-left: 20px; + } + + .error-item { + margin-bottom: 5px; + color: #e65100; + font-size: 13px; + } + } +} + // Responsive styles @media (max-width: 768px) { .coding-container { @@ -309,3 +390,72 @@ padding: 12px; } } + +// Coder Training Modal Overlay Styles +.coder-training-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + box-sizing: border-box; + animation: fadeInOverlay 0.3s ease-in-out; + + .coder-training-modal { + max-width: 90vw; + max-height: 90vh; + overflow: auto; + border-radius: 8px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + animation: slideInModal 0.3s ease-out; + } +} + +@keyframes fadeInOverlay { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideInModal { + from { + transform: scale(0.8) translateY(-20px); + opacity: 0; + } + to { + transform: scale(1) translateY(0); + opacity: 1; + } +} + +// Responsive modal styles +@media (max-width: 768px) { + .coder-training-overlay { + padding: 10px; + + .coder-training-modal { + max-width: 95vw; + max-height: 95vh; + } + } +} + +@media (max-width: 576px) { + .coder-training-overlay { + padding: 5px; + + .coder-training-modal { + max-width: 98vw; + max-height: 98vh; + } + } +} diff --git a/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.ts b/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.ts index c2402fc0f..f6b23f0e3 100755 --- a/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.ts +++ b/apps/frontend/src/app/coding/components/coding-management-manual/coding-management-manual.component.ts @@ -14,9 +14,12 @@ import * as ExcelJS from 'exceljs'; import { Subject, takeUntil } from 'rxjs'; import { CodingJobsComponent } from '../coding-jobs/coding-jobs.component'; import { VariableBundleManagerComponent } from '../variable-bundle-manager/variable-bundle-manager.component'; +import { CoderTrainingComponent, VariableConfig } from '../coder-training/coder-training.component'; +import { Coder } from '../../models/coder.model'; import { TestPersonCodingService } from '../../services/test-person-coding.service'; import { ExpectedCombinationDto } from '../../../../../../../api-dto/coding/expected-combination.dto'; import { ValidateCodingCompletenessResponseDto } from '../../../../../../../api-dto/coding/validate-coding-completeness-response.dto'; +import { ExternalCodingImportResultDto } from '../../../../../../../api-dto/coding/external-coding-import-result.dto'; import { AppService } from '../../../services/app.service'; import { ValidationProgress, @@ -35,6 +38,7 @@ import { MatButton, MatProgressBarModule, VariableBundleManagerComponent, + CoderTrainingComponent, CommonModule ] }) @@ -49,13 +53,38 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { validationProgress: ValidationProgress | null = null; isLoading = false; - // Pagination state + importResults: { + message: string; + processedRows: number; + updatedRows: number; + errors: string[]; + affectedRows: Array<{ + unitAlias: string; + variableId: string; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; + originalCodedStatus: string; + originalCode: number | null; + originalScore: number | null; + updatedCodedStatus: string | null; + updatedCode: number | null; + updatedScore: number | null; + }>; + } | null = null; + + showComparisonTable = false; + showCoderTraining = false; + currentPage = 1; pageSize = 50; expectedCombinations: ExpectedCombinationDto[] = []; validationCacheKey: string | null = null; - // Pagination helper properties + comparisonCurrentPage = 1; + comparisonPageSize = 100; + get totalPages(): number { return this.validationResults?.totalPages || 0; } @@ -68,8 +97,42 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { return this.validationResults?.hasPreviousPage || false; } + // Comparison table pagination getters + get comparisonTotalPages(): number { + if (!this.importResults?.affectedRows) return 0; + return Math.ceil(this.importResults.affectedRows.length / this.comparisonPageSize); + } + + get comparisonHasNextPage(): boolean { + return this.comparisonCurrentPage < this.comparisonTotalPages; + } + + get comparisonHasPreviousPage(): boolean { + return this.comparisonCurrentPage > 1; + } + + get paginatedAffectedRows(): Array<{ + unitAlias: string; + variableId: string; + personCode?: string; + personLogin?: string; + personGroup?: string; + bookletName?: string; + originalCodedStatus: string; + originalCode: number | null; + originalScore: number | null; + updatedCodedStatus: string | null; + updatedCode: number | null; + updatedScore: number | null; + }> { + if (!this.importResults?.affectedRows) return []; + + const startIndex = (this.comparisonCurrentPage - 1) * this.comparisonPageSize; + const endIndex = startIndex + this.comparisonPageSize; + return this.importResults.affectedRows.slice(startIndex, endIndex); + } + ngOnInit(): void { - // Subscribe to validation progress updates this.validationStateService.validationProgress$ .pipe(takeUntil(this.destroy$)) .subscribe(progress => { @@ -81,20 +144,17 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { } }); - // Subscribe to validation results this.validationStateService.validationResults$ .pipe(takeUntil(this.destroy$)) .subscribe(results => { this.validationResults = results; if (results) { - // Store cache key for pagination and Excel export this.validationCacheKey = results.cacheKey || null; this.showSuccess(`Validierung abgeschlossen. ${results.missing} von ${results.total} Kombinationen fehlen.`); } }); - // Restore previous validation state if available const currentResults = this.validationStateService.getValidationResults(); if (currentResults) { this.validationResults = currentResults; @@ -110,6 +170,112 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { this.destroy$.complete(); } + /** + * Handle external coding file selection event + */ + onExternalCodingFileSelected(event: Event): void { + const input = event.target as HTMLInputElement; + + if (!input.files || input.files.length === 0) { + this.showError('Keine Datei ausgewählt'); + return; + } + + const file = input.files[0]; + if (!this.isExcelOrCsvFile(file)) { + this.showError('Bitte wählen Sie eine CSV- oder Excel-Datei aus (.csv, .xlsx, .xls)'); + return; + } + + this.processExternalCodingFile(file); + } + + /** + * Check if the file is a CSV or Excel file + */ + private isExcelOrCsvFile(file: File): boolean { + return file.name.endsWith('.xlsx') || file.name.endsWith('.xls') || file.name.endsWith('.csv'); + } + + /** + * Process external coding file upload with real-time progress tracking + */ + private async processExternalCodingFile(file: File): Promise { + this.isLoading = true; + this.validationStateService.startValidation(); + + try { + const workspaceId = this.appService.selectedWorkspaceId; + + if (!workspaceId) { + this.showError('Kein Arbeitsbereich ausgewählt'); + this.validationStateService.setValidationError('Kein Arbeitsbereich ausgewählt'); + return; + } + + this.validationStateService.updateProgress(10, 'Datei wird verarbeitet...'); + const fileData = await this.fileToBase64(file); + + // Start import with progress tracking via Server-Sent Events + await this.testPersonCodingService.importExternalCodingWithProgress( + workspaceId, + { + file: fileData, + fileName: file.name + }, + // onProgress callback + (progress: number, message: string) => { + this.validationStateService.updateProgress(progress, message); + }, + // onComplete callback + (result: ExternalCodingImportResultDto) => { + // Reset validation state to hide progress UI + this.validationStateService.resetValidation(); + + // Store import results and show comparison table + this.importResults = result; + this.showComparisonTable = true; + this.comparisonCurrentPage = 1; + + this.showSuccess(`Externe Kodierung erfolgreich importiert: ${result.updatedRows} von ${result.processedRows} Zeilen aktualisiert.`); + + if (result.errors && result.errors.length > 0) { + this.showError(`${result.errors.length} Warnungen aufgetreten. Details in der Konsole.`); + } + + this.isLoading = false; + }, + // onError callback + (error: string) => { + this.validationStateService.setValidationError(`Import fehlgeschlagen: ${error}`); + this.showError('Fehler beim Importieren der externen Kodierung'); + this.isLoading = false; + } + ); + } catch (error) { + this.validationStateService.setValidationError('Fehler beim Importieren der externen Kodierung'); + this.showError('Fehler beim Importieren der externen Kodierung'); + this.isLoading = false; + } + } + + /** + * Convert file to base64 string + */ + private fileToBase64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + const result = reader.result as string; + // Remove data:application/...;base64, prefix + const base64Data = result.split(',')[1]; + resolve(base64Data); + }; + reader.onerror = error => reject(error); + }); + } + /** * Handle file selection event */ @@ -126,11 +292,7 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { this.showError('Bitte wählen Sie eine Excel-Datei aus (.xlsx, .xls)'); return; } - - // Start validation process this.validationStateService.startValidation(); - - // Process file in the background setTimeout(() => { this.readExcelFile(file); }, 0); @@ -153,29 +315,20 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { reader.onload = async (e: ProgressEvent) => { try { const buffer = e.target?.result as ArrayBuffer; - - // Update progress this.validationStateService.updateProgress(10, 'Excel-Datei wird geladen...'); - await workbook.xlsx.load(buffer); this.validationStateService.updateProgress(30, 'Excel-Datei wird verarbeitet...'); - - // Get the first worksheet const worksheet = workbook.getWorksheet(1); if (!worksheet || worksheet.rowCount <= 1) { this.validationStateService.setValidationError('Die Datei enthält keine gültigen Daten'); return; } - - // Extract headers from the first row const headers: string[] = []; worksheet.getRow(1).eachCell(cell => { headers.push(cell.value?.toString() || ''); }); this.validationStateService.updateProgress(40, 'Daten werden extrahiert...'); - - // Extract data from the remaining rows const data: Record[] = []; const totalRows = worksheet.rowCount - 1; @@ -190,7 +343,6 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { data.push(rowData); - // Update progress every 100 rows or at the end if (rowNumber % 100 === 0 || rowNumber === worksheet.rowCount) { const progress = 40 + Math.floor(((rowNumber - 2) / totalRows) * 20); this.validationStateService.updateProgress( @@ -219,7 +371,6 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { this.validationStateService.setValidationError('Fehler beim Lesen der Datei'); }; - // Read the file as an ArrayBuffer for exceljs reader.readAsArrayBuffer(file); } @@ -311,6 +462,29 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { this.loadValidationPage(1); // Reset to first page when changing page size } + nextComparisonPage(): void { + if (this.comparisonHasNextPage) { + this.comparisonCurrentPage += 1; + } + } + + previousComparisonPage(): void { + if (this.comparisonHasPreviousPage) { + this.comparisonCurrentPage -= 1; + } + } + + goToComparisonPage(page: number): void { + if (page >= 1 && page <= this.comparisonTotalPages) { + this.comparisonCurrentPage = page; + } + } + + changeComparisonPageSize(newPageSize: number): void { + this.comparisonPageSize = newPageSize; + this.comparisonCurrentPage = 1; + } + /** * Download validation results as Excel file using cache key */ @@ -329,20 +503,13 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { this.validationCacheKey ).subscribe({ next: blob => { - // Create download link const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - - // Generate filename with timestamp const timestamp = new Date().toISOString().slice(0, 10); link.download = `validation-results-${timestamp}.xlsx`; - - // Trigger download document.body.appendChild(link); link.click(); - - // Cleanup document.body.removeChild(link); window.URL.revokeObjectURL(url); @@ -351,8 +518,6 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { }, error: error => { let errorMessage = 'Fehler beim Herunterladen der Excel-Datei'; - - // Provide more specific error messages if (error.status === 404) { errorMessage = 'Validierungsdaten nicht gefunden. Bitte führen Sie zuerst eine neue Validierung durch.'; } else if (error.status === 400) { @@ -371,6 +536,108 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { }); } + /** + * Download comparison table as Excel file + */ + downloadComparisonTable(): void { + if (!this.importResults || !this.importResults.affectedRows || this.importResults.affectedRows.length === 0) { + this.showError('Keine Vergleichsdaten zum Herunterladen verfügbar.'); + return; + } + + this.isLoading = true; + + try { + // Create a new workbook + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Import Vergleich'); + + // Add headers + const headers = [ + 'Unit Alias', + 'Variable ID', + 'Person Code', + 'Original Status', + 'Original Code', + 'Original Score', + 'Updated Status', + 'Updated Code', + 'Updated Score' + ]; + worksheet.addRow(headers); + + // Style headers + const headerRow = worksheet.getRow(1); + headerRow.font = { bold: true }; + headerRow.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFE0E0E0' } + }; + + // Add data rows + this.importResults.affectedRows.forEach(row => { + worksheet.addRow([ + row.unitAlias, + row.variableId, + row.personCode, + row.originalCodedStatus, + row.originalCode, + row.originalScore, + row.updatedCodedStatus, + row.updatedCode, + row.updatedScore + ]); + }); + + // Auto-fit columns + worksheet.columns.forEach(column => { + if (column) { + let maxLength = 0; + column.eachCell?.({ includeEmpty: true }, cell => { + const columnLength = cell.value ? cell.value.toString().length : 10; + if (columnLength > maxLength) { + maxLength = columnLength; + } + }); + if (column.width !== undefined) { + column.width = Math.min(maxLength + 2, 50); + } + } + }); + + // Generate Excel file + workbook.xlsx.writeBuffer().then(buffer => { + const blob = new Blob([buffer], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + const timestamp = new Date().toISOString().slice(0, 10); + link.download = `import-comparison-${timestamp}.xlsx`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + this.showSuccess('Vergleichstabelle wurde erfolgreich als Excel-Datei heruntergeladen'); + this.isLoading = false; + }); + } catch (error) { + this.showError('Fehler beim Erstellen der Excel-Datei'); + this.isLoading = false; + } + } + + /** + * Close the comparison table + */ + closeComparisonTable(): void { + this.showComparisonTable = false; + this.importResults = null; + } + /** * Show error message */ @@ -390,4 +657,34 @@ export class CodingManagementManualComponent implements OnInit, OnDestroy { panelClass: ['success-snackbar'] }); } + + openCoderTraining(): void { + this.showCoderTraining = true; + } + + closeCoderTraining(): void { + this.showCoderTraining = false; + } + + onTrainingStart(data: { selectedCoders: Coder[], variableConfigs: VariableConfig[] }): void { + const workspaceId = this.appService.selectedWorkspaceId; + this.testPersonCodingService.generateCoderTrainingPackages( + workspaceId, + data.selectedCoders, + data.variableConfigs + ).pipe(takeUntil(this.destroy$)) + .subscribe({ + next: packages => { + const totalResponses = packages.reduce((total, pkg) => total + pkg.responses.length, 0); + + this.showSuccess( + `Schulung erfolgreich generiert: ${packages.length} Kodierer-Pakete mit insgesamt ${totalResponses} Antworten erstellt` + ); + this.closeCoderTraining(); + }, + error: () => { + this.showError('Fehler beim Generieren der Kodierer-Schulungspakete'); + } + }); + } } diff --git a/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.html b/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.html index 090620c72..249ede9ea 100755 --- a/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.html +++ b/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.html @@ -39,149 +39,163 @@ - @if (!showManualCoding) { -
- @if (!isLoadingStatistics) { -
-

Kodierstatistiken

-

Übersicht über den Status der Kodierung und Antworten

- -
-
- Gesamtanzahl der gegebenen Antworten: - {{ codingStatistics.totalResponses }} +
+
+
+ @if (!statisticsLoaded && !isLoadingStatistics) { +
+
+ +

Klicken Sie hier, um die Kodierstatistiken zu laden

+
- @for (status of getStatuses(); track status) { - - } +
+ } - @if (isLoadingStatistics) { -
- -

Lade Kodierstatistiken...

+ @if (isLoadingStatistics) { +
+ +

Lade Kodierstatistiken...

+
+ }
- } - @if (isAutoCoding) { -
- -

Antworten werden kodiert...

-
- } +
+ @if (isAutoCoding) { +
+ +

Antworten werden kodiert...

+
+ } - @if (isLoading && !isAutoCoding) { -
- -

Daten werden verarbeitet...

-
- } + @if (isLoading && !isAutoCoding) { +
+ +

Daten werden verarbeitet...

+
+ } - @if (!isLoading && this.data.length > 0) { -
-
-

Kodierdaten

- @if (currentStatusFilter) { -
-

- @if (currentStatusFilter === 'null') { - Unkodierte Antworten - } @else { - Antworten mit Status: {{ currentStatusFilter }} - } -

-
- } -
- -
- - @for (column of displayedColumns; track column) { - - - - - } - - -
{{ column | titlecase }} - @if (element.id === 0) { - - {{ element.unitname || element.variableid }} - - } - @if (element.id !== 0) { - @if (column === 'actions') { -
- - -
- } @else if (column === 'unitname') { - - {{ element[column] }} - + @if (!isLoading && this.data.length > 0) { +
+
+

Kodierdaten

+ @if (currentStatusFilter) { +
+

+ @if (currentStatusFilter === 'null') { + Unkodierte Antworten } @else { - - {{ element[column] }} - + Antworten mit Status: {{ currentStatusFilter }} } - } -

- - -
-
- } + +
+ } +
+ +
+ + @for (column of displayedColumns; track column) { + + + + + } + + +
{{ column | titlecase }} + @if (element.id === 0) { + + {{ element.unitname || element.variableid }} + + } + @if (element.id !== 0) { + @if (column === 'actions') { +
+ + +
+ } @else if (column === 'unitname') { + + {{ element[column] }} + + } @else { + + {{ element[column] }} + + } + } +
+ + +
+
+ } - @if (!isLoading && this.data.length === 0) { -
- code -

Noch keine Kodierdaten angezeigt

-

Klicken Sie auf "Anzeigen" um Daten zu laden.

+ @if (!isLoading && this.data.length === 0) { +
+ code +

Noch keine Kodierdaten angezeigt

+

Klicken Sie auf "Anzeigen" um Daten zu laden.

+
+ }
- } - +
} diff --git a/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.scss b/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.scss index 255d8ff66..b5d4aa226 100755 --- a/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.scss +++ b/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.scss @@ -1,4 +1,3 @@ -// Main container styles .coding-container { padding: 24px; width: 100%; @@ -9,12 +8,29 @@ animation: fadeIn 0.3s ease-in-out; } +.content-row { + width: 100%; +} + +.statistics-section { + flex: 0 0 400px; + min-width: 0; + display: flex; + flex-direction: column; +} + +.data-section { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } -// Header section styles .header-section { margin-bottom: 24px; @@ -34,7 +50,6 @@ } } -// Card styles .action-card, .data-card, .statistics-card { background-color: white; border-radius: 12px; @@ -49,18 +64,7 @@ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); } - // Horizontal layout for statistics and data cards - @media (min-width: 1200px) { - &.statistics-card, &.data-card { - display: inline-block; - vertical-align: top; - width: calc(50% - 12px); - } - &.statistics-card { - margin-right: 24px; - } - } .section-title { font-size: 22px; @@ -111,7 +115,6 @@ } } -// Action buttons styles .action-buttons { display: flex; flex-wrap: wrap; @@ -162,7 +165,6 @@ } } -// Loading container styles .loading-container { display: flex; flex-direction: column; @@ -184,7 +186,10 @@ } } -// Statistics card styles +.large-transparent-spinner { + opacity: 0.7; +} + .statistics-card { .statistics-content { margin: 20px 0; @@ -444,7 +449,7 @@ position: relative; overflow-x: auto; overflow-y: auto; - max-height: 400px; /* Default max height for smaller screens */ + max-height: 60vh; border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.08); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); @@ -470,13 +475,12 @@ background: #a3c0e0; } - /* Responsive max-height adjustments */ @media (min-height: 768px) { - max-height: 500px; /* Medium screens */ + max-height: 70vh; } @media (min-height: 1024px) { - max-height: 600px; /* Larger screens */ + max-height: 80vh; } mat-paginator { @@ -493,7 +497,7 @@ .coding-table { width: 100%; - min-width: 600px; /* Ensures table doesn't get too narrow */ + min-width: 600px; th, td { padding: 14px 18px; @@ -501,7 +505,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - max-width: 200px; /* Prevents cells from getting too wide */ + max-width: 200px; } th { @@ -599,7 +603,6 @@ border: 1px solid rgba(255, 152, 0, 0.2); } -// Empty state styles .empty-state { display: flex; flex-direction: column; @@ -638,7 +641,6 @@ } } -// Snackbar styles ::ng-deep .error-snackbar { background-color: #f44336; color: white; @@ -708,15 +710,14 @@ .clickable-cell { cursor: pointer; - color: #3f51b5; /* Primärfarbe für bessere Sichtbarkeit als Link */ + color: #3f51b5; text-decoration: underline; &:hover { - color: #283593; /* Dunklere Farbe beim Hover */ + color: #283593; } } -// Variable Analysis Bar Chart styles .bar-chart-container { display: flex; align-items: center; @@ -750,7 +751,6 @@ } } -// Dialog styles for test person coding ::ng-deep .full-screen-dialog { .mat-mdc-dialog-container { padding: 0 !important; diff --git a/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.ts b/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.ts index 6316079db..e367516b4 100755 --- a/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.ts +++ b/apps/frontend/src/app/coding/components/coding-managment/coding-management.component.ts @@ -22,13 +22,17 @@ import { MatTableDataSource } from '@angular/material/table'; import { MatSort, MatSortModule, MatSortHeader } from '@angular/material/sort'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatIcon } from '@angular/material/icon'; -import { MatAnchor, MatButton, MatIconButton } from '@angular/material/button'; +import { + MatAnchor, MatButton, MatFabButton, MatIconButton +} from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; import { ScrollingModule } from '@angular/cdk/scrolling'; import { MatDivider } from '@angular/material/divider'; @@ -37,18 +41,25 @@ import { MatDialog } from '@angular/material/dialog'; import { ContentDialogComponent } from '../../../shared/dialogs/content-dialog/content-dialog.component'; import { BackendService } from '../../../services/backend.service'; import { AppService } from '../../../services/app.service'; +import { WorkspaceSettingsService } from '../../../ws-admin/services/workspace-settings.service'; import { CodingStatistics } from '../../../../../../../api-dto/coding/coding-statistics'; import { ExportDialogComponent, ExportFormat } from '../export-dialog/export-dialog.component'; import { Success } from '../../models/success.model'; import { CodingListItem } from '../../models/coding-list-item.model'; +import { ResponseEntity } from '../../../shared/models/response-entity.model'; import { TestPersonCodingDialogComponent } from '../test-person-coding-dialog/test-person-coding-dialog.component'; import { ExportCodingBookComponent } from '../export-coding-book/export-coding-book.component'; import { CodingManagementManualComponent } from '../coding-management-manual/coding-management-manual.component'; import { VariableAnalysisDialogComponent } from '../variable-analysis-dialog/variable-analysis-dialog.component'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; @Component({ selector: 'app-coding-management', templateUrl: './coding-management.component.html', + standalone: true, + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ NgClass, MatTable, @@ -73,6 +84,7 @@ import { VariableAnalysisDialogComponent } from '../variable-analysis-dialog/var MatIcon, MatAnchor, MatIconButton, + MatFabButton, MatTooltipModule, MatDivider, MatButton, @@ -85,22 +97,22 @@ import { VariableAnalysisDialogComponent } from '../variable-analysis-dialog/var export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestroy { private backendService = inject(BackendService); private appService = inject(AppService); + private workspaceSettingsService = inject(WorkspaceSettingsService); private snackBar = inject(MatSnackBar); private dialog = inject(MatDialog); @ViewChild(MatSort) sort!: MatSort; @ViewChild(MatPaginator) paginator!: MatPaginator; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any[] = []; - dataSource = new MatTableDataSource(this.data); + data: Success[] = []; + dataSource = new MatTableDataSource(this.data); displayedColumns: string[] = ['unitname', 'variableid', 'value', 'codedstatus', 'actions']; - isLoading = false; isFilterLoading = false; isLoadingStatistics = false; isAutoCoding = false; showManualCoding = false; + statisticsLoaded = false; currentStatusFilter: string | null = null; pageSizeOptions = [100, 200, 500]; pageSize = 100; @@ -118,7 +130,15 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr } ngOnInit(): void { - this.fetchCodingStatistics(); + const workspaceId = this.appService.selectedWorkspaceId; + if (workspaceId) { + this.workspaceSettingsService.getAutoFetchCodingStatistics(workspaceId) + .subscribe(autoFetch => { + if (autoFetch) { + this.fetchCodingStatistics(); + } + }); + } this.filterTextChanged .pipe( @@ -144,7 +164,6 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr ) .subscribe(({ jobId }) => { if (!jobId) { - // Fallback: fetch directly this.backendService.getCodingStatistics(workspaceId) .pipe( catchError(() => { @@ -163,6 +182,7 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr ) .subscribe(statistics => { this.codingStatistics = statistics; + this.statisticsLoaded = true; }); return; } @@ -179,6 +199,7 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr .subscribe(status => { if (status.status === 'completed' && status.result) { this.codingStatistics = status.result; + this.statisticsLoaded = true; } else if (['failed', 'cancelled', 'paused'].includes(status.status)) { this.snackBar.open(`Statistik-Job ${status.status}`, 'Schließen', { duration: 5000, panelClass: ['error-snackbar'] }); } @@ -197,31 +218,6 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr return Math.round((this.codingStatistics.statusCounts[status] / this.codingStatistics.totalResponses) * 100); } - getStatusColor(status: string): string { - const colorMap: { [key: string]: string } = { - CODING_COMPLETE: '#4CAF50', // Green - CODING_INCOMPLETE: '#FFC107', // Amber - NOT_REACHED: '#9E9E9E', // Grey - INVALID: '#F44336', // Red - INTENDED_INCOMPLETE: '#2196F3' // Blue - }; - return colorMap[status] || '#9C27B0'; // Default to purple for unknown statuses - } - - getChartData(): { status: string; count: number; percentage: number; color: string }[] { - if (!this.codingStatistics.totalResponses) { - return []; - } - - return Object.keys(this.codingStatistics.statusCounts) - .map(status => ({ - status, - count: this.codingStatistics.statusCounts[status], - percentage: this.getStatusPercentage(status), - color: this.getStatusColor(status) - })); - } - fetchResponsesByStatus(status: string, page: number = 1, limit: number = this.pageSize): void { const workspaceId = this.appService.selectedWorkspaceId; this.isLoading = true; @@ -247,26 +243,23 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr }) ) .subscribe(response => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.data = response.data.map((item: any) => ({ + this.data = response.data.map((item: ResponseEntity) => ({ id: item.id, - unitid: item.unitid, + unitid: item.unitId, variableid: item.variableid || '', status: item.status || '', value: item.value || '', subform: item.subform || '', - code: item.code, - score: item.score, + code: item.code?.toString() || null, + score: item.score?.toString() || null, unit: item.unit, codedstatus: item.codedstatus || '', unitname: item.unit?.name || '', - // Extract information for replay URL login_name: item.unit?.booklet?.person?.login || '', - login_group: item.unit?.booklet?.person?.group || '', + login_group: (item.unit?.booklet?.person as { login: string; code: string; group?: string })?.group || '', login_code: item.unit?.booklet?.person?.code || '', booklet_id: item.unit?.booklet?.bookletinfo?.name || '' })); - this.dataSource.data = this.data; this.totalRecords = response.total; @@ -519,8 +512,6 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr .slice(0, 10)}.xlsx`; document.body.appendChild(a); a.click(); - - // Clean up window.URL.revokeObjectURL(url); document.body.removeChild(a); @@ -556,7 +547,7 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr if (codingSchemeRef) { this.showCodingScheme(codingSchemeRef); } else { - this.snackBar.open(`Kein Kodierschema-Verweis in der Unit ${unitId} gefunden.`, 'Schließen', { + this.snackBar.open(`Kein Kodierschema in Kodierdaten für die Unit ${unitId} gefunden.`, 'Schließen', { duration: 5000 }); } @@ -597,7 +588,7 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr ) .subscribe(fileData => { if (!fileData || !fileData.base64Data) { - this.snackBar.open(`Kodierschema '${codingSchemeRef}' wurde nicht gefunden.`, 'Schließen', { + this.snackBar.open(`Kodierschema '${codingSchemeRef}' in Kodierdaten nicht gefunden.`, 'Schließen', { duration: 5000, panelClass: ['error-snackbar'] }); @@ -655,9 +646,6 @@ export class CodingManagementComponent implements AfterViewInit, OnInit, OnDestr }); } - /** - * Opens the export coding book dialog - */ openExportCodingBook(): void { this.dialog.open(ExportCodingBookComponent, { width: '80%', diff --git a/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.html b/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.html index 39eb9e49c..dd443c71e 100644 --- a/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.html +++ b/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.html @@ -62,17 +62,17 @@

Zugriff verweigert

- + Erstellt am - {{element.createdAt | date: 'dd.MM.yyyy HH:mm'}} + {{element.created_at | date: 'dd.MM.yyyy HH:mm'}} - + Aktualisiert am - {{element.updatedAt | date: 'dd.MM.yyyy HH:mm'}} + {{element.updated_at | date: 'dd.MM.yyyy HH:mm'}} diff --git a/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.ts b/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.ts index 9f857539a..29d276902 100644 --- a/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.ts +++ b/apps/frontend/src/app/coding/components/my-coding-jobs/my-coding-jobs.component.ts @@ -18,13 +18,14 @@ import { MatIcon } from '@angular/material/icon'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatAnchor, MatButton } from '@angular/material/button'; import { DatePipe, NgClass } from '@angular/common'; -import { Router } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { AppService } from '../../../services/app.service'; import { BackendService } from '../../../services/backend.service'; import { SearchFilterComponent } from '../../../shared/search-filter/search-filter.component'; import { CodingJob } from '../../models/coding-job.model'; import { WorkspaceUserDto } from '../../../../../../../api-dto/workspaces/workspace-user-dto'; import { CoderService } from '../../services/coder.service'; +import { CodingJobService } from '../../services/coding-job.service'; @Component({ selector: 'coding-box-my-coding-jobs', @@ -50,7 +51,8 @@ import { CoderService } from '../../services/coder.service'; MatRowDef, MatColumnDef, MatSortModule, - MatButton + MatButton, + RouterLink ] }) export class MyCodingJobsComponent implements OnInit, AfterViewInit { @@ -59,8 +61,9 @@ export class MyCodingJobsComponent implements OnInit, AfterViewInit { private snackBar = inject(MatSnackBar); private router = inject(Router); private coderService = inject(CoderService); + private codingJobService = inject(CodingJobService); - displayedColumns: string[] = ['name', 'description', 'status', 'createdAt', 'updatedAt']; + displayedColumns: string[] = ['name', 'description', 'status', 'created_at', 'updated_at']; dataSource = new MatTableDataSource([]); selection = new SelectionModel(true, []); isLoading = false; @@ -105,43 +108,10 @@ export class MyCodingJobsComponent implements OnInit, AfterViewInit { loadMyCodingJobs(): void { this.isLoading = true; - const sampleJobs = [ - { - id: 1, - name: 'Kodierjob 1', - description: 'Beschreibung für Kodierjob 1', - status: 'active', - createdAt: new Date('2023-01-01'), - updatedAt: new Date('2023-01-15'), - assignedCoders: [1, 2] - }, - { - id: 2, - name: 'Kodierjob 2', - description: 'Beschreibung für Kodierjob 2', - status: 'completed', - createdAt: new Date('2023-02-01'), - updatedAt: new Date('2023-02-15'), - assignedCoders: [3] - }, - { - id: 3, - name: 'Kodierjob 3', - description: 'Beschreibung für Kodierjob 3', - status: 'pending', - createdAt: new Date('2023-03-01'), - updatedAt: new Date('2023-03-15'), - assignedCoders: [1] - } - ]; - - this.coderService.getCodersByJobId(this.currentUserId).subscribe({ - next: coders => { - if (coders.length > 0) { - const currentCoder = coders[0]; - const assignedJobIds = currentCoder.assignedJobs || []; - - this.dataSource.data = sampleJobs.filter(job => assignedJobIds.includes(job.id)); + this.coderService.getJobsByCoderId(this.currentUserId).subscribe({ + next: jobs => { + if (jobs.length > 0) { + this.dataSource.data = jobs; } else { this.dataSource.data = []; } @@ -164,7 +134,19 @@ export class MyCodingJobsComponent implements OnInit, AfterViewInit { } startCodingJob(job: CodingJob): void { - this.snackBar.open(`Starten von Kodierjob "${job.name}" noch nicht implementiert`, 'Schließen', { duration: 3000 }); + this.snackBar.open(`Starten von Kodierjob "${job.name}"...`, 'Schließen', { duration: 2000 }); + this.codingJobService.getResponsesForCodingJob(job.id).subscribe({ + next: responses => { + if (responses && responses.length > 0) { + // fetch responses for this job + } else { + this.snackBar.open('Keine Antworten für diesen Kodierjob gefunden', 'Schließen', { duration: 3000 }); + } + }, + error: () => { + this.snackBar.open('Fehler beim Laden der Antworten', 'Schließen', { duration: 3000 }); + } + }); } getStatusClass(status: string): string { diff --git a/apps/frontend/src/app/coding/components/scheme-editor-dialog/scheme-editor-dialog.component.ts b/apps/frontend/src/app/coding/components/scheme-editor-dialog/scheme-editor-dialog.component.ts index 4b870fff7..b742d3528 100644 --- a/apps/frontend/src/app/coding/components/scheme-editor-dialog/scheme-editor-dialog.component.ts +++ b/apps/frontend/src/app/coding/components/scheme-editor-dialog/scheme-editor-dialog.component.ts @@ -33,7 +33,6 @@ export interface SchemeEditorDialogData { template: `

{{ data.fileName }}

-
@if (schemerHtml && !isLoading) { } @else { -
Loading schemer...
+
{{ prettyScheme }}
} -
- - + + `, styles: [` - .editor-container { - height: 80vh; - width: 100%; - overflow: hidden; - } - .loading { - display: flex; - justify-content: center; - align-items: center; + .raw-json { height: 100%; - font-size: 18px; - color: #666; + width: 100%; + box-sizing: border-box; + margin: 0; + padding: 12px; + border-radius: 4px; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 12px; + line-height: 1.5; } unit-schemer-standalone { @@ -85,6 +84,17 @@ export class SchemeEditorDialogComponent implements OnInit { schemeType: 'iqb-standard@3.2' }; + get prettyScheme(): string { + const raw = this.unitScheme?.scheme ?? ''; + if (!raw) return ''; + try { + const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; + return JSON.stringify(parsed, null, 2); + } catch { + return raw.toString?.() ?? String(raw); + } + } + constructor( public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: SchemeEditorDialogData, @@ -100,7 +110,6 @@ export class SchemeEditorDialogComponent implements OnInit { scheme: this.data.content, schemeType: 'iqb-standard@3.2' }; - console.log(this.unitScheme); this.backendService.getVariableInfoForScheme(this.data.workspaceId, this.data.fileName) .subscribe({ next: variables => { @@ -175,9 +184,9 @@ export class SchemeEditorDialogComponent implements OnInit { const confirmRef = this.dialog.open(ConfirmDialogComponent, { width: '400px', data: { - title: 'Unsaved Changes', - content: 'You have unsaved changes. Are you sure you want to close?', - confirmButtonLabel: 'Yes', + title: 'Ungespeicherte Änderungen', + content: 'Sie haben ungespeicherte Änderungen. Möchten Sie wirklich schließen?', + confirmButtonLabel: 'Ja', showCancel: true } }); @@ -192,7 +201,6 @@ export class SchemeEditorDialogComponent implements OnInit { } } - save(): void { if (!this.hasChanges) { this.dialogRef.close(false); diff --git a/apps/frontend/src/app/coding/components/variable-analysis-dialog/variable-analysis-dialog.component.ts b/apps/frontend/src/app/coding/components/variable-analysis-dialog/variable-analysis-dialog.component.ts index 8d2e5a6a7..43cac1c17 100644 --- a/apps/frontend/src/app/coding/components/variable-analysis-dialog/variable-analysis-dialog.component.ts +++ b/apps/frontend/src/app/coding/components/variable-analysis-dialog/variable-analysis-dialog.component.ts @@ -13,7 +13,9 @@ import { MatDividerModule } from '@angular/material/divider'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatSort, MatSortModule } from '@angular/material/sort'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; @@ -24,6 +26,7 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { BackendService } from '../../../services/backend.service'; import { AppService } from '../../../services/app.service'; import { VariableAnalysisItemDto } from '../../../../../../../api-dto/coding/variable-analysis-item.dto'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; export interface VariableAnalysisDialogData { workspaceId: number; @@ -40,6 +43,9 @@ export interface VariableAnalysisDialogData { templateUrl: './variable-analysis-dialog.component.html', styleUrls: ['./variable-analysis-dialog.component.scss'], standalone: true, + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ CommonModule, FormsModule, @@ -58,7 +64,6 @@ export interface VariableAnalysisDialogData { ] }) export class VariableAnalysisDialogComponent implements OnInit { - // Variable analysis data variableAnalysisData: VariableAnalysisItemDto[] = []; variableAnalysisDataSource = new MatTableDataSource([]); variableAnalysisColumns: string[] = [ @@ -68,22 +73,12 @@ export class VariableAnalysisDialogComponent implements OnInit { ]; totalVariableAnalysisRecords = 0; - variableAnalysisPageIndex = 0; - variableAnalysisPageSize = 100; - variableAnalysisPageSizeOptions = [10, 25, 50, 100, 200]; - - // Filters unitIdFilter = ''; - variableIdFilter = ''; - - // Loading state isLoadingVariableAnalysis = false; - - // Filter debounce variableAnalysisFilterChanged = new Subject(); @ViewChild(MatSort) sort!: MatSort; @@ -99,7 +94,6 @@ export class VariableAnalysisDialogComponent implements OnInit { ) {} ngOnInit(): void { - // Set up filter debounce this.variableAnalysisFilterChanged.pipe( debounceTime(500), distinctUntilChanged() @@ -107,7 +101,6 @@ export class VariableAnalysisDialogComponent implements OnInit { this.fetchVariableAnalysis(1, this.variableAnalysisPageSize); }); - // If initial data is provided, use it if (this.data.initialData) { this.variableAnalysisData = this.data.initialData.data; this.variableAnalysisDataSource.data = this.data.initialData.data; @@ -115,7 +108,6 @@ export class VariableAnalysisDialogComponent implements OnInit { this.variableAnalysisPageIndex = this.data.initialData.page - 1; // MatPaginator uses 0-based index this.variableAnalysisPageSize = this.data.initialData.limit; } else { - // Otherwise fetch the data this.fetchVariableAnalysis(1, this.variableAnalysisPageSize); } } @@ -124,7 +116,6 @@ export class VariableAnalysisDialogComponent implements OnInit { const workspaceId = this.data.workspaceId; this.isLoadingVariableAnalysis = true; - // Get filter values, trimming whitespace and only passing non-empty values const unitId = this.unitIdFilter.trim() || undefined; const variableId = this.variableIdFilter.trim() || undefined; @@ -143,7 +134,6 @@ export class VariableAnalysisDialogComponent implements OnInit { this.variableAnalysisPageIndex = response.page - 1; // MatPaginator uses 0-based index this.variableAnalysisPageSize = response.limit; - // Set up sorting for the variable analysis table setTimeout(() => { if (this.sort) { this.variableAnalysisDataSource.sort = this.sort; @@ -171,8 +161,6 @@ export class VariableAnalysisDialogComponent implements OnInit { clearVariableAnalysisFilters(): void { this.unitIdFilter = ''; this.variableIdFilter = ''; - - // Reset to first page and refresh data this.fetchVariableAnalysis(1, this.variableAnalysisPageSize); } diff --git a/apps/frontend/src/app/coding/components/variable-bundle-manager/variable-bundle-manager.component.ts b/apps/frontend/src/app/coding/components/variable-bundle-manager/variable-bundle-manager.component.ts index db422001e..43049224d 100644 --- a/apps/frontend/src/app/coding/components/variable-bundle-manager/variable-bundle-manager.component.ts +++ b/apps/frontend/src/app/coding/components/variable-bundle-manager/variable-bundle-manager.component.ts @@ -22,18 +22,24 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { MatCheckbox } from '@angular/material/checkbox'; import { MatAnchor, MatButton, MatIconButton } from '@angular/material/button'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { SearchFilterComponent } from '../../../shared/search-filter/search-filter.component'; import { VariableBundle } from '../../models/coding-job.model'; import { VariableBundleService, PaginatedBundles } from '../../services/variable-bundle.service'; import { VariableBundleDialogComponent } from '../variable-bundle-dialog/variable-bundle-dialog.component'; import { ConfirmDialogComponent, ConfirmDialogData } from '../../../shared/dialogs/confirm-dialog.component'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; @Component({ selector: 'coding-box-variable-bundle-manager', templateUrl: './variable-bundle-manager.component.html', styleUrls: ['./variable-bundle-manager.component.scss'], standalone: true, + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ CommonModule, TranslateModule, @@ -98,7 +104,6 @@ export class VariableBundleManagerComponent implements OnInit, AfterViewInit { this.variableBundleGroupService.getBundles(page, pageSize).subscribe({ next: (paginatedResult: PaginatedBundles) => { - // Always set the data first this.dataSource.data = paginatedResult.bundles; if (this.currentFilter) { @@ -125,7 +130,7 @@ export class VariableBundleManagerComponent implements OnInit, AfterViewInit { } onPageChange(event: PageEvent): void { - const page = event.pageIndex + 1; // MatPaginator is zero-based + const page = event.pageIndex + 1; const pageSize = event.pageSize; this.loadVariableBundleGroups(page, pageSize); } diff --git a/apps/frontend/src/app/coding/services/coder.service.ts b/apps/frontend/src/app/coding/services/coder.service.ts index 6a8d09a2b..cd2ee9299 100644 --- a/apps/frontend/src/app/coding/services/coder.service.ts +++ b/apps/frontend/src/app/coding/services/coder.service.ts @@ -5,6 +5,7 @@ import { catchError, map } from 'rxjs/operators'; import { Coder } from '../models/coder.model'; import { SERVER_URL } from '../../injection-tokens'; import { AppService } from '../../services/app.service'; +import { CodingJob } from '../models/coding-job.model'; @Injectable({ providedIn: 'root' @@ -18,7 +19,6 @@ export class CoderService { getCoders(): Observable { const workspaceId = this.appService.selectedWorkspaceId; if (!workspaceId) { - console.error('No workspace ID available'); return of([]); } const baseUrl = this.serverUrl.endsWith('/') ? this.serverUrl.slice(0, -1) : this.serverUrl; @@ -33,24 +33,16 @@ export class CoderService { this.http.get<{ data: WorkspaceUser[], total: number }>(url).subscribe({ next: response => { - // Map the workspace users with accessLevel 1 to Coder objects const coders: Coder[] = response.data.map(user => ({ id: user.userId, - name: user.username || `User ${user.userId}`, // Use username if available, otherwise fallback to default - displayName: user.username || `Coder ${user.userId}`, // Use username if available, otherwise fallback to default + name: user.username || `User ${user.userId}`, + displayName: user.username || `Coder ${user.userId}`, assignedJobs: [] })); - - // Update the subject with the fetched coders this.codersSubject.next(coders); - }, - error: error => { - console.error('Error fetching coders:', error); - // Keep the current value in case of error } }); - // Return the observable from the subject return this.codersSubject.asObservable(); } @@ -87,10 +79,6 @@ export class CoderService { return of(updatedCoder); } - /** - * Deletes a coder - * @param id The ID of the coder to delete - */ deleteCoder(id: number): Observable { const coders = this.codersSubject.value; const updatedCoders = coders.filter(c => c.id !== id); @@ -104,119 +92,21 @@ export class CoderService { return of(true); } - /** - * Assigns a coding job to a coder - * @param coderId The ID of the coder - * @param jobId The ID of the coding job - */ - assignJob(coderId: number, jobId: number): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - console.error('No workspace ID available'); - return of(undefined); - } - - // Remove trailing slash from serverUrl if present to avoid double slashes - const baseUrl = this.serverUrl.endsWith('/') ? this.serverUrl.slice(0, -1) : this.serverUrl; - const url = `${baseUrl}/admin/workspace/${workspaceId}/coding-jobs/${jobId}/assign/${coderId}`; - - return this.http.post<{ success: boolean }>(url, {}).pipe( - map(() => { - // Update the local state after successful assignment - const coders = this.codersSubject.value; - const index = coders.findIndex(c => c.id === coderId); - - if (index === -1) { - return undefined; - } - - const coder = coders[index]; - const assignedJobs = coder.assignedJobs || []; - - // Only add the job if it's not already assigned - if (!assignedJobs.includes(jobId)) { - const updatedCoder: Coder = { - ...coder, - assignedJobs: [...assignedJobs, jobId] - }; - - const updatedCoders = [...coders]; - updatedCoders[index] = updatedCoder; - - this.codersSubject.next(updatedCoders); - - return updatedCoder; - } - - return coder; - }), - catchError(error => { - console.error(`Error assigning job ${jobId} to coder ${coderId}:`, error); - return of(undefined); - }) - ); - } - - /** - * Unassigns a coding job from a coder - * @param coderId The ID of the coder - * @param jobId The ID of the coding job - */ - unassignJob(coderId: number, jobId: number): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - console.error('No workspace ID available'); - return of(undefined); - } - - // Remove trailing slash from serverUrl if present to avoid double slashes + getJobsByCoderId(coderId: number): Observable { const baseUrl = this.serverUrl.endsWith('/') ? this.serverUrl.slice(0, -1) : this.serverUrl; - const url = `${baseUrl}/admin/workspace/${workspaceId}/coding-jobs/${jobId}/unassign/${coderId}`; + const url = `${baseUrl}/admin/coding-jobs/${coderId}/coders`; - return this.http.delete<{ success: boolean }>(url).pipe( - map(() => { - // Update the local state after successful unassignment - const coders = this.codersSubject.value; - const index = coders.findIndex(c => c.id === coderId); - - if (index === -1) { - return undefined; - } - - const coder = coders[index]; - const assignedJobs = coder.assignedJobs || []; - - const updatedCoder: Coder = { - ...coder, - assignedJobs: assignedJobs.filter(id => id !== jobId) - }; - - const updatedCoders = [...coders]; - updatedCoders[index] = updatedCoder; - - this.codersSubject.next(updatedCoders); - - return updatedCoder; - }), - catchError(error => { - console.error(`Error unassigning job ${jobId} from coder ${coderId}:`, error); - return of(undefined); - }) + return this.http.get<{ data: CodingJob[], total: number }>(url).pipe( + map(response => response.data) ); } - /** - * Gets all coders assigned to a specific job - * @param jobId The ID of the coding job - */ getCodersByJobId(jobId: number): Observable { const workspaceId = this.appService.selectedWorkspaceId; if (!workspaceId) { - console.error('No workspace ID available'); return of([]); } - // Remove trailing slash from serverUrl if present to avoid double slashes const baseUrl = this.serverUrl.endsWith('/') ? this.serverUrl.slice(0, -1) : this.serverUrl; const url = `${baseUrl}/admin/workspace/${workspaceId}/coding-jobs/${jobId}/coders`; @@ -229,7 +119,6 @@ export class CoderService { return this.http.get<{ data: WorkspaceUser[], total: number }>(url).pipe( map(response => { - // Map WorkspaceUser objects to Coder objects const fetchedCoders: Coder[] = response.data.map(user => ({ id: user.userId, name: user.username || `User ${user.userId}`, @@ -237,34 +126,27 @@ export class CoderService { assignedJobs: [jobId] })); - // Merge with existing coders to maintain other properties const existingCoders = this.codersSubject.value; const mergedCoders = [...existingCoders]; fetchedCoders.forEach(fetchedCoder => { const index = mergedCoders.findIndex(c => c.id === fetchedCoder.id); if (index !== -1) { - // Update existing coder mergedCoders[index] = { ...mergedCoders[index], ...fetchedCoder, assignedJobs: [...(mergedCoders[index].assignedJobs || []), jobId] }; } else { - // Add new coder mergedCoders.push(fetchedCoder); } }); - // Update the subject with the merged coders this.codersSubject.next(mergedCoders); return fetchedCoders; }), - catchError(error => { - console.error(`Error fetching coders for job ${jobId}:`, error); - - // Fallback to local data if API call fails + catchError(() => { const coders = this.codersSubject.value.filter( coder => coder.assignedJobs?.includes(jobId) ); @@ -273,9 +155,6 @@ export class CoderService { ); } - /** - * Gets the next available ID for a new coder - */ private getNextId(): number { const coders = this.codersSubject.value; return coders.length > 0 ? diff --git a/apps/frontend/src/app/coding/services/coding-job.service.ts b/apps/frontend/src/app/coding/services/coding-job.service.ts index e1ad25be3..e9e7352b2 100644 --- a/apps/frontend/src/app/coding/services/coding-job.service.ts +++ b/apps/frontend/src/app/coding/services/coding-job.service.ts @@ -6,6 +6,7 @@ import { import { CodingJob } from '../models/coding-job.model'; import { SERVER_URL } from '../../injection-tokens'; import { AppService } from '../../services/app.service'; +import { ResponseEntity } from '../../shared/models/response-entity.model'; @Injectable({ providedIn: 'root' @@ -16,151 +17,6 @@ export class CodingJobService { private appService = inject(AppService); private codingJobsSubject = new BehaviorSubject([]); - /** - * Gets all coding jobs for the current workspace - */ - getCodingJobs(): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - return of([]); - } - - const url = `${this.serverUrl}/admin/workspace/${workspaceId}/coding-jobs`; - - this.http.get<{ data: CodingJob[], total: number }>(url).subscribe({ - next: response => { - // Map the response data to CodingJob objects - const codingJobs: CodingJob[] = response.data.map(job => ({ - ...job, - createdAt: new Date(job.createdAt), - updatedAt: new Date(job.updatedAt) - })); - - // Update the subject with the fetched coding jobs - this.codingJobsSubject.next(codingJobs); - }, - error: () => { - // Keep the current value in case of error - } - }); - - // Return the observable from the subject - return this.codingJobsSubject.asObservable(); - } - - /** - * Gets a coding job by ID - * @param id The ID of the coding job - */ - getCodingJob(id: number): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - return of(undefined); - } - - const url = `${this.serverUrl}/admin/workspace/${workspaceId}/coding-jobs/${id}`; - - return this.http.get(url).pipe( - map(job => ({ - ...job, - createdAt: new Date(job.createdAt), - updatedAt: new Date(job.updatedAt) - })), - catchError(() => of(undefined)) - ); - } - - /** - * Creates a new coding job - * @param job The coding job to create - */ - createCodingJob(job: Omit): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - return of(undefined); - } - - const url = `${this.serverUrl}/admin/workspace/${workspaceId}/coding-jobs`; - - return this.http.post(url, job).pipe( - map(newJob => ({ - ...newJob, - createdAt: new Date(newJob.createdAt), - updatedAt: new Date(newJob.updatedAt) - })), - tap(newJob => { - if (newJob) { - const currentJobs = this.codingJobsSubject.value; - this.codingJobsSubject.next([...currentJobs, newJob]); - } - }), - catchError(() => of(undefined)) - ); - } - - /** - * Updates a coding job - * @param id The ID of the coding job to update - * @param job The updated coding job data - */ - updateCodingJob(id: number, job: Partial): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - return of(undefined); - } - - const url = `${this.serverUrl}/admin/workspace/${workspaceId}/coding-jobs/${id}`; - - return this.http.put(url, job).pipe( - map(updatedJob => ({ - ...updatedJob, - createdAt: new Date(updatedJob.createdAt), - updatedAt: new Date(updatedJob.updatedAt) - })), - tap(updatedJob => { - if (updatedJob) { - const currentJobs = this.codingJobsSubject.value; - const index = currentJobs.findIndex(j => j.id === id); - if (index !== -1) { - const updatedJobs = [...currentJobs]; - updatedJobs[index] = updatedJob; - this.codingJobsSubject.next(updatedJobs); - } - } - }), - catchError(() => of(undefined)) - ); - } - - /** - * Deletes a coding job - * @param id The ID of the coding job to delete - */ - deleteCodingJob(id: number): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - return of(false); - } - - const url = `${this.serverUrl}/admin/workspace/${workspaceId}/coding-jobs/${id}`; - - return this.http.delete<{ success: boolean }>(url).pipe( - map(response => response.success), - tap(success => { - if (success) { - const currentJobs = this.codingJobsSubject.value; - this.codingJobsSubject.next(currentJobs.filter(job => job.id !== id)); - } - }), - catchError(() => of(false)) - ); - } - - /** - * Assigns a coder to a coding job - * @param codingJobId The ID of the coding job - * @param coderId The ID of the coder - */ assignCoder(codingJobId: number, coderId: number): Observable { const workspaceId = this.appService.selectedWorkspaceId; if (!workspaceId) { @@ -190,63 +46,10 @@ export class CodingJobService { ); } - /** - * Unassigns a coder from a coding job - * @param codingJobId The ID of the coding job - * @param coderId The ID of the coder - */ - unassignCoder(codingJobId: number, coderId: number): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - return of(undefined); - } - - const url = `${this.serverUrl}/admin/workspace/${workspaceId}/coding-jobs/${codingJobId}/assign/${coderId}`; - - return this.http.delete(url).pipe( - map(updatedJob => ({ - ...updatedJob, - createdAt: new Date(updatedJob.createdAt), - updatedAt: new Date(updatedJob.updatedAt) - })), - tap(updatedJob => { - if (updatedJob) { - const currentJobs = this.codingJobsSubject.value; - const index = currentJobs.findIndex(j => j.id === codingJobId); - if (index !== -1) { - const updatedJobs = [...currentJobs]; - updatedJobs[index] = updatedJob; - this.codingJobsSubject.next(updatedJobs); - } - } - }), - catchError(() => of(undefined)) - ); - } - - /** - * Gets all coding jobs assigned to a coder - * @param coderId The ID of the coder - */ - getCodingJobsByCoder(coderId: number): Observable { - const workspaceId = this.appService.selectedWorkspaceId; - if (!workspaceId) { - console.error('No workspace ID available'); - return of([]); - } - - const url = `${this.serverUrl}/admin/workspace/${workspaceId}/coders/${coderId}/coding-jobs`; - - return this.http.get<{ data: CodingJob[] }>(url).pipe( - map(response => response.data.map(job => ({ - ...job, - createdAt: new Date(job.createdAt), - updatedAt: new Date(job.updatedAt) - }))), - catchError(error => { - console.error(`Error fetching coding jobs for coder ${coderId}:`, error); - return of([]); - }) + getResponsesForCodingJob(codingJobId: number): Observable { + const url = `${this.serverUrl}admin/coding-jobs/${codingJobId}/responses`; + return this.http.get<{ data: ResponseEntity[] }>(url).pipe( + map(response => response.data) ); } } diff --git a/apps/frontend/src/app/coding/services/test-person-coding.service.ts b/apps/frontend/src/app/coding/services/test-person-coding.service.ts index 56a4cf80f..34a47edf9 100644 --- a/apps/frontend/src/app/coding/services/test-person-coding.service.ts +++ b/apps/frontend/src/app/coding/services/test-person-coding.service.ts @@ -13,6 +13,7 @@ import { import { ValidateCodingCompletenessRequestDto } from '../../../../../../api-dto/coding/validate-coding-completeness-request.dto'; +import { ExternalCodingImportResultDto } from '../../../../../../api-dto/coding/external-coding-import-result.dto'; export interface CodingStatistics { totalResponses: number; @@ -358,4 +359,147 @@ export class TestPersonCodingService { }) ); } + + /** + * Import external coding with real-time progress updates via streaming response + * @param workspaceId Workspace ID + * @param data File data containing file and fileName + * @param onProgress Callback for progress updates + * @param onComplete Callback for completion + * @param onError Callback for errors + */ + async importExternalCodingWithProgress( + workspaceId: number, + data: { file: string; fileName: string }, + onProgress: (progress: number, message: string) => void, + onComplete: (result: ExternalCodingImportResultDto) => void, + onError: (error: string) => void + ): Promise { + try { + const response = await fetch( + `${this.serverUrl}admin/workspace/${workspaceId}/coding/external-coding-import/stream`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.authHeader + }, + body: JSON.stringify(data) + } + ); + + if (!response.ok) { + onError(`HTTP ${response.status}: ${response.statusText}`); + return; + } + + if (!response.body) { + onError('No response body available for streaming'); + return; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + try { + let done = false; + while (!done) { + const result = await reader.read(); + done = result.done; + const value = result.value; + + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + // Process complete lines from the buffer + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.startsWith('data: ')) { + try { + const progressData = JSON.parse(line.substring(6)); + + if (progressData.error) { + onError(progressData.message); + return; + } if (progressData.result) { + onComplete(progressData.result); + return; + } + onProgress(progressData.progress, progressData.message); + } catch (parseError) { + // Skip invalid SSE data lines silently + } + } + } + } + } finally { + reader.releaseLock(); + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + onError(`Failed to start import: ${errorMessage}`); + } + } + + /** + * Generate coder training packages based on CODING_INCOMPLETE responses + * @param workspaceId Workspace ID + * @param selectedCoders Array of selected coders with id and name + * @param variableConfigs Array of variable configurations with variableName and sampleCount + * @returns Observable of coder training packages + */ + generateCoderTrainingPackages( + workspaceId: number, + selectedCoders: { id: number; name: string }[], + variableConfigs: { variableName: string; sampleCount: number }[] + ): Observable<{ + coderId: number; + coderName: string; + responses: { + responseId: number; + unitAlias: string; + variableId: string; + unitName: string; + value: string; + personLogin: string; + personCode: string; + personGroup: string; + bookletName: string; + variable: string; + }[]; + }[]> { + const request = { + selectedCoders, + variableConfigs + }; + + return this.http + .post<{ + coderId: number; + coderName: string; + responses: { + responseId: number; + unitAlias: string; + variableId: string; + unitName: string; + value: string; + personLogin: string; + personCode: string; + personGroup: string; + bookletName: string; + variable: string; + }[]; + }[]>( + `${this.serverUrl}admin/workspace/${workspaceId}/coding/coder-training-packages`, + request, + { headers: this.authHeader } + ) + .pipe( + catchError(() => of([])) + ); + } } diff --git a/apps/frontend/src/app/components/home/home.component.html b/apps/frontend/src/app/components/home/home.component.html index 1349577d6..4b4a84fdf 100755 --- a/apps/frontend/src/app/components/home/home.component.html +++ b/apps/frontend/src/app/components/home/home.component.html @@ -9,7 +9,7 @@ [appTitle]="'Web application for coding'" [introHtml]="'appService.appConfig.introHtml'" [appName]="'IQB-Kodierbox'" - [appVersion]="'1.0.0'" + [appVersion]="'1.1.0'" [userName]="authData.userName" [userLongName]="appService.userProfile.firstName + ' ' + appService.userProfile.lastName" [isUserLoggedIn]="Number(authData.userId) > 0" diff --git a/apps/frontend/src/app/core/guards/admin.guard.ts b/apps/frontend/src/app/core/guards/admin.guard.ts new file mode 100644 index 000000000..f28e4a6ab --- /dev/null +++ b/apps/frontend/src/app/core/guards/admin.guard.ts @@ -0,0 +1,28 @@ +import { + ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateFn, UrlTree +} from '@angular/router'; +import { createAuthGuard, AuthGuardData } from 'keycloak-angular'; +import { inject } from '@angular/core'; +import { AuthService } from '../services/auth.service'; + +const isAdminAccessAllowed = async ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + authData: AuthGuardData +): Promise => { + const { authenticated } = authData; + if (!authenticated) { + return false; + } + + const authService = inject(AuthService); + const userRoles = authService.getRoles(); + + const adminRoles = ['admin', 'system-admin', 'sys-admin', 'administrator']; + const hasAdminRole = userRoles.some((role : string) => adminRoles.includes(role.toLowerCase()) + ); + + return hasAdminRole; +}; + +export const canActivateAdmin = createAuthGuard(isAdminAccessAllowed); diff --git a/apps/frontend/src/app/core/services/auth.service.ts b/apps/frontend/src/app/core/services/auth.service.ts index 9276a57e3..6fc03884a 100755 --- a/apps/frontend/src/app/core/services/auth.service.ts +++ b/apps/frontend/src/app/core/services/auth.service.ts @@ -1,7 +1,10 @@ import { inject, Injectable } from '@angular/core'; import Keycloak, { KeycloakProfile, KeycloakTokenParsed } from 'keycloak-js'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) + export class AuthService { private readonly keycloak = inject(Keycloak); getLoggedUser(): KeycloakTokenParsed | undefined { diff --git a/apps/frontend/src/app/services/backend.service.ts b/apps/frontend/src/app/services/backend.service.ts index a51e917c9..0affff173 100755 --- a/apps/frontend/src/app/services/backend.service.ts +++ b/apps/frontend/src/app/services/backend.service.ts @@ -5,7 +5,6 @@ import { map } from 'rxjs/operators'; import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; import { FilesInListDto } from 'api-dto/files/files-in-list.dto'; import { UnitNoteDto } from 'api-dto/unit-notes/unit-note.dto'; -import { UpdateUnitTagDto } from 'api-dto/unit-tags/update-unit-tag.dto'; import { UnitTagDto } from 'api-dto/unit-tags/unit-tag.dto'; import { CreateUnitTagDto } from 'api-dto/unit-tags/create-unit-tag.dto'; import { CreateWorkspaceDto } from 'api-dto/workspaces/create-workspace-dto'; @@ -25,8 +24,7 @@ import { TestResultService } from './test-result.service'; import { ResourcePackageService } from './resource-package.service'; import { ValidationService } from './validation.service'; import { UnitService } from './unit.service'; -// eslint-disable-next-line import/no-cycle -import { ImportService } from './import.service'; +import { ImportService, ImportOptions, Result } from './import.service'; import { AuthenticationService } from './authentication.service'; import { VariableAnalysisService, VariableAnalysisResultDto } from './variable-analysis.service'; import { VariableAnalysisJobDto } from '../models/variable-analysis-job.dto'; @@ -44,8 +42,6 @@ import { UserWorkspaceAccessDto } from '../../../../../api-dto/workspaces/user-w import { UserInListDto } from '../../../../../api-dto/user/user-in-list-dto'; import { ResourcePackageDto } from '../../../../../api-dto/resource-package/resource-package-dto'; import { TestTakersValidationDto } from '../../../../../api-dto/files/testtakers-validation.dto'; -import { ImportOptions, Result } from '../ws-admin/components/test-center-import/test-center-import.component'; -import { UpdateUnitNoteDto } from '../../../../../api-dto/unit-notes/update-unit-note.dto'; import { ResponseDto } from '../../../../../api-dto/responses/response-dto'; import { InvalidVariableDto } from '../../../../../api-dto/files/variable-validation.dto'; import { BookletInfoDto } from '../../../../../api-dto/booklet-info/booklet-info.dto'; @@ -53,10 +49,8 @@ import { UnitInfoDto } from '../../../../../api-dto/unit-info/unit-info.dto'; import { CodeBookContentSetting } from '../../../../../api-dto/coding/codebook-content-setting'; import { MissingsProfilesDto } from '../../../../../api-dto/coding/missings-profiles.dto'; import { VariableAnalysisItemDto } from '../../../../../api-dto/coding/variable-analysis-item.dto'; +import { ResponseEntity } from '../shared/models/response-entity.model'; -/** - * Response type for replay statistics - */ type ReplayStatisticsResponse = { id: number; timestamp: string; @@ -90,31 +84,6 @@ export interface CodingListItem { url: string; } -interface ResponseEntity { - id: number; - unitId: number; - variableId: string; - status: string; - value: string; - subform: string; - code: number; - score: number; - codedStatus: string; - unit?: { - name: string; - alias: string; - booklet?: { - person?: { - login: string; - code: string; - }; - bookletinfo?: { - name: string; - }; - }; - }; -} - @Injectable({ providedIn: 'root' }) @@ -228,30 +197,6 @@ export class BackendService { return this.codingService.getCodingJobStatus(workspace_id, jobId); } - cancelCodingJob(workspace_id: number, jobId: string): Observable<{ - success: boolean; - message: string; - }> { - return this.codingService.cancelCodingJob(workspace_id, jobId); - } - - getAllCodingJobs(workspace_id: number): Observable<{ - jobId: string; - status: 'pending' | 'processing' | 'completed' | 'failed' | 'cancelled' | 'paused'; - progress: number; - result?: { - totalResponses: number; - statusCounts: { - [key: string]: number; - }; - }; - error?: string; - workspaceId?: number; - createdAt?: Date; - }[]> { - return this.codingService.getAllCodingJobs(workspace_id); - } - getCodingList(workspace_id: number, page: number = 1, limit: number = 100): Observable> { return this.codingService.getCodingList(workspace_id, page, limit); } @@ -308,24 +253,10 @@ export class BackendService { return this.userService.setUserWorkspaceAccessRight(userId, workspaceIds); } - // Unit Tags API methods - createUnitTag(workspaceId: number, createUnitTagDto: CreateUnitTagDto): Observable { return this.unitTagService.createUnitTag(workspaceId, createUnitTagDto); } - getUnitTags(workspaceId: number, unitId: number): Observable { - return this.unitTagService.getUnitTags(workspaceId, unitId); - } - - getUnitTag(workspaceId: number, tagId: number): Observable { - return this.unitTagService.getUnitTag(workspaceId, tagId); - } - - updateUnitTag(workspaceId: number, tagId: number, updateUnitTagDto: UpdateUnitTagDto): Observable { - return this.unitTagService.updateUnitTag(workspaceId, tagId, updateUnitTagDto); - } - deleteUnitTag(workspaceId: number, tagId: number): Observable { return this.unitTagService.deleteUnitTag(workspaceId, tagId); } @@ -342,14 +273,6 @@ export class BackendService { return this.unitNoteService.getNotesForMultipleUnits(workspaceId, unitIds); } - getUnitNote(workspaceId: number, noteId: number): Observable { - return this.unitNoteService.getUnitNote(workspaceId, noteId); - } - - updateUnitNote(workspaceId: number, noteId: number, updateUnitNoteDto: UpdateUnitNoteDto): Observable { - return this.unitNoteService.updateUnitNote(workspaceId, noteId, updateUnitNoteDto); - } - deleteUnitNote(workspaceId: number, noteId: number): Observable { return this.unitNoteService.deleteUnitNote(workspaceId, noteId); } @@ -583,16 +506,6 @@ export class BackendService { return this.responseService.deleteResponse(workspaceId, responseId); } - deleteMultipleResponses(workspaceId: number, responseIds: number[]): Observable<{ - success: boolean; - report: { - deletedResponses: number[]; - warnings: string[]; - }; - }> { - return this.responseService.deleteMultipleResponses(workspaceId, responseIds); - } - deleteBooklet(workspaceId: number, bookletId: number): Observable<{ success: boolean; report: { @@ -630,14 +543,6 @@ export class BackendService { return this.validationService.validateGroupResponses(workspaceId, page, limit); } - deleteInvalidResponses(workspaceId: number, responseIds: number[]): Observable { - return this.validationService.deleteInvalidResponses(workspaceId, responseIds); - } - - deleteAllInvalidResponses(workspaceId: number, validationType: 'variables' | 'variableTypes' | 'responseStatus' | 'duplicateResponses'): Observable { - return this.validationService.deleteAllInvalidResponses(workspaceId, validationType); - } - createVariableAnalysisJob( workspaceId: number, unitId?: number, @@ -650,13 +555,6 @@ export class BackendService { ); } - getVariableAnalysisJob( - workspaceId: number, - jobId: number - ): Observable { - return this.variableAnalysisService.getAnalysisJob(workspaceId, jobId); - } - getVariableAnalysisResults( workspaceId: number, jobId: number diff --git a/apps/frontend/src/app/services/coding.service.ts b/apps/frontend/src/app/services/coding.service.ts index 70344a338..d832349b7 100644 --- a/apps/frontend/src/app/services/coding.service.ts +++ b/apps/frontend/src/app/services/coding.service.ts @@ -12,6 +12,7 @@ import { AppService } from './app.service'; import { CodeBookContentSetting } from '../../../../../api-dto/coding/codebook-content-setting'; import { MissingsProfilesDto } from '../../../../../api-dto/coding/missings-profiles.dto'; import { VariableAnalysisItemDto } from '../../../../../api-dto/coding/variable-analysis-item.dto'; +import { ResponseEntity } from '../shared/models/response-entity.model'; interface PaginatedResponse { data: T[]; @@ -32,31 +33,6 @@ export interface CodingListItem { url: string; } -interface ResponseEntity { - id: number; - unitId: number; - variableId: string; - status: string; - value: string; - subform: string; - code: number; - score: number; - codedStatus: string; - unit?: { - name: string; - alias: string; - booklet?: { - person?: { - login: string; - code: string; - }; - bookletinfo?: { - name: string; - }; - }; - }; -} - @Injectable({ providedIn: 'root' }) diff --git a/apps/frontend/src/app/services/import.service.ts b/apps/frontend/src/app/services/import.service.ts index c58eb2bfe..d3bb0eaad 100644 --- a/apps/frontend/src/app/services/import.service.ts +++ b/apps/frontend/src/app/services/import.service.ts @@ -6,13 +6,36 @@ import { of } from 'rxjs'; import { SERVER_URL } from '../injection-tokens'; -// eslint-disable-next-line import/no-cycle -import { - ImportOptions, - Result -} from '../ws-admin/components/test-center-import/test-center-import.component'; import { TestGroupsInfoDto } from '../../../../../api-dto/files/test-groups-info.dto'; +export type ImportOptions = { + responses:string, + definitions:string, + units:string, + player:string, + codings:string, + logs:string, + testTakers:string, + booklets:string +}; + +export type Result = { + success: boolean, + testFiles: number, + responses: number, + logs: number, + booklets: number, + units: number, + persons: number, + importedGroups: string[], + filesPlayer?: number, + filesUnits?: number, + filesDefinitions?: number, + filesCodings?: number, + filesBooklets?: number, + filesTestTakers?: number +}; + @Injectable({ providedIn: 'root' }) @@ -64,7 +87,13 @@ export class ImportService { booklets: 0, units: 0, persons: 0, - importedGroups: [] + importedGroups: [], + filesPlayer: 0, + filesUnits: 0, + filesDefinitions: 0, + filesCodings: 0, + filesBooklets: 0, + filesTestTakers: 0 })) ); } diff --git a/apps/frontend/src/app/services/test-result.service.ts b/apps/frontend/src/app/services/test-result.service.ts index 61928dcc2..f03c30d97 100644 --- a/apps/frontend/src/app/services/test-result.service.ts +++ b/apps/frontend/src/app/services/test-result.service.ts @@ -2,8 +2,6 @@ import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { catchError, - forkJoin, - map, Observable, of } from 'rxjs'; @@ -11,6 +9,24 @@ import { logger } from 'nx/src/utils/logger'; import { SERVER_URL } from '../injection-tokens'; import { TestResultCacheService } from './test-result-cache.service'; +interface TestResultsResponse { + data: TestResultItem[]; + total: number; +} + +interface TestResultItem { + id: number; + code: string; + group: string; + login: string; + uploaded_at: Date; + [key: string]: unknown; +} + +interface PersonTestResult { + [key: string]: unknown; +} + @Injectable({ providedIn: 'root' }) @@ -29,23 +45,14 @@ export class TestResultService { { headers: this.authHeader }); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getTestResults(workspaceId: number, page: number, limit: number, searchText?: string): Observable { - // Use the cache service to get test results + getTestResults(workspaceId: number, page: number, limit: number, searchText?: string): Observable { return this.cacheService.getTestResults(workspaceId, page, limit, searchText); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getPersonTestResults(workspaceId: number, personId: number): Observable { - // Use the cache service to get person test results + getPersonTestResults(workspaceId: number, personId: number): Observable { return this.cacheService.getPersonTestResults(workspaceId, personId); } - /** - * Invalidate the cache for a specific workspace - * This should be called whenever test results are modified - * @param workspaceId The workspace ID - */ invalidateCache(workspaceId: number): void { this.cacheService.invalidateWorkspaceCache(workspaceId); } @@ -164,12 +171,6 @@ export class TestResultService { ); } - /** - * Delete a unit and all its associated responses - * @param workspaceId The ID of the workspace - * @param unitId The ID of the unit to delete - * @returns An Observable of the deletion result - */ deleteUnit(workspaceId: number, unitId: number): Observable<{ success: boolean; report: { @@ -194,61 +195,6 @@ export class TestResultService { ); } - /** - * Delete multiple units and all their associated responses - * @param workspaceId The ID of the workspace - * @param unitIds Array of unit IDs to delete - * @returns An Observable of the deletion result - */ - deleteMultipleUnits(workspaceId: number, unitIds: number[]): Observable<{ - success: boolean; - report: { - deletedUnits: number[]; - warnings: string[]; - }; - }> { - // Create a series of delete requests for each unit - const deleteRequests = unitIds.map(unitId => this.deleteUnit(workspaceId, unitId)); - - // Combine all requests and aggregate the results - return forkJoin(deleteRequests).pipe( - map(results => { - const successfulDeletes = results.filter(result => result.success); - const deletedUnits = successfulDeletes - .map(result => result.report.deletedUnit) - .filter(id => id !== null) as number[]; - - const warnings = results - .filter(result => !result.success || result.report.warnings.length > 0) - .flatMap(result => result.report.warnings); - - return { - success: deletedUnits.length > 0, - report: { - deletedUnits, - warnings - } - }; - }), - catchError(() => { - logger.error('Error deleting multiple units'); - return of({ - success: false, - report: { - deletedUnits: [], - warnings: ['Failed to delete units'] - } - }); - }) - ); - } - - /** - * Delete a booklet and all its associated units and responses - * @param workspaceId The ID of the workspace - * @param bookletId The ID of the booklet to delete - * @returns An Observable of the deletion result - */ deleteBooklet(workspaceId: number, bookletId: number): Observable<{ success: boolean; report: { diff --git a/apps/frontend/src/app/shared/models/response-entity.model.ts b/apps/frontend/src/app/shared/models/response-entity.model.ts index ff9d4c8c1..b28bda1da 100644 --- a/apps/frontend/src/app/shared/models/response-entity.model.ts +++ b/apps/frontend/src/app/shared/models/response-entity.model.ts @@ -1,13 +1,19 @@ export interface ResponseEntity { id: number; unitId: number; - variableId: string; + variableid: string; status: string; value: string; subform: string; code: number; score: number; - codedStatus: string; + codedstatus: string; + status_v2?: string; + code_v2?: number; + score_v2?: number; + status_v3?: string; + code_v3?: number; + score_v3?: number; unit?: { name: string; alias: string; diff --git a/apps/frontend/src/app/shared/services/german-paginator-intl.service.ts b/apps/frontend/src/app/shared/services/german-paginator-intl.service.ts new file mode 100644 index 000000000..600e5349b --- /dev/null +++ b/apps/frontend/src/app/shared/services/german-paginator-intl.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@angular/core'; +import { MatPaginatorIntl } from '@angular/material/paginator'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable() +export class GermanPaginatorIntl extends MatPaginatorIntl { + constructor(private translateService: TranslateService) { + super(); + + this.itemsPerPageLabel = this.translateService.instant('paginator.itemsPerPageLabel'); + this.nextPageLabel = this.translateService.instant('paginator.nextPageLabel'); + this.previousPageLabel = this.translateService.instant('paginator.previousPageLabel'); + this.firstPageLabel = this.translateService.instant('paginator.firstPageLabel'); + this.lastPageLabel = this.translateService.instant('paginator.lastPageLabel'); + + this.translateService.onLangChange.subscribe(() => { + this.itemsPerPageLabel = this.translateService.instant('paginator.itemsPerPageLabel'); + this.nextPageLabel = this.translateService.instant('paginator.nextPageLabel'); + this.previousPageLabel = this.translateService.instant('paginator.previousPageLabel'); + this.firstPageLabel = this.translateService.instant('paginator.firstPageLabel'); + this.lastPageLabel = this.translateService.instant('paginator.lastPageLabel'); + this.changes.next(); + }); + } + + override getRangeLabel = (page: number, pageSize: number, length: number): string => { + if (length === 0 || pageSize === 0) { + return this.translateService.instant('paginator.getRangeLabel', { + startIndex: 0, + endIndex: 0, + length: length + }); + } + + const startIndex = page * pageSize + 1; + const endIndex = Math.min((page + 1) * pageSize, length); + + return this.translateService.instant('paginator.getRangeLabel', { + startIndex: startIndex, + endIndex: endIndex, + length: length + }); + }; +} diff --git a/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.html b/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.html index 4ac0e1fef..a899fa134 100755 --- a/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.html +++ b/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.html @@ -83,7 +83,7 @@

Alternativtext für das Logo

Hintergrundfarbe - + Geben Sie eine Farbe oder einen Farbverlauf im CSS-Format ein
@@ -97,4 +97,34 @@

Alternativtext für das Logo

+ + + + Datenbank-Export + + +
+

+ Exportieren Sie die gesamte PostgreSQL-Datenbank als SQLite-Datei. + Diese Funktion unterstützt auch große Datenbanken mit mehreren GB. +

+
+ +
+ @if (isExporting) { +
+ +

Dies kann bei großen Datenbanken einige Minuten dauern...

+
+ } +
+
+
diff --git a/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.ts b/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.ts index 70f925ebd..6a18b984f 100755 --- a/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.ts +++ b/apps/frontend/src/app/sys-admin/components/sys-admin-settings/sys-admin-settings.component.ts @@ -7,6 +7,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; import { AppService, standardLogo } from '../../../services/app.service'; import { LogoService } from '../../../services/logo.service'; import { AppLogoDto } from '../../../../../../../api-dto/app-logo-dto'; @@ -22,7 +23,8 @@ import { AppLogoDto } from '../../../../../../../api-dto/app-logo-dto'; MatCardModule, MatFormFieldModule, MatIconModule, - MatInputModule + MatInputModule, + MatProgressBarModule ] }) export class SysAdminSettingsComponent { @@ -35,6 +37,7 @@ export class SysAdminSettingsComponent { isDefaultLogo = true; logoAltText = ''; backgroundColorValue = ''; + isExporting = false; private readonly ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp']; constructor() { @@ -43,10 +46,6 @@ export class SysAdminSettingsComponent { this.backgroundColorValue = this.appService.appLogo.bodyBackground || ''; } - /** - * Handles file selection for logo upload - * @param event The file input change event - */ onFileSelected(event: Event): void { const input = event.target as HTMLInputElement; if (input.files && input.files.length > 0) { @@ -64,14 +63,10 @@ export class SysAdminSettingsComponent { return; } - // Create preview this.createImagePreview(); } } - /** - * Creates a preview of the selected image - */ private createImagePreview(): void { if (!this.selectedFile) return; @@ -82,13 +77,9 @@ export class SysAdminSettingsComponent { reader.readAsDataURL(this.selectedFile); } - /** - * Resets the file input - */ resetFileInput(): void { this.selectedFile = null; this.previewUrl = null; - // Reset the file input element const fileInput = document.getElementById('logo-upload') as HTMLInputElement; if (fileInput) { fileInput.value = ''; @@ -172,14 +163,6 @@ export class SysAdminSettingsComponent { }); } - updateBackgroundPreview(): void { - // The preview is automatically updated through data binding - // This method is called when the input changes - } - - /** - * Saves the background color for the application - */ saveBackgroundColor(): void { const updatedLogo = { ...this.appService.appLogo, @@ -201,9 +184,6 @@ export class SysAdminSettingsComponent { }); } - /** - * Resets the background color to the standard linear gradient - */ resetToDefaultBackground(): void { this.backgroundColorValue = standardLogo.bodyBackground || ''; const updatedLogo = { @@ -224,4 +204,56 @@ export class SysAdminSettingsComponent { } }); } + + exportDatabase(): void { + if (this.isExporting) { + return; + } + + this.isExporting = true; + const anchor = document.createElement('a'); + anchor.style.display = 'none'; + document.body.appendChild(anchor); + const apiUrl = `${window.location.origin}/api/admin/database/export/sqlite`; + const token = localStorage.getItem('id_token'); + + if (!token) { + this.snackBar.open('Nicht authentifiziert. Bitte melden Sie sich erneut an.', 'Schließen', { duration: 5000 }); + this.isExporting = false; + return; + } + + fetch(apiUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/x-sqlite3' + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.blob(); + }) + .then(blob => { + const url = window.URL.createObjectURL(blob); + anchor.href = url; + anchor.download = `database-export-${new Date().toISOString().split('T')[0]}.sqlite`; + anchor.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(anchor); + + this.snackBar.open('Datenbank erfolgreich exportiert', 'Schließen', { duration: 3000 }); + }) + .catch(() => { + this.snackBar.open('Fehler beim Exportieren der Datenbank. Bitte versuchen Sie es erneut.', 'Schließen', { duration: 5000 }); + if (document.body.contains(anchor)) { + document.body.removeChild(anchor); + } + }) + .finally(() => { + this.isExporting = false; + }); + } } diff --git a/apps/frontend/src/app/sys-admin/sys-admin.routes.ts b/apps/frontend/src/app/sys-admin/sys-admin.routes.ts index b430360c3..d3ef254d7 100644 --- a/apps/frontend/src/app/sys-admin/sys-admin.routes.ts +++ b/apps/frontend/src/app/sys-admin/sys-admin.routes.ts @@ -1,10 +1,10 @@ import { Routes } from '@angular/router'; -import { canActivateAuth } from '../core/guards/auth.guard'; +import { canActivateAdmin } from '../core/guards/admin.guard'; export const sysAdminRoutes: Routes = [ { path: 'admin', - canActivate: [canActivateAuth], + canActivate: [canActivateAdmin], loadComponent: () => import('./components/admin/admin.component').then(m => m.AdminComponent), children: [ { path: '', redirectTo: 'users', pathMatch: 'full' }, diff --git a/apps/frontend/src/app/workspace/components/user-workspaces/user-workspaces.component.html b/apps/frontend/src/app/workspace/components/user-workspaces/user-workspaces.component.html index 55c9c3f79..9980e530f 100755 --- a/apps/frontend/src/app/workspace/components/user-workspaces/user-workspaces.component.html +++ b/apps/frontend/src/app/workspace/components/user-workspaces/user-workspaces.component.html @@ -8,7 +8,7 @@ @if (workspaces.length > 0) {

Arbeitsbereiche

- @for (ws of workspaces; track ws) { + @for (ws of workspaces; track ws.id) {
-

Booklet Information: {{ data.bookletId }}

+

Booklet-Information: {{ data.bookletId }}

@@ -12,7 +12,7 @@

Booklet Information: {{ data.bookletId }}

-

Loading booklet information...

+

Booklet-Informationen werden geladen...

@@ -25,26 +25,26 @@

Booklet Information: {{ data.bookletId }}

- +
-

Booklet Metadata

+

Booklet-Metadaten

ID: {{ data.bookletInfo.metadata.id }}
- Label: + Bezeichnung: {{ data.bookletInfo.metadata.label }}
- Description: + Beschreibung: {{ data.bookletInfo.metadata.description }}
-

Restrictions

+

Einschränkungen

{{ restriction.type }}: {{ restriction.value }} @@ -55,10 +55,10 @@

Restrictions

+ label="Konfiguration ({{ data.bookletInfo.config.items.length }})">
-

Booklet Configuration

+

Booklet-Konfiguration

{{ configItem.key }} @@ -74,7 +74,7 @@

Booklet Configuration

label="Testlets ({{ data.bookletInfo.testlets.length }})">
-

Testlets in Booklet

+

Testlets im Booklet

@@ -84,7 +84,7 @@

Testlets in Booklet

-

Restrictions

+

Einschränkungen

{{ restriction.type }}: {{ restriction.value }} @@ -93,7 +93,7 @@

Restrictions

-

Units ({{ testlet.units.length }})

+

Aufgaben ({{ testlet.units.length }})

@@ -115,10 +115,10 @@

Units ({{ testlet.units.length }})

+ label="Aufgaben ({{ data.bookletInfo.units.length }})">
-

Units in Booklet

+

Aufgaben im Booklet

@@ -139,7 +139,7 @@

Units in Booklet

-

Raw XML

+

Roh-XML

{{ data.bookletInfo.rawXml }}
@@ -153,6 +153,6 @@

Raw XML

- +
diff --git a/apps/frontend/src/app/ws-admin/components/booklet-search-dialog/booklet-search-dialog.component.ts b/apps/frontend/src/app/ws-admin/components/booklet-search-dialog/booklet-search-dialog.component.ts index 6530d99c2..bb4fc2f78 100644 --- a/apps/frontend/src/app/ws-admin/components/booklet-search-dialog/booklet-search-dialog.component.ts +++ b/apps/frontend/src/app/ws-admin/components/booklet-search-dialog/booklet-search-dialog.component.ts @@ -16,7 +16,9 @@ import { MatInputModule } from '@angular/material/input'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { MatSnackBar } from '@angular/material/snack-bar'; import { TranslateModule } from '@ngx-translate/core'; import { Router } from '@angular/router'; @@ -25,6 +27,7 @@ import { BackendService } from '../../../services/backend.service'; import { AppService } from '../../../services/app.service'; import { ConfirmDialogComponent, ConfirmDialogData } from '../../../shared/dialogs/confirm-dialog.component'; import { BookletInfoDialogComponent } from '../booklet-info-dialog/booklet-info-dialog.component'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; interface BookletSearchResult { bookletId: number; @@ -45,6 +48,9 @@ interface BookletSearchResult { templateUrl: './booklet-search-dialog.component.html', styleUrls: ['./booklet-search-dialog.component.scss'], standalone: true, + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ CommonModule, FormsModule, diff --git a/apps/frontend/src/app/ws-admin/components/replay-statistics-dialog/replay-statistics-dialog.component.ts b/apps/frontend/src/app/ws-admin/components/replay-statistics-dialog/replay-statistics-dialog.component.ts index 8e96286a6..3d8434811 100644 --- a/apps/frontend/src/app/ws-admin/components/replay-statistics-dialog/replay-statistics-dialog.component.ts +++ b/apps/frontend/src/app/ws-admin/components/replay-statistics-dialog/replay-statistics-dialog.component.ts @@ -51,6 +51,9 @@ interface ReplayFrequencyData { [yAxisLabel]="'workspace.replay-count' | translate" [scheme]="colorScheme" [showDataLabel]="true" + [rotateXAxisTicks]="true" + [xAxisTickFormatting]="formatXAxisLabel.bind(this)" + [view]="[800, 400]" >
@@ -106,6 +109,8 @@ interface ReplayFrequencyData { [yAxisLabel]="'workspace.avg-duration-milliseconds' | translate" [scheme]="colorScheme" [showDataLabel]="true" + [rotateXAxisTicks]="true" + [xAxisTickFormatting]="formatXAxisLabel.bind(this)" [view]="[450, 300]" >
@@ -210,6 +215,9 @@ interface ReplayFrequencyData { [yAxisLabel]="'workspace.failure-count' | translate" [scheme]="colorScheme" [showDataLabel]="true" + [rotateXAxisTicks]="true" + [xAxisTickFormatting]="formatXAxisLabel.bind(this)" + [view]="[800, 400]" >
@@ -376,6 +384,18 @@ export class ReplayStatisticsDialogComponent implements OnInit { return `${(milliseconds / 1000).toFixed(2)} s`; } + /** + * Format X-axis labels to handle long unit names + * @param value The label value + * @returns Truncated label if too long + */ + formatXAxisLabel(value: string): string { + if (true && value.length > 20) { + return `${value.substring(0, 17)}...`; + } + return value; + } + constructor() { this.workspaceId = this.data.workspaceId; } @@ -530,11 +550,9 @@ export class ReplayStatisticsDialogComponent implements OnInit { // Sort by unit ID this.failureByUnitData.sort((a, b) => a.name.localeCompare(b.name)); - // Load failure distribution by day this.loadFailureDistributionByDay(); }, error: () => { - // Continue with day distribution even if unit distribution fails this.loadFailureDistributionByDay(); } }); @@ -552,11 +570,9 @@ export class ReplayStatisticsDialogComponent implements OnInit { // Sort by date (oldest first) this.failureByDayData.sort((a, b) => a.name.localeCompare(b.name)); - // Load failure distribution by hour this.loadFailureDistributionByHour(); }, error: () => { - // Continue with hour distribution even if day distribution fails this.loadFailureDistributionByHour(); } }); @@ -578,7 +594,6 @@ export class ReplayStatisticsDialogComponent implements OnInit { return hourA - hourB; }); - // Complete loading this.loading = false; }, error: () => { diff --git a/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.html b/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.html index 7aae21177..f3bf6141f 100755 --- a/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.html +++ b/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.html @@ -223,6 +223,44 @@

Import Statistik:

description {{ uploadData.testFiles }} Testdateien importiert + @if (data.importType === 'testFiles') { + @if (uploadData.filesUnits && uploadData.filesUnits > 0) { +
  • + assignment + {{ uploadData.filesUnits }} Unit-Dateien +
  • + } + @if (uploadData.filesBooklets && uploadData.filesBooklets > 0) { +
  • + book + {{ uploadData.filesBooklets }} Booklet-Dateien +
  • + } + @if (uploadData.filesPlayer && uploadData.filesPlayer > 0) { +
  • + smart_display + {{ uploadData.filesPlayer }} Player-Dateien +
  • + } + @if (uploadData.filesDefinitions && uploadData.filesDefinitions > 0) { +
  • + schema + {{ uploadData.filesDefinitions }} Aufgabendefinitionen +
  • + } + @if (uploadData.filesCodings && uploadData.filesCodings > 0) { +
  • + code + {{ uploadData.filesCodings }} Codings +
  • + } + @if (uploadData.filesTestTakers && uploadData.filesTestTakers > 0) { +
  • + group + {{ uploadData.filesTestTakers }} Testtakers-Dateien +
  • + } + } } @if (uploadData.responses > 0) {
  • diff --git a/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.ts b/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.ts index ae301b25a..62b2effd3 100755 --- a/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.ts +++ b/apps/frontend/src/app/ws-admin/components/test-center-import/test-center-import.component.ts @@ -21,12 +21,11 @@ import { MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatHeaderCellDef, MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef, MatTable } from '@angular/material/table'; -// eslint-disable-next-line import/no-cycle import { MatTooltip } from '@angular/material/tooltip'; -// eslint-disable-next-line import/no-cycle import { BackendService } from '../../../services/backend.service'; import { AppService } from '../../../services/app.service'; import { WorkspaceAdminService } from '../../services/workspace-admin.service'; +import { ImportOptions, Result } from '../../../services/import.service'; import { TestGroupsInfoDto } from '../../../../../../../api-dto/files/test-groups-info.dto'; import { ConfirmDialogComponent, ConfirmDialogData } from '../../../shared/dialogs/confirm-dialog.component'; @@ -39,33 +38,11 @@ export type WorkspaceAdmin = { } }; -export type ImportOptions = { - responses:string, - definitions:string, - units:string, - player:string, - codings:string, - logs:string, - testTakers:string, - booklets:string -}; - export type Testcenter = { id:number, label:string }; -export type Result = { - success: boolean, - testFiles: number, - responses: number, - logs: number, - booklets: number, - units: number, - persons: number, - importedGroups: string[] -}; - export interface ImportFormValues { testCenter: number; workspace: string; @@ -134,7 +111,7 @@ export class TestCenterImportComponent { authenticated: boolean = false; isUploadingTestFiles: boolean = false; isUploadingTestResults: boolean = false; - uploadData!: Result; + uploadData: Result | null = null; testCenterInstance: Testcenter[] = []; showTestGroups: boolean = false; constructor() { @@ -262,7 +239,7 @@ export class TestCenterImportComponent { } startNewImport(): void { - this.uploadData = {} as Result; + this.uploadData = null; this.showTestGroups = false; this.selectedRows = []; @@ -272,14 +249,11 @@ export class TestCenterImportComponent { } goBackToTestGroups(): void { - this.uploadData = {} as Result; + this.uploadData = null; this.selectedRows = []; this.showTestGroups = true; } - /** - * Refreshes the test groups list to update status after import - */ refreshTestGroups(): void { const formValues = { testCenter: this.loginForm.get('testCenter')?.value, @@ -310,24 +284,15 @@ export class TestCenterImportComponent { }); } - /** - * Check if any selected groups have logs - * @returns True if any selected group has logs - */ private hasSelectedGroupsWithLogs(): boolean { return this.selectedRows.some(group => group.hasBookletLogs); } - /** - * Show confirmation dialog for overwriting logs - * @returns Promise that resolves to true if user confirms, false otherwise - */ private async confirmOverwriteLogs(): Promise { - // Count groups with logs const groupsWithLogs = this.selectedRows.filter(group => group.hasBookletLogs); if (groupsWithLogs.length === 0) { - return true; // No confirmation needed + return true; } const dialogRef = this.dialog.open(ConfirmDialogComponent, { @@ -361,7 +326,7 @@ export class TestCenterImportComponent { } }; - this.uploadData = {} as Result; + this.uploadData = null; this.isUploadingTestFiles = true; this.isUploadingTestResults = this.data.importType === 'testResults'; const selectedGroupNames = this.selectedRows.map(group => group.groupName); @@ -388,12 +353,6 @@ export class TestCenterImportComponent { } } - /** - * Perform the actual import - * @param formValues The form values - * @param selectedGroupNames The selected group names - * @param overwriteExistingLogs Whether to overwrite existing logs - */ private performImport( formValues: ImportFormValues, selectedGroupNames: string[], @@ -431,7 +390,13 @@ export class TestCenterImportComponent { booklets: 0, units: 0, persons: 0, - importedGroups: selectedGroupNames + importedGroups: selectedGroupNames, + filesPlayer: 0, + filesUnits: 0, + filesDefinitions: 0, + filesCodings: 0, + filesBooklets: 0, + filesTestTakers: 0 }; this.isUploadingTestFiles = false; this.isUploadingTestResults = false; diff --git a/apps/frontend/src/app/ws-admin/components/test-files/test-files.component.ts b/apps/frontend/src/app/ws-admin/components/test-files/test-files.component.ts index 52bd36fad..4332aba96 100755 --- a/apps/frontend/src/app/ws-admin/components/test-files/test-files.component.ts +++ b/apps/frontend/src/app/ws-admin/components/test-files/test-files.component.ts @@ -25,7 +25,7 @@ import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { Subject, Subscription } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatPaginator, MatPaginatorIntl, PageEvent } from '@angular/material/paginator'; import { FilesValidationDialogComponent } from '../files-validation-result/files-validation.component'; import { TestCenterImportComponent } from '../test-center-import/test-center-import.component'; import { ResourcePackagesDialogComponent } from '../resource-packages-dialog/resource-packages-dialog.component'; @@ -43,11 +43,15 @@ import { FileDownloadDto } from '../../../../../../../api-dto/files/file-downloa import { ContentDialogComponent } from '../../../shared/dialogs/content-dialog/content-dialog.component'; import { ConfirmDialogComponent } from '../../../shared/dialogs/confirm-dialog.component'; import { getFileIcon } from '../../utils/file-utils'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; @Component({ selector: 'coding-box-test-files', templateUrl: './test-files.component.html', styleUrls: ['./test-files.component.scss'], + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ TranslateModule, DatePipe, @@ -130,7 +134,6 @@ export class TestFilesComponent implements OnInit, OnDestroy { } } - /** Getter for setting table sorting */ get matSort(): MatSort { if (this.dataSource) { this.dataSource.sort = this.sort; @@ -361,8 +364,6 @@ export class TestFilesComponent implements OnInit, OnDestroy { dialogRef.afterClosed().subscribe(result => { if (result === true) { this.resourcePackagesModified = true; - // Optionally reload test files if they include resource packages - // this.loadTestFiles(); } }); } @@ -380,8 +381,8 @@ export class TestFilesComponent implements OnInit, OnDestroy { if (file.file_type === 'Resource' && file.filename.toLowerCase().endsWith('.vocs')) { const dialogRef = this.dialog.open(SchemeEditorDialogComponent, { - width: '90%', - height: '90%', + width: '100vw', + height: '90vh', data: { workspaceId: this.appService.selectedWorkspaceId, fileId: file.id, diff --git a/apps/frontend/src/app/ws-admin/components/test-results-search/test-results-search.component.ts b/apps/frontend/src/app/ws-admin/components/test-results-search/test-results-search.component.ts index 0164eb7b9..f572bfeff 100644 --- a/apps/frontend/src/app/ws-admin/components/test-results-search/test-results-search.component.ts +++ b/apps/frontend/src/app/ws-admin/components/test-results-search/test-results-search.component.ts @@ -16,7 +16,9 @@ import { MatInputModule } from '@angular/material/input'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { MatSnackBar } from '@angular/material/snack-bar'; import { TranslateModule } from '@ngx-translate/core'; import { Router } from '@angular/router'; @@ -26,6 +28,7 @@ import { AppService } from '../../../services/app.service'; import { ConfirmDialogComponent, ConfirmDialogData } from '../../../shared/dialogs/confirm-dialog.component'; import { BookletInfoDialogComponent } from '../booklet-info-dialog/booklet-info-dialog.component'; import { BookletInfoDto } from '../../../../../../../api-dto/booklet-info/booklet-info.dto'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; interface UnitSearchResult { unitId: number; @@ -79,6 +82,9 @@ interface BookletSearchResult { templateUrl: './test-results-search.component.html', styleUrls: ['./test-results-search.component.scss'], standalone: true, + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ CommonModule, FormsModule, @@ -143,7 +149,7 @@ export class TestResultsSearchComponent implements OnInit { debounceTime(this.SEARCH_DEBOUNCE_TIME), distinctUntilChanged() ).subscribe(searchText => { - this.pageIndex = 0; // Reset to first page on new search + this.pageIndex = 0; this.searchUnits(searchText); }); @@ -151,7 +157,7 @@ export class TestResultsSearchComponent implements OnInit { debounceTime(this.SEARCH_DEBOUNCE_TIME), distinctUntilChanged((prev, curr) => prev.value === curr.value && prev.variableId === curr.variableId && prev.unitName === curr.unitName && prev.status === curr.status && prev.codedStatus === curr.codedStatus && prev.group === curr.group && prev.code === curr.code) ).subscribe(searchParams => { - this.pageIndex = 0; // Reset to first page on new search + this.pageIndex = 0; this.searchResponses(searchParams); }); @@ -159,7 +165,7 @@ export class TestResultsSearchComponent implements OnInit { debounceTime(this.SEARCH_DEBOUNCE_TIME), distinctUntilChanged() ).subscribe(searchText => { - this.pageIndex = 0; // Reset to first page on new search + this.pageIndex = 0; this.searchBooklets(searchText); }); } @@ -231,7 +237,6 @@ export class TestResultsSearchComponent implements OnInit { } this.isLoading = true; - // Add 1 to pageIndex because backend uses 1-based indexing this.backendService.searchUnitsByName( this.appService.selectedWorkspaceId, unitName, @@ -253,7 +258,6 @@ export class TestResultsSearchComponent implements OnInit { searchResponses(searchParams: { value?: string; variableId?: string; unitName?: string; status?: string; codedStatus?: string; group?: string; code?: string }): void { this.isLoading = true; - // Add 1 to pageIndex because backend uses 1-based indexing this.backendService.searchResponses( this.appService.selectedWorkspaceId, searchParams, @@ -282,7 +286,6 @@ export class TestResultsSearchComponent implements OnInit { } this.isLoading = true; - // Add 1 to pageIndex because backend uses 1-based indexing this.backendService.searchBookletsByName( this.appService.selectedWorkspaceId, bookletName, @@ -508,20 +511,15 @@ export class TestResultsSearchComponent implements OnInit { const responseIds = this.responseSearchResults.map(response => response.responseId); let successCount = 0; let failCount = 0; - this.isLoading = true; - - // Process each response deletion sequentially const processNextResponse = (index: number) => { if (index >= responseIds.length) { - // All responses processed this.isLoading = false; this.snackBar.open( `${successCount} Antworten gelöscht, ${failCount} fehlgeschlagen.`, 'OK', { duration: 5000 } ); - // Refresh the search results this.searchResponses({ value: this.searchValue.trim() !== '' ? this.searchValue : undefined, variableId: this.searchVariableId.trim() !== '' ? this.searchVariableId : undefined, @@ -552,8 +550,6 @@ export class TestResultsSearchComponent implements OnInit { } }); }; - - // Start processing processNextResponse(0); } }); @@ -612,7 +608,6 @@ export class TestResultsSearchComponent implements OnInit { ).subscribe({ next: response => { if (response.success) { - // Remove the deleted booklet from the results this.bookletSearchResults = this.bookletSearchResults.filter( b => b.bookletId !== booklet.bookletId ); @@ -670,20 +665,15 @@ export class TestResultsSearchComponent implements OnInit { const bookletIds = this.bookletSearchResults.map(booklet => booklet.bookletId); let successCount = 0; let failCount = 0; - this.isLoading = true; - - // Process each booklet deletion sequentially const processNextBooklet = (index: number) => { if (index >= bookletIds.length) { - // All booklets processed this.isLoading = false; this.snackBar.open( `${successCount} Booklets gelöscht, ${failCount} fehlgeschlagen.`, 'OK', { duration: 5000 } ); - // Refresh the search results this.searchBooklets(this.bookletSearchText); return; } @@ -706,8 +696,6 @@ export class TestResultsSearchComponent implements OnInit { } }); }; - - // Start processing processNextBooklet(0); } }); diff --git a/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.ts b/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.ts index 4422e98f3..f60409d7c 100755 --- a/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.ts +++ b/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.ts @@ -12,7 +12,9 @@ import { import { MatSort, MatSortHeader } from '@angular/material/sort'; import { FormsModule, UntypedFormGroup } from '@angular/forms'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { Subject, Subscription, @@ -47,8 +49,6 @@ import { NoteDialogComponent } from '../note-dialog/note-dialog.component'; import { TestResultsSearchComponent } from '../test-results-search/test-results-search.component'; import { ConfirmDialogComponent, ConfirmDialogData } from '../../../shared/dialogs/confirm-dialog.component'; import { UnitTagDto } from '../../../../../../../api-dto/unit-tags/unit-tag.dto'; -import { CreateUnitTagDto } from '../../../../../../../api-dto/unit-tags/create-unit-tag.dto'; -import { UpdateUnitTagDto } from '../../../../../../../api-dto/unit-tags/update-unit-tag.dto'; import { UnitNoteDto } from '../../../../../../../api-dto/unit-notes/unit-note.dto'; import { ValidationDialogComponent } from '../validation-dialog/validation-dialog.component'; import { VariableValidationDto } from '../../../../../../../api-dto/files/variable-validation.dto'; @@ -59,6 +59,7 @@ import { BookletInfoDto } from '../../../../../../../api-dto/booklet-info/bookle import { BookletInfoDialogComponent } from '../booklet-info-dialog/booklet-info-dialog.component'; import { UnitInfoDialogComponent } from '../unit-info-dialog/unit-info-dialog.component'; import { UnitInfoDto } from '../../../../../../../api-dto/unit-info/unit-info.dto'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; interface BookletLog { id: number; @@ -145,7 +146,10 @@ interface P { templateUrl: './test-results.component.html', styleUrls: ['./test-results.component.scss'], standalone: true, - providers: [DatePipe], + providers: [ + DatePipe, + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [CommonModule, FormsModule, MatExpansionPanelHeader, @@ -212,7 +216,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { isSearching: boolean = false; isLoadingBooklets: boolean = false; unitTags: UnitTagDto[] = []; - newTagText: string = ''; unitTagsMap: Map = new Map(); unitNotes: UnitNoteDto[] = []; unitNotesMap: Map = new Map(); @@ -244,26 +247,17 @@ export class TestResultsComponent implements OnInit, OnDestroy { this.searchSubscription = null; } - // Stop interval when component is destroyed this.stopValidationStatusCheck(); } - /** - * Start interval to check validation status - */ private startValidationStatusCheck(): void { - // Check immediately this.checkValidationStatus(); - // Then check every 5 seconds this.validationStatusInterval = window.setInterval(() => { this.checkValidationStatus(); }, 5000); } - /** - * Stop interval for checking validation status - */ private stopValidationStatusCheck(): void { if (this.validationStatusInterval !== null) { window.clearInterval(this.validationStatusInterval); @@ -271,9 +265,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { } } - /** - * Check validation status by querying active tasks - */ private checkValidationStatus(): void { if (!this.isInitialized || !this.appService.selectedWorkspaceId) { return; @@ -281,13 +272,11 @@ export class TestResultsComponent implements OnInit, OnDestroy { const taskIds = this.validationTaskStateService.getAllTaskIds(this.appService.selectedWorkspaceId); - // If there are active tasks, check their status if (Object.keys(taskIds).length > 0) { for (const [type, taskId] of Object.entries(taskIds)) { this.backendService.getValidationTask(this.appService.selectedWorkspaceId, taskId) .subscribe({ next: task => { - // If task is completed or failed, remove it from the service if (task.status === 'completed' || task.status === 'failed') { this.validationTaskStateService.removeTaskId( this.appService.selectedWorkspaceId, @@ -296,7 +285,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { } }, error: () => { - // If there's an error, remove the task from the service this.validationTaskStateService.removeTaskId( this.appService.selectedWorkspaceId, type as 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' @@ -307,10 +295,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { } } - /** - * Check if any validation task is running - * @returns True if any validation task is running - */ isAnyValidationRunning(): boolean { if (!this.appService.selectedWorkspaceId) { return false; @@ -320,10 +304,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { return Object.keys(taskIds).length > 0; } - /** - * Get the overall validation status - * @returns The status: 'running', 'failed', 'success', or 'not-run' - */ getOverallValidationStatus(): 'running' | 'failed' | 'success' | 'not-run' { if (this.isAnyValidationRunning()) { return 'running'; @@ -364,8 +344,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { .subscribe({ next: booklets => { this.selectedBooklet = row.group; - const uniqueBooklets = this.filterUniqueBooklets(booklets); - this.booklets = uniqueBooklets; + this.booklets = booklets; this.sortBooklets(); this.sortBookletUnits(); this.loadAllUnitTags(); @@ -378,18 +357,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { }); } - filterUniqueBooklets(booklets: Booklet[]): Booklet[] { - const uniqueBookletsMap = new Map(); - - booklets.forEach(booklet => { - if (!uniqueBookletsMap.has(booklet.name)) { - uniqueBookletsMap.set(booklet.name, booklet); - } - }); - - return Array.from(uniqueBookletsMap.values()); - } - sortBooklets(): void { if (!this.booklets || this.booklets.length === 0) { return; @@ -408,7 +375,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { this.booklets.forEach(booklet => { if (booklet.units && Array.isArray(booklet.units)) { - // Sort units by alias (or name if alias is not available) booklet.units.sort((a, b) => { const aliasA = a.alias || a.name || ''; const aliasB = b.alias || b.name || ''; @@ -426,9 +392,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { if (!this.booklets || this.booklets.length === 0) { return; } - this.unitTagsMap.clear(); - this.booklets.forEach(booklet => { if (booklet.units && Array.isArray(booklet.units)) { booklet.units.forEach(unit => { @@ -444,10 +408,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { if (!this.booklets || this.booklets.length === 0) { return; } - this.unitNotesMap.clear(); - - // Extract all unit IDs from the booklets const unitIds: number[] = []; this.booklets.forEach(booklet => { if (booklet.units && Array.isArray(booklet.units)) { @@ -492,14 +453,12 @@ export class TestResultsComponent implements OnInit, OnDestroy { return; } - // Show loading indicator const loadingSnackBar = this.snackBar.open( 'Lade Testheft...', '', { duration: 3000 } ); - // Get the booklet from file_upload using the new method this.bookletReplayService.getBookletFromFileUpload( this.appService.selectedWorkspaceId, booklet.name @@ -515,10 +474,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { ); return; } - - // Serialize the booklet data for URL transmission const serializedBooklet = this.serializeBookletData(bookletReplay); - const firstUnit = bookletReplay.units[0]; this.appService @@ -530,7 +486,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { bookletData: serializedBooklet }; - // Construct the URL with the first unit const url = this.router .serializeUrl( this.router.createUrlTree( @@ -538,7 +493,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { { queryParams: queryParams }) ); - // Open the replay in a new tab window.open(`#/${url}`, '_blank'); }); }, @@ -674,35 +628,20 @@ export class TestResultsComponent implements OnInit, OnDestroy { ...response, expanded: false })); - const getUniqueKey = (r: Response) => `${r.variableid}|${r.unitid}|${r.value}`; - - const uniqueMap = new Map(); - mappedResponses.forEach(response => { - const key = getUniqueKey(response); - if (!uniqueMap.has(key)) { - uniqueMap.set(key, response); - } - }); - - const uniqueResponses = Array.from(uniqueMap.values()); - - this.responses = uniqueResponses; + this.responses = Array.from(mappedResponses); this.selectedBooklet = booklet.name; this.responses.sort((a: Response, b: Response) => { - // First prioritize VALUE_CHANGED status if (a.status === 'VALUE_CHANGED' && b.status !== 'VALUE_CHANGED') { return -1; } if (a.status !== 'VALUE_CHANGED' && b.status === 'VALUE_CHANGED') { return 1; } - // Then sort alphabetically by variableid return a.variableid.localeCompare(b.variableid); }); this.logs = unit.logs; - // this.logs = this.createUnitHistory(unit); this.selectedUnit = unit; this.loadUnitTags(); @@ -711,8 +650,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { loadUnitTags(): void { if (this.selectedUnit && this.selectedUnit.id) { - const tags = this.unitTagsMap.get(this.selectedUnit.id as number) || []; - this.unitTags = tags; + this.unitTags = this.unitTagsMap.get(this.selectedUnit.id as number) || []; } else { this.unitTags = []; } @@ -722,7 +660,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { if (this.selectedUnit && this.selectedUnit.id) { const unitId = this.selectedUnit.id as number; if (this.unitNotesMap.has(unitId)) { - // Use the pre-fetched notes this.unitNotes = this.unitNotesMap.get(unitId) || []; } else { this.backendService.getUnitNotes( @@ -755,161 +692,10 @@ export class TestResultsComponent implements OnInit, OnDestroy { return notes.length > 0; } - addUnitTag(): void { - if (!this.newTagText.trim()) { - this.snackBar.open( - 'Bitte geben Sie einen Tag-Text ein', - 'Fehler', - { duration: 3000 } - ); - return; - } - - if (this.selectedUnit && this.selectedUnit.id) { - this.addTagToUnit(this.selectedUnit.id as number, this.newTagText.trim()); - this.newTagText = ''; // Clear the input field - } - } - - addTagToUnit(unitId: number, tagText: string): void { - if (!tagText.trim()) { - this.snackBar.open( - 'Bitte geben Sie einen Tag-Text ein', - 'Fehler', - { duration: 3000 } - ); - return; - } - - const createTagDto: CreateUnitTagDto = { - unitId: unitId, - tag: tagText.trim() - }; - - this.backendService.createUnitTag( - this.appService.selectedWorkspaceId, - createTagDto - ).subscribe({ - next: tag => { - // If this is the selected unit, update the unitTags array - if (this.selectedUnit && this.selectedUnit.id === unitId) { - this.unitTags.push(tag); - } - - // Update the unitTagsMap - const tags = this.unitTagsMap.get(unitId) || []; - tags.push(tag); - this.unitTagsMap.set(unitId, tags); - - this.snackBar.open( - 'Tag erfolgreich hinzugefügt', - 'Erfolg', - { duration: 3000 } - ); - }, - error: () => { - this.snackBar.open( - 'Fehler beim Hinzufügen des Tags', - 'Fehler', - { duration: 3000 } - ); - } - }); - } - - updateUnitTag(tagId: number, newText: string): void { - if (!newText.trim()) { - this.snackBar.open( - 'Bitte geben Sie einen Tag-Text ein', - 'Fehler', - { duration: 3000 } - ); - return; - } - - const updateTagDto: UpdateUnitTagDto = { - tag: newText.trim() - }; - - this.backendService.updateUnitTag( - this.appService.selectedWorkspaceId, - tagId, - updateTagDto - ).subscribe({ - next: updatedTag => { - const index = this.unitTags.findIndex(tag => tag.id === tagId); - if (index !== -1) { - this.unitTags[index] = updatedTag; - } - - this.snackBar.open( - 'Tag erfolgreich aktualisiert', - 'Erfolg', - { duration: 3000 } - ); - }, - error: () => { - this.snackBar.open( - 'Fehler beim Aktualisieren des Tags', - 'Fehler', - { duration: 3000 } - ); - } - }); - } - - deleteUnitTag(tagId: number): void { - if (this.selectedUnit && this.selectedUnit.id) { - this.deleteTagFromUnit(tagId, this.selectedUnit.id as number); - } - } - - deleteTagFromUnit(tagId: number, unitId: number): void { - this.backendService.deleteUnitTag( - this.appService.selectedWorkspaceId, - tagId - ).subscribe({ - next: success => { - if (success) { - if (this.selectedUnit && this.selectedUnit.id === unitId) { - this.unitTags = this.unitTags.filter(tag => tag.id !== tagId); - } - - const tags = this.unitTagsMap.get(unitId) || []; - this.unitTagsMap.set(unitId, tags.filter(tag => tag.id !== tagId)); - - this.snackBar.open( - 'Tag erfolgreich gelöscht', - 'Erfolg', - { duration: 3000 } - ); - } else { - this.snackBar.open( - 'Fehler beim Löschen des Tags', - 'Fehler', - { duration: 3000 } - ); - } - }, - error: () => { - this.snackBar.open( - 'Fehler beim Löschen des Tags', - 'Fehler', - { duration: 3000 } - ); - } - }); - } - setSelectedBooklet(booklet: Booklet) { this.selectedBooklet = booklet.name; } - formatTimestamp(timestamp: string): string { - const date = new Date(Number(timestamp)); - return date.toLocaleString(); - } - calculateBookletProcessingTime(booklet: Booklet): number | null { if (!booklet.logs || !Array.isArray(booklet.logs) || booklet.logs.length === 0) { return null; @@ -929,14 +715,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { return null; } - formatDuration(durationMs: number | null): string { - if (durationMs === null || durationMs < 0) return '00:00'; - const totalSeconds = Math.floor(durationMs / 1000); - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; - } - isBookletComplete(booklet: Booklet): boolean { if (!booklet.logs || !Array.isArray(booklet.logs) || booklet.logs.length === 0) { return true; @@ -1092,7 +870,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { this.appService.selectedWorkspaceId, inputElement.files, resultType, - overwriteExisting // Pass the user's choice + overwriteExisting ).subscribe(() => { setTimeout(() => { this.createTestResultsList(this.pageIndex, this.pageSize, this.getCurrentSearchText()); @@ -1158,7 +936,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { ); this.pollCodingJobStatus(result.jobId); - } else if (result.totalResponses > 0) { // Handle synchronous result (backward compatibility) + } else if (result.totalResponses > 0) { this.snackBar.open( this.translateService.instant('ws-admin.test-group-coded'), '', @@ -1189,33 +967,26 @@ export class TestResultsComponent implements OnInit, OnDestroy { private pollCodingJobStatus(jobId: string): void { const pollingInterval = 5000; - - // Set up a timer to check job status const timer = setInterval(() => { this.backendService.getCodingJobStatus( this.appService.selectedWorkspaceId, jobId ).subscribe({ next: job => { - // Check if the job is completed or failed if (job.status === 'completed') { - // Stop polling clearInterval(timer); - // Show success notification const snackBarRef = this.snackBar.open( 'Kodierung abgeschlossen', 'Ergebnisse anzeigen', { duration: 10000 } ); - // Handle click on action button snackBarRef.onAction().subscribe(() => { this.showCodingResults(job.result); this.createTestResultsList(this.pageIndex, this.pageSize, this.getCurrentSearchText()); }); } else if (job.status === 'failed') { - // Stop polling clearInterval(timer); this.snackBar.open( @@ -1224,10 +995,8 @@ export class TestResultsComponent implements OnInit, OnDestroy { { duration: 5000 } ); } - // If status is 'pending' or 'processing', continue polling }, error: () => { - // Stop polling on error clearInterval(timer); this.snackBar.open( @@ -1298,13 +1067,11 @@ export class TestResultsComponent implements OnInit, OnDestroy { ).subscribe({ next: result => { if (result.success) { - // Remove the unit from the booklet's units array const unitIndex = booklet.units.findIndex(u => u.id === unit.id); if (unitIndex !== -1) { booklet.units.splice(unitIndex, 1); } - // If this was the selected unit, clear the selection if (this.selectedUnit && this.selectedUnit.id === unit.id) { this.selectedUnit = undefined; this.responses = []; @@ -1364,7 +1131,6 @@ export class TestResultsComponent implements OnInit, OnDestroy { ).subscribe({ next: result => { if (result.success) { - // Remove the response from the responses array const responseIndex = this.responses.findIndex(r => r.id === response.id); if (responseIndex !== -1) { this.responses.splice(responseIndex, 1); @@ -1416,10 +1182,8 @@ export class TestResultsComponent implements OnInit, OnDestroy { private serializeBookletData(booklet: BookletReplay): string { try { - // Convert the booklet to a JSON string const jsonString = JSON.stringify(booklet); - // Base64 encode the JSON string to make it URL-safe return btoa(jsonString); } catch (error) { return ''; @@ -1444,7 +1208,7 @@ export class TestResultsComponent implements OnInit, OnDestroy { this.dialog.open(VariableAnalysisDialogComponent, { width: '900px', data: { - unitId: this.selectedUnit?.id, // Optional unit ID, may be undefined + unitId: this.selectedUnit?.id, title: 'Item/Variablen Analyse', workspaceId: this.appService.selectedWorkspaceId, jobs: variableAnalysisJobs diff --git a/apps/frontend/src/app/ws-admin/components/unit-info-dialog/unit-info-dialog.component.html b/apps/frontend/src/app/ws-admin/components/unit-info-dialog/unit-info-dialog.component.html index fc87e744f..af25692ba 100644 --- a/apps/frontend/src/app/ws-admin/components/unit-info-dialog/unit-info-dialog.component.html +++ b/apps/frontend/src/app/ws-admin/components/unit-info-dialog/unit-info-dialog.component.html @@ -1,6 +1,6 @@

    - Unit Information: {{ data.unitId }} + Unit-Informationen: {{ data.unitId }} @@ -9,7 +9,7 @@

    -

    Loading unit information...

    +

    Unit-Informationen werden geladen...

    @@ -20,16 +20,16 @@

    - +
    -

    Basic Information

    +

    Grundinformationen

    ID: {{ data.unitInfo.metadata.id }}
    - Label: + Bezeichnung: {{ data.unitInfo.metadata.label }}
    @@ -56,7 +56,7 @@

    Basic Information

    -

    Definition Information

    +

    Definitionsinformationen

    Type: {{ data.unitInfo.definition.type }} @@ -82,11 +82,11 @@

    Definition Information

    - +
    -

    Base Variables

    +

    Basisvariablen

    ID: @@ -106,11 +106,11 @@

    Base Variables

    Multiple: - {{ variable.multiple ? 'Yes' : 'No' }} + {{ variable.multiple ? 'Ja' : 'Nein' }}
    Nullable: - {{ variable.nullable ? 'Yes' : 'No' }} + {{ variable.nullable ? 'Ja' : 'Nein' }}
    Page: @@ -119,7 +119,7 @@

    Base Variables

    -

    Values

    +

    Werte

    Label: @@ -138,7 +138,7 @@

    Values

    -

    Derived Variables

    +

    Abgeleitete Variablen

    ID: @@ -158,11 +158,11 @@

    Derived Variables

    Multiple: - {{ variable.multiple ? 'Yes' : 'No' }} + {{ variable.multiple ? 'Ja' : 'Nein' }}
    Nullable: - {{ variable.nullable ? 'Yes' : 'No' }} + {{ variable.nullable ? 'Ja' : 'Nein' }}
    Page: @@ -171,7 +171,7 @@

    Derived Variables

    -

    Values

    +

    Werte

    Label: @@ -191,10 +191,10 @@

    Values

    - +
    -

    Dependencies

    +

    Abhängigkeiten

    Type: @@ -215,10 +215,10 @@

    Dependencies

    - +
    -

    Coding Scheme Reference

    +

    Kodierungsschema-Referenz

    Schemer: {{ data.unitInfo.codingSchemeRef.schemer }} @@ -240,7 +240,7 @@

    Coding Scheme Reference

    - +
    {{ data.unitInfo.rawXml }}
    diff --git a/apps/frontend/src/app/ws-admin/components/unit-search-dialog/unit-search-dialog.component.ts b/apps/frontend/src/app/ws-admin/components/unit-search-dialog/unit-search-dialog.component.ts index 1180444dc..fe5c3f391 100644 --- a/apps/frontend/src/app/ws-admin/components/unit-search-dialog/unit-search-dialog.component.ts +++ b/apps/frontend/src/app/ws-admin/components/unit-search-dialog/unit-search-dialog.component.ts @@ -16,7 +16,9 @@ import { MatInputModule } from '@angular/material/input'; import { MatTableModule } from '@angular/material/table'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { MatSnackBar } from '@angular/material/snack-bar'; import { TranslateModule } from '@ngx-translate/core'; import { Router } from '@angular/router'; @@ -26,6 +28,7 @@ import { AppService } from '../../../services/app.service'; import { ConfirmDialogComponent, ConfirmDialogData } from '../../../shared/dialogs/confirm-dialog.component'; import { BookletInfoDialogComponent } from '../booklet-info-dialog/booklet-info-dialog.component'; import { BookletInfoDto } from '../../../../../../../api-dto/booklet-info/booklet-info.dto'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; interface UnitSearchResult { unitId: number; @@ -79,6 +82,9 @@ interface BookletSearchResult { templateUrl: './unit-search-dialog.component.html', styleUrls: ['./unit-search-dialog.component.scss'], standalone: true, + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ CommonModule, FormsModule, @@ -143,7 +149,7 @@ export class UnitSearchDialogComponent implements OnInit { debounceTime(this.SEARCH_DEBOUNCE_TIME), distinctUntilChanged() ).subscribe(searchText => { - this.pageIndex = 0; // Reset to first page on new search + this.pageIndex = 0; this.searchUnits(searchText); }); @@ -151,7 +157,7 @@ export class UnitSearchDialogComponent implements OnInit { debounceTime(this.SEARCH_DEBOUNCE_TIME), distinctUntilChanged((prev, curr) => prev.value === curr.value && prev.variableId === curr.variableId && prev.unitName === curr.unitName && prev.status === curr.status && prev.codedStatus === curr.codedStatus && prev.group === curr.group && prev.code === curr.code) ).subscribe(searchParams => { - this.pageIndex = 0; // Reset to first page on new search + this.pageIndex = 0; this.searchResponses(searchParams); }); @@ -159,7 +165,7 @@ export class UnitSearchDialogComponent implements OnInit { debounceTime(this.SEARCH_DEBOUNCE_TIME), distinctUntilChanged() ).subscribe(searchText => { - this.pageIndex = 0; // Reset to first page on new search + this.pageIndex = 0; this.searchBooklets(searchText); }); } @@ -211,7 +217,7 @@ export class UnitSearchDialogComponent implements OnInit { setSearchMode(mode: 'unit' | 'response' | 'booklet'): void { if (this.searchMode === mode) { - return; // Don't do anything if the mode hasn't changed + return; } this.searchMode = mode; @@ -231,7 +237,6 @@ export class UnitSearchDialogComponent implements OnInit { } this.isLoading = true; - // Add 1 to pageIndex because backend uses 1-based indexing this.backendService.searchUnitsByName( this.appService.selectedWorkspaceId, unitName, @@ -253,7 +258,6 @@ export class UnitSearchDialogComponent implements OnInit { searchResponses(searchParams: { value?: string; variableId?: string; unitName?: string; status?: string; codedStatus?: string; group?: string; code?: string }): void { this.isLoading = true; - // Add 1 to pageIndex because backend uses 1-based indexing this.backendService.searchResponses( this.appService.selectedWorkspaceId, searchParams, @@ -282,7 +286,6 @@ export class UnitSearchDialogComponent implements OnInit { } this.isLoading = true; - // Add 1 to pageIndex because backend uses 1-based indexing this.backendService.searchBookletsByName( this.appService.selectedWorkspaceId, bookletName, @@ -401,7 +404,6 @@ export class UnitSearchDialogComponent implements OnInit { { duration: 3000 } ); } else { - // Show error message this.snackBar.open( `Fehler beim Löschen der Antwort: ${apiResponse.report.warnings.join(', ')}`, 'Fehler', @@ -508,20 +510,15 @@ export class UnitSearchDialogComponent implements OnInit { const responseIds = this.responseSearchResults.map(response => response.responseId); let successCount = 0; let failCount = 0; - this.isLoading = true; - - // Process each response deletion sequentially const processNextResponse = (index: number) => { if (index >= responseIds.length) { - // All responses processed this.isLoading = false; this.snackBar.open( `${successCount} Antworten gelöscht, ${failCount} fehlgeschlagen.`, 'OK', { duration: 5000 } ); - // Refresh the search results this.searchResponses({ value: this.searchValue.trim() !== '' ? this.searchValue : undefined, variableId: this.searchVariableId.trim() !== '' ? this.searchVariableId : undefined, @@ -552,8 +549,6 @@ export class UnitSearchDialogComponent implements OnInit { } }); }; - - // Start processing processNextResponse(0); } }); @@ -612,7 +607,6 @@ export class UnitSearchDialogComponent implements OnInit { ).subscribe({ next: response => { if (response.success) { - // Remove the deleted booklet from the results this.bookletSearchResults = this.bookletSearchResults.filter( b => b.bookletId !== booklet.bookletId ); @@ -670,20 +664,15 @@ export class UnitSearchDialogComponent implements OnInit { const bookletIds = this.bookletSearchResults.map(booklet => booklet.bookletId); let successCount = 0; let failCount = 0; - this.isLoading = true; - - // Process each booklet deletion sequentially const processNextBooklet = (index: number) => { if (index >= bookletIds.length) { - // All booklets processed this.isLoading = false; this.snackBar.open( `${successCount} Booklets gelöscht, ${failCount} fehlgeschlagen.`, 'OK', { duration: 5000 } ); - // Refresh the search results this.searchBooklets(this.bookletSearchText); return; } @@ -706,8 +695,6 @@ export class UnitSearchDialogComponent implements OnInit { } }); }; - - // Start processing processNextBooklet(0); } }); diff --git a/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.ts b/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.ts index 1b4443f22..067965978 100644 --- a/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.ts +++ b/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.ts @@ -12,7 +12,9 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTableModule, MatTableDataSource } from '@angular/material/table'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; -import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator'; +import { + MatPaginator, MatPaginatorModule, MatPaginatorIntl, PageEvent +} from '@angular/material/paginator'; import { MatIconModule } from '@angular/material/icon'; import { Subscription } from 'rxjs'; import { BackendService } from '../../../services/backend.service'; @@ -25,11 +27,15 @@ import { DuplicateResponsesResultDto } from '../../../../../../../api-dto/files/ import { DuplicateResponseSelectionDto } from '../../../models/duplicate-response-selection.dto'; import { ContentDialogComponent } from '../../../shared/dialogs/content-dialog/content-dialog.component'; import { ValidationTaskDto } from '../../../models/validation-task.dto'; +import { GermanPaginatorIntl } from '../../../shared/services/german-paginator-intl.service'; @Component({ selector: 'coding-box-validation-dialog', templateUrl: './validation-dialog.component.html', standalone: true, + providers: [ + { provide: MatPaginatorIntl, useClass: GermanPaginatorIntl } + ], imports: [ CommonModule, MatDialogModule, @@ -170,38 +176,31 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr validationTaskStateService = inject(ValidationTaskStateService); validationService = inject(ValidationService); - // Subscriptions private subscriptions: Subscription[] = []; - // Flag to indicate if we're closing the dialog private isClosing = false; - // Validation tasks private variableValidationTask: ValidationTaskDto | null = null; private variableTypeValidationTask: ValidationTaskDto | null = null; private responseStatusValidationTask: ValidationTaskDto | null = null; private testTakersValidationTask: ValidationTaskDto | null = null; private groupResponsesValidationTask: ValidationTaskDto | null = null; - // Variable validation properties invalidVariables: InvalidVariableDto[] = []; totalInvalidVariables: number = 0; currentVariablePage: number = 1; variablePageSize: number = 10; - // Variable type validation properties invalidTypeVariables: InvalidVariableDto[] = []; totalInvalidTypeVariables: number = 0; currentTypeVariablePage: number = 1; typeVariablePageSize: number = 10; - // Response status validation properties invalidStatusVariables: InvalidVariableDto[] = []; totalInvalidStatusVariables: number = 0; currentStatusVariablePage: number = 1; statusVariablePageSize: number = 10; - // Group responses validation properties groupResponsesResult: { testTakersFound: boolean; groupsWithResponses: { group: string; hasResponse: boolean }[]; @@ -216,25 +215,21 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr paginatedGroupResponses = new MatTableDataSource<{ group: string; hasResponse: boolean }>([]); - // Group responses pagination properties currentGroupResponsesPage: number = 1; groupResponsesPageSize: number = 10; totalGroupResponses: number = 0; - // TestTakers validation properties testTakersValidationResult: TestTakersValidationDto | null = null; isTestTakersValidationRunning: boolean = false; testTakersValidationWasRun: boolean = false; expandedMissingPersonsPanel: boolean = false; paginatedMissingPersons = new MatTableDataSource([]); - // Validation running flags isVariableValidationRunning: boolean = false; isVariableTypeValidationRunning: boolean = false; isResponseStatusValidationRunning: boolean = false; isDuplicateResponsesValidationRunning: boolean = false; - // Validation was run flags validateVariablesWasRun: boolean = false; validateVariableTypesWasRun: boolean = false; validateResponseStatusWasRun: boolean = false; @@ -248,7 +243,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr selectedResponses: Set = new Set(); selectedTypeResponses: Set = new Set(); selectedStatusResponses: Set = new Set(); - duplicateResponseSelections: Map = new Map(); // Maps duplicate key to selected response ID + duplicateResponseSelections: Map = new Map(); pageSizeOptions = [25, 50, 100, 200]; @@ -256,7 +251,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr paginatedTypeVariables = new MatTableDataSource([]); paginatedStatusVariables = new MatTableDataSource([]); - // Duplicate responses validation properties duplicateResponses: DuplicateResponseSelectionDto[] = []; duplicateResponsesResult: DuplicateResponsesResultDto | null = null; totalDuplicateResponses: number = 0; @@ -272,15 +266,11 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr ) {} ngOnInit(): void { - // Check for existing validation tasks this.checkForExistingTasks(); - - // Load previous validation results this.loadPreviousValidationResults(); } ngAfterViewInit(): void { - // Set up paginators after view is initialized this.paginatedVariables.paginator = this.variablePaginator; this.paginatedTypeVariables.paginator = this.variableTypePaginator; this.paginatedStatusVariables.paginator = this.statusVariablePaginator; @@ -288,27 +278,18 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr } ngOnDestroy(): void { - // If we're closing the dialog, don't cancel running tasks if (this.isClosing) { - // Store running task IDs in the service this.storeRunningTasks(); - - // Only unsubscribe from subscriptions, don't cancel tasks this.subscriptions.forEach(sub => sub.unsubscribe()); } else { - // Clean up subscriptions to prevent memory leaks this.subscriptions.forEach(sub => sub.unsubscribe()); } } - /** - * Check for existing validation tasks and load them if they exist - */ private checkForExistingTasks(): void { const workspaceId = this.appService.selectedWorkspaceId; const taskIds = this.validationTaskStateService.getAllTaskIds(workspaceId); - // Check for each type of validation task if (taskIds.variables) { this.loadExistingTask('variables', taskIds.variables); } @@ -330,18 +311,12 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr } } - /** - * Load an existing validation task - * @param type The type of validation task - * @param taskId The task ID - */ private loadExistingTask( type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses', taskId: number ): void { const workspaceId = this.appService.selectedWorkspaceId; - // Set the appropriate task object switch (type) { case 'variables': this.isVariableValidationRunning = true; @@ -359,11 +334,9 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.isGroupResponsesValidationRunning = true; break; default: - // No action needed for unknown types break; } - // Get the task status const subscription = this.backendService.getValidationTask(workspaceId, taskId) .subscribe({ next: task => { @@ -387,18 +360,14 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr break; } - // If the task is still running, poll for updates if (task.status === 'pending' || task.status === 'processing') { this.pollExistingTask(type, taskId); } else if (task.status === 'completed') { - // If the task is completed, get the results this.loadTaskResults(type, taskId); } else if (task.status === 'failed') { - // If the task failed, show an error message this.snackBar.open(`Validierung fehlgeschlagen: ${task.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 }); this.validationTaskStateService.removeTaskId(workspaceId, type); - // Reset the running flag switch (type) { case 'variables': this.isVariableValidationRunning = false; @@ -424,7 +393,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 }); this.validationTaskStateService.removeTaskId(workspaceId, type); - // Reset the running flag switch (type) { case 'variables': this.isVariableValidationRunning = false; @@ -450,11 +418,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.subscriptions.push(subscription); } - /** - * Poll for updates on an existing validation task - * @param type The type of validation task - * @param taskId The task ID - */ private pollExistingTask( type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses', taskId: number @@ -466,14 +429,12 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr taskId ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.loadTaskResults(type, taskId); } else if (updatedTask.status === 'failed') { this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 }); this.validationTaskStateService.removeTaskId(workspaceId, type); - // Reset the running flag switch (type) { case 'variables': this.isVariableValidationRunning = false; @@ -499,7 +460,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 }); this.validationTaskStateService.removeTaskId(workspaceId, type); - // Reset the running flag switch (type) { case 'variables': this.isVariableValidationRunning = false; @@ -525,11 +485,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.subscriptions.push(pollSubscription); } - /** - * Load the results of a validation task - * @param type The type of validation task - * @param taskId The task ID - */ private loadTaskResults( type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses', taskId: number @@ -541,7 +496,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr taskId ).subscribe({ next: result => { - // Define result type interfaces outside of switch interface PaginatedResult { data: InvalidVariableDto[]; total: number; @@ -558,7 +512,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr limit: number; } - // Process results based on type switch (type) { case 'variables': { const typedResult = result as PaginatedResult; @@ -569,8 +522,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedVariables(); this.isVariableValidationRunning = false; this.validateVariablesWasRun = true; - - // Save validation result to the service this.saveValidationResult(type); break; } @@ -584,8 +535,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedTypeVariables(); this.isVariableTypeValidationRunning = false; this.validateVariableTypesWasRun = true; - - // Save validation result to the service this.saveValidationResult(type); break; } @@ -599,8 +548,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedStatusVariables(); this.isResponseStatusValidationRunning = false; this.validateResponseStatusWasRun = true; - - // Save validation result to the service this.saveValidationResult(type); break; } @@ -610,8 +557,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedMissingPersons(); this.isTestTakersValidationRunning = false; this.testTakersValidationWasRun = true; - - // Save validation result to the service this.saveValidationResult(type); break; } @@ -624,7 +569,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.isGroupResponsesValidationRunning = false; this.groupResponsesValidationWasRun = true; - // Save validation result to the service this.saveValidationResult(type); break; } @@ -633,14 +577,12 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr break; } - // Remove the task ID from the service since we've loaded the results this.validationTaskStateService.removeTaskId(workspaceId, type); }, error: () => { this.snackBar.open('Fehler beim Abrufen der Validierungsergebnisse', 'Schließen', { duration: 5000 }); this.validationTaskStateService.removeTaskId(workspaceId, type); - // Reset the running flag switch (type) { case 'variables': this.isVariableValidationRunning = false; @@ -658,7 +600,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.isGroupResponsesValidationRunning = false; break; default: - // No action needed for unknown types break; } } @@ -667,13 +608,9 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.subscriptions.push(subscription); } - /** - * Store running tasks in the service - */ private storeRunningTasks(): void { const workspaceId = this.appService.selectedWorkspaceId; - // Store each running task if (this.variableValidationTask && (this.variableValidationTask.status === 'pending' || this.variableValidationTask.status === 'processing')) { this.validationTaskStateService.setTaskId(workspaceId, 'variables', this.variableValidationTask.id); } @@ -708,22 +645,15 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr updatePaginatedGroupResponses(): void { if (this.groupResponsesResult) { this.paginatedGroupResponses.data = this.groupResponsesResult.groupsWithResponses; - // totalGroupResponses is now set from the server response } } onGroupResponsesPageChange(event: PageEvent): void { this.currentGroupResponsesPage = event.pageIndex + 1; this.groupResponsesPageSize = event.pageSize; - - // Reload data from server with new pagination parameters using background task this.isGroupResponsesValidationRunning = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'groupResponses', @@ -731,20 +661,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.groupResponsesPageSize ).subscribe(task => { this.groupResponsesValidationTask = task; - - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result const typedResult = result as { testTakersFound: boolean; groupsWithResponses: { group: string; hasResponse: boolean }[]; @@ -780,44 +706,31 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.isTestTakersValidationRunning = true; this.testTakersValidationResult = null; this.testTakersValidationWasRun = false; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'testTakers' ).subscribe(task => { this.testTakersValidationTask = task; - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // Update progress if available - if (updatedTask.progress) { - // Could show progress here if needed - } - - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as TestTakersValidationDto this.testTakersValidationResult = result as TestTakersValidationDto; - // Check if the result indicates errors const hasErrors = !this.testTakersValidationResult.testTakersFound || this.testTakersValidationResult.missingPersons.length > 0; - // Create a validation result with the appropriate status const validationResult: ValidationResult = { status: hasErrors ? 'failed' : 'success', timestamp: Date.now(), @@ -828,7 +741,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr } }; - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult( this.appService.selectedWorkspaceId, 'testTakers', @@ -875,12 +787,8 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.groupResponsesResult = null; this.groupResponsesValidationWasRun = false; this.currentGroupResponsesPage = 1; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'groupResponses', @@ -889,24 +797,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr ).subscribe(task => { this.groupResponsesValidationTask = task; - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // Update progress if available - if (updatedTask.progress) { - // Could show progress here if needed - } - - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result const typedResult = result as { testTakersFound: boolean; groupsWithResponses: { group: string; hasResponse: boolean }[]; @@ -915,12 +815,9 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr page: number; limit: number; }; - - // Check if the result indicates errors const hasErrors = !typedResult.testTakersFound || !typedResult.allGroupsHaveResponses; - // Create a validation result with the appropriate status const validationResult: ValidationResult = { status: hasErrors ? 'failed' : 'success', timestamp: Date.now(), @@ -930,8 +827,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr hasErrors: hasErrors } }; - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult( this.appService.selectedWorkspaceId, 'groupResponses', @@ -943,8 +838,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedGroupResponses(); this.isGroupResponsesValidationRunning = false; this.groupResponsesValidationWasRun = true; - - // Save the validation result to the service this.saveValidationResult('groupResponses'); }); } else if (updatedTask.status === 'failed') { @@ -972,42 +865,27 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.validateDuplicateResponsesWasRun = false; this.currentDuplicateResponsesPage = 1; this.duplicateResponseSelections.clear(); - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'duplicateResponses', this.currentDuplicateResponsesPage, this.duplicateResponsesPageSize ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // Update progress if available - if (updatedTask.progress) { - // Could show progress here if needed - } - - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as DuplicateResponsesResultDto const typedResult = result as DuplicateResponsesResultDto; - - // Check if the result indicates errors (duplicate responses found) const hasErrors = typedResult.total > 0; - - // Create a validation result with the appropriate status const validationResult: ValidationResult = { status: hasErrors ? 'failed' : 'success', timestamp: Date.now(), @@ -1017,16 +895,13 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr } }; - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult( this.appService.selectedWorkspaceId, 'duplicateResponses', validationResult ); - // Convert to DuplicateResponseSelectionDto[] and initialize selections this.duplicateResponses = typedResult.data.map(duplicate => { - // For each duplicate, select the first response by default const defaultSelectedId = duplicate.duplicates.length > 0 ? duplicate.duplicates[0].responseId : undefined; @@ -1047,7 +922,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.isDuplicateResponsesValidationRunning = false; this.validateDuplicateResponsesWasRun = true; - // Save the validation result to the service this.saveValidationResult('duplicateResponses'); }); } else if (updatedTask.status === 'failed') { @@ -1082,37 +956,18 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr ); } - /** - * Checks if a specific response is selected for a duplicate - * @param duplicate The duplicate response - * @param responseId The response ID to check - * @returns True if the response is selected, false otherwise - */ isSelectedDuplicateResponse(duplicate: DuplicateResponseSelectionDto, responseId: number): boolean { return this.duplicateResponseSelections.get(duplicate.key) === responseId; } - /** - * Selects a specific response for a duplicate - * @param duplicate The duplicate response - * @param responseId The response ID to select - */ selectDuplicateResponse(duplicate: DuplicateResponseSelectionDto, responseId: number): void { this.duplicateResponseSelections.set(duplicate.key, responseId); } - /** - * Checks if any duplicate responses are selected - * @returns True if any duplicate responses are selected, false otherwise - */ hasSelectedDuplicateResponses(): boolean { return this.duplicateResponseSelections.size > 0; } - /** - * Gets the count of selected duplicate responses - * @returns The count of selected duplicate responses - */ getSelectedDuplicateResponsesCount(): number { return this.duplicateResponseSelections.size; } @@ -1120,41 +975,29 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr onDuplicateResponsesPageChange(event: PageEvent): void { this.currentDuplicateResponsesPage = event.pageIndex + 1; this.duplicateResponsesPageSize = event.pageSize; - - // Reload data from server with new pagination parameters using background task this.isDuplicateResponsesValidationRunning = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'duplicateResponses', this.currentDuplicateResponsesPage, this.duplicateResponsesPageSize ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as DuplicateResponsesResultDto const typedResult = result as DuplicateResponsesResultDto; - - // Convert to DuplicateResponseSelectionDto[] and preserve selections this.duplicateResponses = typedResult.data.map(duplicate => { const key = `${duplicate.unitId}_${duplicate.variableId}_${duplicate.testTakerLogin}`; - // If we don't have a selection for this duplicate yet, select the first response by default if (!this.duplicateResponseSelections.has(key) && duplicate.duplicates.length > 0) { this.duplicateResponseSelections.set(key, duplicate.duplicates[0].responseId); } @@ -1187,26 +1030,17 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.subscriptions.push(subscription); } - /** - * Resolves duplicate responses by keeping the selected responses - * This method sends the selected responses to the backend for resolution - */ resolveDuplicateResponses(): void { if (this.isResolvingDuplicateResponses || !this.hasSelectedDuplicateResponses()) { return; } this.isResolvingDuplicateResponses = true; - - // Create a map of response IDs to keep const responseIdsToKeep: Record = {}; - - // Convert the Map to a Record for the API request this.duplicateResponseSelections.forEach((responseId, key) => { responseIdsToKeep[key] = responseId; }); - // Call the validation service to resolve duplicates const request = { resolutionMap: responseIdsToKeep }; @@ -1243,18 +1077,11 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr }); } - /** - * Resolves all duplicate responses automatically by keeping the first response for each duplicate - * This method uses the deleteAllInvalidResponses endpoint with 'duplicateResponses' type - */ resolveAllDuplicateResponses(): void { if (this.isResolvingDuplicateResponses || this.duplicateResponses.length === 0) { return; } - this.isResolvingDuplicateResponses = true; - - // Create a background task to delete all duplicate responses except the first one const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'deleteAllResponses', @@ -1262,7 +1089,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr undefined, { validationType: 'duplicateResponses' } ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id @@ -1281,7 +1107,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr { duration: 3000 } ); - // Refresh the duplicate responses list this.validateDuplicateResponses(); this.isResolvingDuplicateResponses = false; }); @@ -1317,11 +1142,8 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.validateVariablesWasRun = false; this.selectedResponses.clear(); - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variables', @@ -1329,25 +1151,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.variablePageSize ).subscribe(task => { this.variableValidationTask = task; - - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // Update progress if available - if (updatedTask.progress) { - // Could show progress here if needed - } - - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as a PaginatedResponse const typedResult = result as { data: InvalidVariableDto[]; total: number; @@ -1355,10 +1168,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr limit: number; }; - // Check if the result indicates errors const hasErrors = typedResult.total > 0; - - // Create a validation result with the appropriate status const validationResult: ValidationResult = { status: hasErrors ? 'failed' : 'success', timestamp: Date.now(), @@ -1367,8 +1177,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr hasErrors: hasErrors } }; - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult( this.appService.selectedWorkspaceId, 'variables', @@ -1382,8 +1190,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedVariables(); this.isVariableValidationRunning = false; this.validateVariablesWasRun = true; - - // Save the validation result to the service this.saveValidationResult('variables'); }); } else if (updatedTask.status === 'failed') { @@ -1406,15 +1212,9 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr onVariablePageChange(event: PageEvent): void { this.currentVariablePage = event.pageIndex + 1; this.variablePageSize = event.pageSize; - - // Reload data from server with new pagination parameters using background task this.isVariableValidationRunning = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variables', @@ -1422,20 +1222,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.variablePageSize ).subscribe(task => { this.variableValidationTask = task; - - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as a PaginatedResponse const typedResult = result as { data: InvalidVariableDto[]; total: number; @@ -1499,26 +1295,19 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.snackBar.open('Keine Antworten ausgewählt', 'Schließen', { duration: 3000 }); return; } - this.isDeletingResponses = true; const responseIds = Array.from(this.selectedResponses); - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background deletion task const subscription = this.backendService.createDeleteResponsesTask( this.appService.selectedWorkspaceId, responseIds ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, @@ -1527,11 +1316,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr const typedResult = result as { deletedCount: number }; this.isDeletingResponses = false; this.snackBar.open(`${typedResult.deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 }); - - // Start background validation task to refresh the data this.isVariableValidationRunning = true; - - // Create a background validation task const validationSubscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variables', @@ -1539,20 +1324,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.variablePageSize ).subscribe(validationTask => { this.variableValidationTask = validationTask; - - // Poll for validation task completion const validationPollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, validationTask.id ).subscribe({ next: updatedValidationTask => { - // If task is completed, get the results if (updatedValidationTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedValidationTask.id ).subscribe(validationResult => { - // Type the result as a PaginatedResponse const typedValidationResult = validationResult as { data: InvalidVariableDto[]; total: number; @@ -1622,23 +1403,17 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr dialogRef.afterClosed().subscribe(deleteFromDb => { if (deleteFromDb) { this.isDeletingResponses = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background deletion task const subscription = this.backendService.createDeleteAllResponsesTask( this.appService.selectedWorkspaceId, 'variables' ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, @@ -1647,11 +1422,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr const typedResult = result as { deletedCount: number }; this.isDeletingResponses = false; this.snackBar.open(`${typedResult.deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 }); - - // Start background validation task to refresh the data this.isVariableValidationRunning = true; - - // Create a background validation task const validationSubscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variables', @@ -1659,20 +1430,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.variablePageSize ).subscribe(validationTask => { this.variableValidationTask = validationTask; - - // Poll for validation task completion const validationPollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, validationTask.id ).subscribe({ next: updatedValidationTask => { - // If task is completed, get the results if (updatedValidationTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedValidationTask.id ).subscribe(validationResult => { - // Type the result as a PaginatedResponse const typedValidationResult = validationResult as { data: InvalidVariableDto[]; total: number; @@ -1733,12 +1500,8 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.totalInvalidTypeVariables = 0; this.validateVariableTypesWasRun = false; this.selectedTypeResponses.clear(); - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variableTypes', @@ -1746,25 +1509,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.typeVariablePageSize ).subscribe(task => { this.variableTypeValidationTask = task; - - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // Update progress if available - if (updatedTask.progress) { - // Could show progress here if needed - } - - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as a PaginatedResponse const typedResult = result as { data: InvalidVariableDto[]; total: number; @@ -1772,10 +1526,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr limit: number; }; - // Check if the result indicates errors const hasErrors = typedResult.total > 0; - - // Create a validation result with the appropriate status const validationResult: ValidationResult = { status: hasErrors ? 'failed' : 'success', timestamp: Date.now(), @@ -1785,7 +1536,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr } }; - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult( this.appService.selectedWorkspaceId, 'variableTypes', @@ -1799,8 +1549,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedTypeVariables(); this.isVariableTypeValidationRunning = false; this.validateVariableTypesWasRun = true; - - // Save the validation result to the service this.saveValidationResult('variableTypes'); }); } else if (updatedTask.status === 'failed') { @@ -1826,12 +1574,8 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.totalInvalidStatusVariables = 0; this.validateResponseStatusWasRun = false; this.selectedStatusResponses.clear(); - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'responseStatus', @@ -1839,36 +1583,23 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.statusVariablePageSize ).subscribe(task => { this.responseStatusValidationTask = task; - - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // Update progress if available - if (updatedTask.progress) { - // Could show progress here if needed - } - - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as a PaginatedResponse const typedResult = result as { data: InvalidVariableDto[]; total: number; page: number; limit: number; }; - - // Check if the result indicates errors const hasErrors = typedResult.total > 0; - - // Create a validation result with the appropriate status const validationResult: ValidationResult = { status: hasErrors ? 'failed' : 'success', timestamp: Date.now(), @@ -1877,8 +1608,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr hasErrors: hasErrors } }; - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult( this.appService.selectedWorkspaceId, 'responseStatus', @@ -1892,8 +1621,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.updatePaginatedStatusVariables(); this.isResponseStatusValidationRunning = false; this.validateResponseStatusWasRun = true; - - // Save the validation result to the service this.saveValidationResult('responseStatus'); }); } else if (updatedTask.status === 'failed') { @@ -1916,15 +1643,9 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr onTypeVariablePageChange(event: PageEvent): void { this.currentTypeVariablePage = event.pageIndex + 1; this.typeVariablePageSize = event.pageSize; - - // Reload data from server with new pagination parameters using background task this.isVariableTypeValidationRunning = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variableTypes', @@ -1932,20 +1653,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.typeVariablePageSize ).subscribe(task => { this.variableTypeValidationTask = task; - - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as a PaginatedResponse const typedResult = result as { data: InvalidVariableDto[]; total: number; @@ -1981,15 +1698,9 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr onStatusVariablePageChange(event: PageEvent): void { this.currentStatusVariablePage = event.pageIndex + 1; this.statusVariablePageSize = event.pageSize; - - // Reload data from server with new pagination parameters using background task this.isResponseStatusValidationRunning = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background validation task const subscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'responseStatus', @@ -1997,20 +1708,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.statusVariablePageSize ).subscribe(task => { this.responseStatusValidationTask = task; - - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedTask.id ).subscribe(result => { - // Type the result as a PaginatedResponse const typedResult = result as { data: InvalidVariableDto[]; total: number; @@ -2074,26 +1781,19 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.snackBar.open('Keine Antworten ausgewählt', 'Schließen', { duration: 3000 }); return; } - this.isDeletingResponses = true; const responseIds = Array.from(this.selectedTypeResponses); - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background deletion task const subscription = this.backendService.createDeleteResponsesTask( this.appService.selectedWorkspaceId, responseIds ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, @@ -2102,11 +1802,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr const typedResult = result as { deletedCount: number }; this.isDeletingResponses = false; this.snackBar.open(`${typedResult.deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 }); - - // Start background validation task to refresh the data this.isVariableTypeValidationRunning = true; - - // Create a background validation task const validationSubscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variableTypes', @@ -2114,20 +1810,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.typeVariablePageSize ).subscribe(validationTask => { this.variableTypeValidationTask = validationTask; - - // Poll for validation task completion const validationPollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, validationTask.id ).subscribe({ next: updatedValidationTask => { - // If task is completed, get the results if (updatedValidationTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedValidationTask.id ).subscribe(validationResult => { - // Type the result as a PaginatedResponse const typedValidationResult = validationResult as { data: InvalidVariableDto[]; total: number; @@ -2197,23 +1889,17 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr dialogRef.afterClosed().subscribe(deleteFromDb => { if (deleteFromDb) { this.isDeletingResponses = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background deletion task const subscription = this.backendService.createDeleteAllResponsesTask( this.appService.selectedWorkspaceId, 'variableTypes' ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, @@ -2222,11 +1908,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr const typedResult = result as { deletedCount: number }; this.isDeletingResponses = false; this.snackBar.open(`${typedResult.deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 }); - - // Start background validation task to refresh the data this.isVariableTypeValidationRunning = true; - - // Create a background validation task const validationSubscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'variableTypes', @@ -2235,19 +1917,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr ).subscribe(validationTask => { this.variableTypeValidationTask = validationTask; - // Poll for validation task completion const validationPollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, validationTask.id ).subscribe({ next: updatedValidationTask => { - // If task is completed, get the results if (updatedValidationTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedValidationTask.id ).subscribe(validationResult => { - // Type the result as a PaginatedResponse const typedValidationResult = validationResult as { data: InvalidVariableDto[]; total: number; @@ -2336,23 +2015,17 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.isDeletingResponses = true; const responseIds = Array.from(this.selectedStatusResponses); - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background deletion task const subscription = this.backendService.createDeleteResponsesTask( this.appService.selectedWorkspaceId, responseIds ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, @@ -2361,11 +2034,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr const typedResult = result as { deletedCount: number }; this.isDeletingResponses = false; this.snackBar.open(`${typedResult.deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 }); - - // Start background validation task to refresh the data this.isResponseStatusValidationRunning = true; - - // Create a background validation task const validationSubscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'responseStatus', @@ -2374,19 +2043,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr ).subscribe(validationTask => { this.responseStatusValidationTask = validationTask; - // Poll for validation task completion const validationPollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, validationTask.id ).subscribe({ next: updatedValidationTask => { - // If task is completed, get the results if (updatedValidationTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedValidationTask.id ).subscribe(validationResult => { - // Type the result as a PaginatedResponse const typedValidationResult = validationResult as { data: InvalidVariableDto[]; total: number; @@ -2456,23 +2122,17 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr dialogRef.afterClosed().subscribe(deleteFromDb => { if (deleteFromDb) { this.isDeletingResponses = true; - - // Cancel any existing subscription this.subscriptions.forEach(sub => sub.unsubscribe()); this.subscriptions = []; - - // Create a background deletion task const subscription = this.backendService.createDeleteAllResponsesTask( this.appService.selectedWorkspaceId, 'responseStatus' ).subscribe(task => { - // Poll for task completion const pollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, task.id ).subscribe({ next: updatedTask => { - // If task is completed, get the results if (updatedTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, @@ -2481,11 +2141,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr const typedResult = result as { deletedCount: number }; this.isDeletingResponses = false; this.snackBar.open(`${typedResult.deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 }); - - // Start background validation task to refresh the data this.isResponseStatusValidationRunning = true; - - // Create a background validation task const validationSubscription = this.backendService.createValidationTask( this.appService.selectedWorkspaceId, 'responseStatus', @@ -2493,20 +2149,16 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.statusVariablePageSize ).subscribe(validationTask => { this.responseStatusValidationTask = validationTask; - - // Poll for validation task completion const validationPollSubscription = this.backendService.pollValidationTask( this.appService.selectedWorkspaceId, validationTask.id ).subscribe({ next: updatedValidationTask => { - // If task is completed, get the results if (updatedValidationTask.status === 'completed') { this.backendService.getValidationResults( this.appService.selectedWorkspaceId, updatedValidationTask.id ).subscribe(validationResult => { - // Type the result as a PaginatedResponse const typedValidationResult = validationResult as { data: InvalidVariableDto[]; total: number; @@ -2684,17 +2336,12 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr private saveValidationResult(type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'duplicateResponses'): void { const workspaceId = this.appService.selectedWorkspaceId; const status = this.getValidationStatus(type); - - // Only save completed results (success or failed) if (status === 'success' || status === 'failed') { - // Create validation result object const validationResult = { status: status as 'success' | 'failed', timestamp: Date.now(), details: this.getValidationDetails(type) }; - - // Save to service this.validationTaskStateService.setValidationResult(workspaceId, type, validationResult); } } @@ -2744,12 +2391,7 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr private loadPreviousValidationResults(): void { const workspaceId = this.appService.selectedWorkspaceId; - - // First load any in-memory results const inMemoryResults = this.validationTaskStateService.getAllValidationResults(workspaceId); - - // Process each type of in-memory validation result - // Pass false for fromCurrentSession since these are from previous sessions if (inMemoryResults.variables) { this.processVariablesResult(inMemoryResults.variables, false); } @@ -2770,11 +2412,9 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.processGroupResponsesResult(inMemoryResults.groupResponses, false); } - // Then fetch and process the last validation results from the backend const subscription = this.validationService.getLastValidationResults(workspaceId) .subscribe({ next: results => { - // Process each type of validation result from the backend if (results.variables) { const { task, result } = results.variables; let status: 'success' | 'failed' | 'not-run' = 'not-run'; @@ -2787,8 +2427,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr details: result }; this.processVariablesResult(validationResult, false); - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult(workspaceId, 'variables', validationResult); } @@ -2804,8 +2442,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr details: result }; this.processVariableTypesResult(validationResult, false); - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult(workspaceId, 'variableTypes', validationResult); } @@ -2821,8 +2457,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr details: result }; this.processResponseStatusResult(validationResult, false); - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult(workspaceId, 'responseStatus', validationResult); } @@ -2838,8 +2472,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr details: result }; this.processTestTakersResult(validationResult, false); - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult(workspaceId, 'testTakers', validationResult); } @@ -2855,13 +2487,10 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr details: result }; this.processGroupResponsesResult(validationResult, false); - - // Store the result in the validation task state service this.validationTaskStateService.setValidationResult(workspaceId, 'groupResponses', validationResult); } }, error: () => { - // Error occurred while loading previous validation results } }); @@ -2870,7 +2499,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr private processVariablesResult(result: ValidationResult, fromCurrentSession: boolean = false): void { if (result.status === 'success') { - // Only set validateVariablesWasRun to true if the result is from the current session if (fromCurrentSession) { this.validateVariablesWasRun = true; } @@ -2878,14 +2506,12 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.invalidVariables = []; this.updatePaginatedVariables(); } else if (result.status === 'failed' && result.details) { - // Only set validateVariablesWasRun to true if the result is from the current session if (fromCurrentSession) { this.validateVariablesWasRun = true; } const details = result.details as { total: number; hasErrors: boolean }; this.totalInvalidVariables = details.total; - // If we have details but no data, we need to fetch the data if (details.total > 0 && this.invalidVariables.length === 0) { this.validateVariables(); } @@ -2894,7 +2520,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr private processVariableTypesResult(result: ValidationResult, fromCurrentSession: boolean = false): void { if (result.status === 'success') { - // Only set validateVariableTypesWasRun to true if the result is from the current session if (fromCurrentSession) { this.validateVariableTypesWasRun = true; } @@ -2902,14 +2527,12 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.invalidTypeVariables = []; this.updatePaginatedTypeVariables(); } else if (result.status === 'failed' && result.details) { - // Only set validateVariableTypesWasRun to true if the result is from the current session if (fromCurrentSession) { this.validateVariableTypesWasRun = true; } const details = result.details as { total: number; hasErrors: boolean }; this.totalInvalidTypeVariables = details.total; - // If we have details but no data, we need to fetch the data if (details.total > 0 && this.invalidTypeVariables.length === 0) { this.validateVariableTypes(); } @@ -2918,7 +2541,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr private processResponseStatusResult(result: ValidationResult, fromCurrentSession: boolean = false): void { if (result.status === 'success') { - // Only set validateResponseStatusWasRun to true if the result is from the current session if (fromCurrentSession) { this.validateResponseStatusWasRun = true; } @@ -2926,14 +2548,12 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr this.invalidStatusVariables = []; this.updatePaginatedStatusVariables(); } else if (result.status === 'failed' && result.details) { - // Only set validateResponseStatusWasRun to true if the result is from the current session if (fromCurrentSession) { this.validateResponseStatusWasRun = true; } const details = result.details as { total: number; hasErrors: boolean }; this.totalInvalidStatusVariables = details.total; - // If we have details but no data, we need to fetch the data if (details.total > 0 && this.invalidStatusVariables.length === 0) { this.validateResponseStatus(); } @@ -2942,7 +2562,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr private processTestTakersResult(result: ValidationResult, fromCurrentSession: boolean = false): void { if (result.status === 'success') { - // Only set testTakersValidationWasRun to true if the result is from the current session if (fromCurrentSession) { this.testTakersValidationWasRun = true; } @@ -2955,7 +2574,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr }; this.updatePaginatedMissingPersons(); } else if (result.status === 'failed' && result.details) { - // Only set testTakersValidationWasRun to true if the result is from the current session if (fromCurrentSession) { this.testTakersValidationWasRun = true; } @@ -2965,7 +2583,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr hasErrors: boolean }; - // If we have details but no data, we need to fetch the data if (details.hasErrors && (!this.testTakersValidationResult || this.testTakersValidationResult.missingPersons.length === 0)) { this.validateTestTakers(); } @@ -2974,7 +2591,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr private processGroupResponsesResult(result: ValidationResult, fromCurrentSession: boolean = false): void { if (result.status === 'success') { - // Only set groupResponsesValidationWasRun to true if the result is from the current session if (fromCurrentSession) { this.groupResponsesValidationWasRun = true; } @@ -2985,7 +2601,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr }; this.updatePaginatedGroupResponses(); } else if (result.status === 'failed' && result.details) { - // Only set groupResponsesValidationWasRun to true if the result is from the current session if (fromCurrentSession) { this.groupResponsesValidationWasRun = true; } @@ -2995,7 +2610,6 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestr hasErrors: boolean }; - // If we have details but no data, we need to fetch the data if (details.hasErrors && (!this.groupResponsesResult || this.groupResponsesResult.groupsWithResponses.length === 0)) { this.validateGroupResponses(); } diff --git a/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.html b/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.html index 6584fec3e..75970aed8 100755 --- a/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.html +++ b/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.html @@ -21,6 +21,10 @@
    + } @else { +
    +

    {{ 'ws-admin.no-access' | translate }}

    +
    }
    diff --git a/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.scss b/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.scss index f5240cd1d..fe66fbf6a 100755 --- a/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.scss +++ b/apps/frontend/src/app/ws-admin/components/ws-admin/ws-admin.component.scss @@ -35,3 +35,9 @@ a{ flex: 1; overflow: auto; } + +.no-access-message { + text-align: center; + padding: 50px; + font-size: 18px; +} diff --git a/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.html b/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.html index f84542eec..66c874f8a 100755 --- a/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.html +++ b/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.html @@ -71,6 +71,29 @@

    Generiertes Token

    + + + Kodierung + + +
    +
    +
    +

    Automatisches Laden der Kodierstatistiken

    +

    Wenn aktiviert, werden die Kodierstatistiken automatisch beim Öffnen der Kodierungsverwaltung geladen. Wenn deaktiviert, müssen die Statistiken manuell über einen Button geladen werden.

    +
    +
    + + {{autoFetchCodingStatistics ? 'Aktiviert' : 'Deaktiviert'}} + +
    +
    +
    +
    +
    + Nutzerrechte @@ -88,4 +111,32 @@

    Generiertes Token

    + + + + Testergebnisse exportieren + + +
    +

    Exportieren Sie die Testergebnisse dieses Arbeitsbereichs als SQLite-Datenbank. Die Exportdatei enthält alle relevanten Daten wie Personen, Testhefte, Aufgaben, Antworten und Protokolle.

    +
    + +
    + @if (isExporting) { + + } +
    +
    +
    diff --git a/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.ts b/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.ts index f0cd7a534..ff5b69378 100755 --- a/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.ts +++ b/apps/frontend/src/app/ws-admin/components/ws-settings/ws-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { TranslateModule } from '@ngx-translate/core'; import { MatButtonModule } from '@angular/material/button'; @@ -8,6 +8,8 @@ import { MatCardModule } from '@angular/material/card'; import { MatIconModule } from '@angular/material/icon'; import { MatSnackBar } from '@angular/material/snack-bar'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { Clipboard } from '@angular/cdk/clipboard'; @@ -16,6 +18,7 @@ import { WsAccessRightsComponent } from '../ws-access-rights/ws-access-rights.co import { JournalComponent } from '../journal/journal.component'; import { EditMissingsProfilesDialogComponent } from '../../../coding/components/edit-missings-profiles-dialog/edit-missings-profiles-dialog.component'; import { ReplayStatisticsDialogComponent } from '../replay-statistics-dialog/replay-statistics-dialog.component'; +import { WorkspaceSettingsService } from '../../services/workspace-settings.service'; @Component({ selector: 'coding-box-ws-settings', @@ -31,19 +34,34 @@ import { ReplayStatisticsDialogComponent } from '../replay-statistics-dialog/rep MatCardModule, MatIconModule, MatDialogModule, + MatSlideToggleModule, + MatProgressBarModule, CdkTextareaAutosize, WsAccessRightsComponent, JournalComponent ] }) -export class WsSettingsComponent { +export class WsSettingsComponent implements OnInit { private appService = inject(AppService); + private workspaceSettingsService = inject(WorkspaceSettingsService); private clipboard = inject(Clipboard); private snackBar = inject(MatSnackBar); private dialog = inject(MatDialog); authToken: string | null = null; duration = 60; + autoFetchCodingStatistics = true; + isExporting = false; + + ngOnInit(): void { + const workspaceId = this.appService.selectedWorkspaceId; + if (workspaceId) { + this.workspaceSettingsService.getAutoFetchCodingStatistics(workspaceId) + .subscribe(enabled => { + this.autoFetchCodingStatistics = enabled; + }); + } + } openReplayStatistics(): void { const workspaceId = this.appService.selectedWorkspaceId; @@ -80,4 +98,91 @@ export class WsSettingsComponent { }); } } + + toggleAutoFetchCodingStatistics(toggleEvent: { checked: boolean } + ): void { + this.autoFetchCodingStatistics = toggleEvent.checked; + const workspaceId = this.appService.selectedWorkspaceId; + + if (workspaceId) { + this.workspaceSettingsService.setAutoFetchCodingStatistics(workspaceId, this.autoFetchCodingStatistics) + .subscribe({ + next: () => { + this.snackBar.open( + this.autoFetchCodingStatistics ? + 'Automatisches Laden der Kodierstatistiken aktiviert' : + 'Automatisches Laden der Kodierstatistiken deaktiviert', + 'Schließen', + { duration: 3000 } + ); + }, + error: () => { + this.snackBar.open('Fehler beim Speichern der Einstellung', 'Schließen', { + duration: 3000, + panelClass: ['error-snackbar'] + }); + this.autoFetchCodingStatistics = !this.autoFetchCodingStatistics; + } + }); + } + } + + exportWorkspaceDatabase(): void { + if (this.isExporting) { + return; + } + + const workspaceId = this.appService.selectedWorkspaceId; + if (!workspaceId) { + this.snackBar.open('Kein Arbeitsbereich ausgewählt', 'Schließen', { duration: 3000 }); + return; + } + + this.isExporting = true; + const anchor = document.createElement('a'); + anchor.style.display = 'none'; + document.body.appendChild(anchor); + + const apiUrl = `${window.location.origin}/api/admin/workspace/${workspaceId}/export/sqlite`; + const token = localStorage.getItem('id_token'); + + if (!token) { + this.snackBar.open('Nicht authentifiziert. Bitte melden Sie sich erneut an.', 'Schließen', { duration: 5000 }); + this.isExporting = false; + return; + } + + fetch(apiUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/x-sqlite3' + } + }) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.blob(); + }) + .then(blob => { + const url = window.URL.createObjectURL(blob); + anchor.href = url; + anchor.download = `workspace-${workspaceId}-export-${new Date().toISOString().split('T')[0]}.sqlite`; + anchor.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(anchor); + + this.snackBar.open('Arbeitsbereich-Datenbank erfolgreich exportiert', 'Schließen', { duration: 3000 }); + }) + .catch(() => { + this.snackBar.open('Fehler beim Exportieren der Arbeitsbereich-Datenbank. Bitte versuchen Sie es erneut.', 'Schließen', { duration: 5000 }); + if (document.body.contains(anchor)) { + document.body.removeChild(anchor); + } + }) + .finally(() => { + this.isExporting = false; + }); + } } diff --git a/apps/frontend/src/app/ws-admin/services/workspace-settings.service.ts b/apps/frontend/src/app/ws-admin/services/workspace-settings.service.ts new file mode 100644 index 000000000..225cb96cc --- /dev/null +++ b/apps/frontend/src/app/ws-admin/services/workspace-settings.service.ts @@ -0,0 +1,66 @@ +import { Injectable, inject } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { SERVER_URL } from '../../injection-tokens'; +import { WorkspaceSettings } from '../models/workspace-settings.model'; + +@Injectable({ + providedIn: 'root' +}) +export class WorkspaceSettingsService { + private http = inject(HttpClient); + private serverUrl = inject(SERVER_URL); + + getWorkspaceSetting(workspaceId: number, key: string): Observable { + return this.http.get(`${this.serverUrl}/api/workspace/${workspaceId}/settings/${key}`); + } + + setWorkspaceSetting(workspaceId: number, key: string, value: string, description?: string): Observable { + return this.http.post(`${this.serverUrl}/api/workspace/${workspaceId}/settings`, { + key, + value, + description + }); + } + + updateWorkspaceSetting(workspaceId: number, settingId: number, value: string): Observable { + return this.http.put(`${this.serverUrl}/api/workspace/${workspaceId}/settings/${settingId}`, { + value + }); + } + + deleteWorkspaceSetting(workspaceId: number, settingId: number): Observable { + return this.http.delete(`${this.serverUrl}/api/workspace/${workspaceId}/settings/${settingId}`); + } + + getAutoFetchCodingStatistics(workspaceId: number): Observable { + return new Observable(observer => { + this.getWorkspaceSetting(workspaceId, 'auto-fetch-coding-statistics') + .subscribe({ + next: setting => { + try { + const parsed = JSON.parse(setting.value); + observer.next(parsed.enabled ?? true); // Default to true + } catch { + observer.next(true); // Default to true if parsing fails + } + observer.complete(); + }, + error: () => { + observer.next(true); // Default to true if setting doesn't exist + observer.complete(); + } + }); + }); + } + + setAutoFetchCodingStatistics(workspaceId: number, enabled: boolean): Observable { + const value = JSON.stringify({ enabled }); + return this.setWorkspaceSetting( + workspaceId, + 'auto-fetch-coding-statistics', + value, + 'Controls whether coding statistics are automatically fetched in the coding management component' + ); + } +} diff --git a/apps/frontend/src/assets/i18n/de.json b/apps/frontend/src/assets/i18n/de.json index 0793e6e82..c88a212be 100755 --- a/apps/frontend/src/assets/i18n/de.json +++ b/apps/frontend/src/assets/i18n/de.json @@ -170,7 +170,8 @@ "export-subtitle": "Export der Daten nach Kodierung und Bereinigung", "export-description": "Diese Komponente wird für den Export der Daten nach Kodierung und Bereinigung verwendet.", "export-placeholder": "Die Funktionalität zum Datenexport wird in einer zukünftigen Version implementiert.", - "export-action-button": "Datenexport starten" + "export-action-button": "Datenexport starten", + "no-access": "Sie haben keinen Zugriff auf diesen Arbeitsbereich. Bitte kontaktieren Sie den Studienadministrator für Zugriff." }, "search-filter": { "filter-users": "Suche nach Nutzer:innen", @@ -327,5 +328,14 @@ "failure-distribution-by-hour": "Verteilung der Fehler nach Stunde", "failure-count": "Anzahl der Fehler", "no-failures": "Keine Fehler vorhanden" + }, + + "paginator": { + "itemsPerPageLabel": "Elemente pro Seite:", + "nextPageLabel": "Nächste Seite", + "previousPageLabel": "Vorherige Seite", + "firstPageLabel": "Erste Seite", + "lastPageLabel": "Letzte Seite", + "getRangeLabel": "{{startIndex}} - {{endIndex}} von {{length}}" } } diff --git a/database/changelog/coding-box.changelog-1.1.0.sql b/database/changelog/coding-box.changelog-1.1.0.sql new file mode 100644 index 000000000..38c502f2b --- /dev/null +++ b/database/changelog/coding-box.changelog-1.1.0.sql @@ -0,0 +1,28 @@ +-- liquibase formatted sql + +-- changeset jurei733:1 +-- Add new columns to response table for coding functionality + +ALTER TABLE "public"."response" ADD COLUMN "status_v2" TEXT; +ALTER TABLE "public"."response" ADD COLUMN "code_v2" INTEGER; +ALTER TABLE "public"."response" ADD COLUMN "score_v2" INTEGER; +ALTER TABLE "public"."response" ADD COLUMN "status_v3" TEXT; +ALTER TABLE "public"."response" ADD COLUMN "code_v3" INTEGER; +ALTER TABLE "public"."response" ADD COLUMN "score_v3" INTEGER; + +-- rollback ALTER TABLE "public"."response" DROP COLUMN IF EXISTS "status_v2"; +-- rollback ALTER TABLE "public"."response" DROP COLUMN IF EXISTS "code_v2"; +-- rollback ALTER TABLE "public"."response" DROP COLUMN IF EXISTS "score_v2"; +-- rollback ALTER TABLE "public"."response" DROP COLUMN IF EXISTS "status_v3"; +-- rollback ALTER TABLE "public"."response" DROP COLUMN IF EXISTS "code_v3"; +-- rollback ALTER TABLE "public"."response" DROP COLUMN IF EXISTS "score_v3"; + +-- changeset jurei733:2 +-- comment: Migrate response table columns from codedstatus to status_v1, code to code_v1, and score to score_v1 +ALTER TABLE "public"."response" RENAME COLUMN "codedstatus" TO "status_v1"; +ALTER TABLE "public"."response" RENAME COLUMN "code" TO "code_v1"; +ALTER TABLE "public"."response" RENAME COLUMN "score" TO "score_v1"; + +-- rollback ALTER TABLE "public"."response" RENAME COLUMN "status_v1" TO "codedstatus"; +-- rollback ALTER TABLE "public"."response" RENAME COLUMN "code_v1" TO "code"; +-- rollback ALTER TABLE "public"."response" RENAME COLUMN "score_v1" TO "score"; diff --git a/database/changelog/coding-box.changelog-root.xml b/database/changelog/coding-box.changelog-root.xml index e922aaf30..d377f53f8 100644 --- a/database/changelog/coding-box.changelog-root.xml +++ b/database/changelog/coding-box.changelog-root.xml @@ -22,4 +22,5 @@ + diff --git a/docker-compose.yaml b/docker-compose.yaml index ef83b3f6e..ff66eee03 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,6 +10,12 @@ x-env-redis: &env-redis REDIS_PORT: 6379 REDIS_PREFIX: coding-box +x-env-keycloak: &env-keycloak + KEYCLOAK_URL: ${KEYCLOAK_URL} + KEYCLOAK_REALM: ${KEYCLOAK_REALM} + KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID} + KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET} + services: redis: image: redis:alpine @@ -60,7 +66,7 @@ services: environment: API_HOST: backend JWT_SECRET: ${JWT_SECRET} - <<: [ *env-redis, *env-postgres ] + <<: [ *env-redis, *env-postgres, *env-keycloak ] networks: - application-network @@ -72,11 +78,6 @@ services: condition: service_completed_successfully backend: condition: service_started - environment: - - KEYCLOAK_URL=${KEYCLOAK_URL:-https://keycloak.kodierbox.iqb.hu-berlin.de/} - - KEYCLOAK_REALM=${KEYCLOAK_REALM:-iqb} - - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-coding-box} - - BACKEND_URL=${BACKEND_URL:-api/} networks: - application-network diff --git a/package-lock.json b/package-lock.json index cac9f8b55..8db1c59ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coding-box", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coding-box", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { "@angular/animations": "20.0.3", @@ -19,8 +19,9 @@ "@angular/platform-browser": "20.0.3", "@angular/platform-browser-dynamic": "20.0.3", "@angular/router": "20.0.3", - "@iqb/responses": "^3.6.0", - "@iqbspecs/response": "1.4.0", + "@iqb/responses": "^4.0.2", + "@iqbspecs/coding-scheme": "^3.2.0", + "@iqbspecs/response": "1.5.1", "@iqbspecs/variable-info": "1.3.0", "@nestjs-modules/ioredis": "^2.0.2", "@nestjs/axios": "^4.0.1", @@ -43,12 +44,13 @@ "adm-zip": "^0.5.9", "ajv": "^8.17.1", "ajv-keywords": "^5.1.0", - "axios": "^1.3.1", + "axios": "^1.12.2", "bull": "^4.16.5", "cheerio": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "docx": "^9.5.1", + "domhandler": "^5.0.3", "exceljs": "^4.4.0", "fast-csv": "^5.0.1", "file-saver-es": "^2.0.5", @@ -63,6 +65,7 @@ "pg": "^8.11.5", "reflect-metadata": "^0.2.0", "rxjs": "~7.8.0", + "sqlite3": "^5.1.7", "stream": "^0.0.2", "timers": "^0.1.1", "tslib": "^2.3.0", @@ -78,7 +81,7 @@ "@angular/compiler-cli": "20.0.3", "@golevelup/ts-jest": "^0.5.0", "@iqb/eslint-config": "^2.1.1", - "@nx/angular": "21.3.1", + "@nx/angular": "21.5.2", "@nx/cypress": "21.2.0", "@nx/esbuild": "21.2.0", "@nx/eslint": "21.2.0", @@ -106,13 +109,6 @@ "typescript": "5.8.3" } }, - "node_modules/@adobe/css-tools": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", - "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -4520,20 +4516,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", - "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", "dev": true, "license": "MIT", "dependencies": { - "@emnapi/wasi-threads": "1.0.1", + "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4541,9 +4537,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", - "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5147,6 +5143,13 @@ "lodash.uniq": "^4.5.0" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, "node_modules/@golevelup/ts-jest": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@golevelup/ts-jest/-/ts-jest-0.5.0.tgz", @@ -5259,9 +5262,9 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.15", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.15.tgz", - "integrity": "sha512-8xrp836RZvKkpNbVvgWUlxjT4CraKk2q+I3Ksy+seI2zkcE+y6wNs1BVhgcv8VyImFecUhdQrYLdW32pAjwBdA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.0.tgz", + "integrity": "sha512-NyDSjPqhSvpZEMZrLCYUquWNl+XC/moEcVFqS55IEYIYsY0a1cUCevSqk7ctOlnm/RaSBU5psFryNlxcmGrjaA==", "dev": true, "license": "MIT", "dependencies": { @@ -5300,13 +5303,13 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.17", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.17.tgz", - "integrity": "sha512-r6bQLsyPSzbWrZZ9ufoWL+CztkSatnJ6uSxqd6N+o41EZC51sQeWOzI6s5jLb+xxTWxl7PlUppqm8/sow241gg==", + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.18.tgz", + "integrity": "sha512-yeQN3AXjCm7+Hmq5L6Dm2wEDeBRdAZuyZ4I7tWSSanbxDzqM0KqzoDbKM7p4ebllAYdoQuPJS6N71/3L281i6w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.15", + "@inquirer/core": "^10.2.0", "@inquirer/external-editor": "^1.0.1", "@inquirer/type": "^3.0.8" }, @@ -5596,18 +5599,24 @@ } }, "node_modules/@iqb/responses": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@iqb/responses/-/responses-3.6.0.tgz", - "integrity": "sha512-x4Qs11JdF3FYAYIplA1y7sorV6yN7dVNaJ0zdpDbcPkD/ZyRn9Ntgl00jDOk90RUiNooPUDczZ/Xifg6umlWfg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@iqb/responses/-/responses-4.0.2.tgz", + "integrity": "sha512-bEm8Fh+r240MV6YNwJIu/QlD5r0d/BfxFKZfvmXcDPqfPjIWxCrs6hyoauUtI18c7Wx0qkmGINJ5ntxTy87D+A==", "license": "CC0 1.0", "dependencies": { "mathjs": "^12.4.2" } }, + "node_modules/@iqbspecs/coding-scheme": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@iqbspecs/coding-scheme/-/coding-scheme-3.2.0.tgz", + "integrity": "sha512-lPE6W3yNEuPbwk97boYn/ndO+yM0+4MHVKWvKSebG1trOo8jJVaWyMjTYmSRs+HC6L3ogO5Rj0pXVEl9e+G65w==", + "license": "CC0 1.0" + }, "node_modules/@iqbspecs/response": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@iqbspecs/response/-/response-1.4.0.tgz", - "integrity": "sha512-GRpKAl0EUcMvJlniUrViRYdFigZ3UR26RE/BinRhbFR4p3FNWdbC7stAhUPkOio/FRBGdPRh+QL7ScCA5B1lDg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@iqbspecs/response/-/response-1.5.1.tgz", + "integrity": "sha512-rHrGOnd5jrpYPeBXGLFpn+xym0E+DavyiAWU7x8Uuolg+hbUtG1a7bkRuSzGX+0HiDbhhptvGHrealagsEdNgA==", "license": "CC0 1.0" }, "node_modules/@iqbspecs/variable-info": { @@ -5902,9 +5911,9 @@ } }, "node_modules/@jest/get-type": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", "engines": { @@ -6151,17 +6160,75 @@ "tslib": "2" } }, + "node_modules/@jsonjoy.com/buffers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz", + "integrity": "sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/@jsonjoy.com/json-pack": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", - "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.12.0.tgz", + "integrity": "sha512-qPwWjCmcwtDiY9MZ4hz3YY4UYkVleWq5rXt+EEx4Jtl22byaLlfBEn8vXze43P2/30DkwzQvdA56EB3TH7t3Pg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/base64": "^1.1.1", - "@jsonjoy.com/util": "^1.1.2", + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.1", + "@jsonjoy.com/util": "^1.9.0", "hyperdyperid": "^1.2.0", - "thingies": "^1.20.0" + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" }, "engines": { "node": ">=10.0" @@ -6175,11 +6242,15 @@ } }, "node_modules/@jsonjoy.com/util": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", - "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, "engines": { "node": ">=10.0" }, @@ -6841,27 +6912,27 @@ } }, "node_modules/@module-federation/bridge-react-webpack-plugin": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.17.1.tgz", - "integrity": "sha512-lv06kqarQJtXnOZ5Kd7SIH2mAi+O3cwqS5/EiSlXDNU5hBsqsInFMeHpj8nY0wwNzeYv4o7t/F1QFQkaqAVEwQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.18.4.tgz", + "integrity": "sha512-tYgso9izSinWzzVlsOUsBjW5lPMsvsVp95Jrw5W4Ajg9Un/yTkjOqEqmsMYpiL7drEN2+gPPVYyQ/hUK4QWz8Q==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "0.17.1", + "@module-federation/sdk": "0.18.4", "@types/semver": "7.5.8", "semver": "7.6.3" } }, "node_modules/@module-federation/cli": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-0.17.1.tgz", - "integrity": "sha512-jXA/ZutIfEyk0va8Q0ufJcZoG/w5kyJj4xvV4/LXAfcAOv/aKR/Mp51YrAIDAyEJN8i05y+dLMzLRfhewFK4GA==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-0.18.4.tgz", + "integrity": "sha512-31c+2OjtRdsYq7oV+rCoTO9AXizT3D9CNzofZ9EVRGsaS9+H+nJKTkK+pw+IhK0Y8I0HsP+uxgLrazqF0tLbgg==", "dev": true, "license": "MIT", "dependencies": { "@modern-js/node-bundle-require": "2.68.2", - "@module-federation/dts-plugin": "0.17.1", - "@module-federation/sdk": "0.17.1", + "@module-federation/dts-plugin": "0.18.4", + "@module-federation/sdk": "0.18.4", "chalk": "3.0.0", "commander": "11.1.0" }, @@ -6887,14 +6958,14 @@ } }, "node_modules/@module-federation/data-prefetch": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-0.17.1.tgz", - "integrity": "sha512-kRS9LWbK/agC2ybO2Y2Xj3JfoyyBxOxwpxwftl1KnuWBPafV6dpvKxn5ig3im5OWHsYLd/W8W4XyGsSQdVoyIw==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-0.18.4.tgz", + "integrity": "sha512-XOHFFO1wrVbjjfP2JRMbht+ILim5Is6Mfb5f2H4I9w0CSaZNRltG0fTnebECB1jgosrd8xaYnrwzXsCI/S53qQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.17.1", - "@module-federation/sdk": "0.17.1", + "@module-federation/runtime": "0.18.4", + "@module-federation/sdk": "0.18.4", "fs-extra": "9.1.0" }, "peerDependencies": { @@ -6903,23 +6974,23 @@ } }, "node_modules/@module-federation/dts-plugin": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-0.17.1.tgz", - "integrity": "sha512-cRvHorIlVBUfh2UCQySZ7026CyzCJqQxwFzF4E1kp+mmIGxRpr4wLZA8GshThYvwN6dkeHINuKuzFmErhtFhAQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-0.18.4.tgz", + "integrity": "sha512-5FlrajLCypQ8+vEsncgEGpDmxUDG+Ub6ogKOE00e2gMxcYlgcCZNUSn5VbEGdCMcHQmIK2xt3WGQT30/7j2KiQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.17.1", - "@module-federation/managers": "0.17.1", - "@module-federation/sdk": "0.17.1", - "@module-federation/third-party-dts-extractor": "0.17.1", + "@module-federation/error-codes": "0.18.4", + "@module-federation/managers": "0.18.4", + "@module-federation/sdk": "0.18.4", + "@module-federation/third-party-dts-extractor": "0.18.4", "adm-zip": "^0.5.10", "ansi-colors": "^4.1.3", - "axios": "^1.8.2", + "axios": "^1.11.0", "chalk": "3.0.0", "fs-extra": "9.1.0", "isomorphic-ws": "5.0.0", - "koa": "2.16.1", + "koa": "3.0.1", "lodash.clonedeepwith": "4.5.0", "log4js": "6.9.1", "node-schedule": "2.1.1", @@ -6951,23 +7022,23 @@ } }, "node_modules/@module-federation/enhanced": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-0.17.1.tgz", - "integrity": "sha512-YEdHA/rXlydI+ecmsidM0imAhAgyN+fSCOWRJtm72Kx10J6kS2tN1/Zah/hf9C9Msj9OOl0w22aOo7/Sy0qRqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "0.17.1", - "@module-federation/cli": "0.17.1", - "@module-federation/data-prefetch": "0.17.1", - "@module-federation/dts-plugin": "0.17.1", - "@module-federation/error-codes": "0.17.1", - "@module-federation/inject-external-runtime-core-plugin": "0.17.1", - "@module-federation/managers": "0.17.1", - "@module-federation/manifest": "0.17.1", - "@module-federation/rspack": "0.17.1", - "@module-federation/runtime-tools": "0.17.1", - "@module-federation/sdk": "0.17.1", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-0.18.4.tgz", + "integrity": "sha512-KiBw7e+aIBFoO2cmN5hJlKrYv3nUuXsB8yOSVnV9JBAkYNyRZQ9xoSbRCDt8rDRz/ydgEURUIwnGyL2ZU5jZYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/bridge-react-webpack-plugin": "0.18.4", + "@module-federation/cli": "0.18.4", + "@module-federation/data-prefetch": "0.18.4", + "@module-federation/dts-plugin": "0.18.4", + "@module-federation/error-codes": "0.18.4", + "@module-federation/inject-external-runtime-core-plugin": "0.18.4", + "@module-federation/managers": "0.18.4", + "@module-federation/manifest": "0.18.4", + "@module-federation/rspack": "0.18.4", + "@module-federation/runtime-tools": "0.18.4", + "@module-federation/sdk": "0.18.4", "btoa": "^1.2.1", "schema-utils": "^4.3.0", "upath": "2.0.1" @@ -6993,44 +7064,44 @@ } }, "node_modules/@module-federation/error-codes": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.17.1.tgz", - "integrity": "sha512-n6Elm4qKSjwAPxLUGtwnl7qt4y1dxB8OpSgVvXBIzqI9p27a3ZXshLPLnumlpPg1Qudaj8sLnSnFtt9yGpt5yQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.4.tgz", + "integrity": "sha512-cpLsqL8du9CfTTCKvXbRg93ALF+lklqHnuPryhbwVEQg2eYo6CMoMQ6Eb7kJhLigUABIDujbHD01SvBbASGkeQ==", "dev": true, "license": "MIT" }, "node_modules/@module-federation/inject-external-runtime-core-plugin": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-0.17.1.tgz", - "integrity": "sha512-Wqi6VvPHK5LKkLPhXgabulHygQKDJxreWs+LyrA5/LFGXHwD/7cM+V/xHriVJIbU+5HeKBT7y0Jyfe6uW1p/dQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-0.18.4.tgz", + "integrity": "sha512-x+IakEXu+ammna2SMKkb1NRDXKxhKckOJIYanNHh1FtG2bvhu8xJplShvStmfO+BUv1n0KODSq89qGVYxFMbGQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@module-federation/runtime-tools": "0.17.1" + "@module-federation/runtime-tools": "0.18.4" } }, "node_modules/@module-federation/managers": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-0.17.1.tgz", - "integrity": "sha512-jMWD3w1j7n47EUNr44DXjvuEDQU4BjS7fanPN+1tTwUzuCYEnkaQKXDalv583VDKm4vP8s1TaJVIyjz+uTWiMQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-0.18.4.tgz", + "integrity": "sha512-wJ8wheGNq4vnaLHx17F8Y0L+T9nzO5ijqMxQ7q9Yohm7MGeC5DoSjjurv/afxL6Dg5rGky+kHsYGM4qRTMFXaA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "0.17.1", + "@module-federation/sdk": "0.18.4", "find-pkg": "2.0.0", "fs-extra": "9.1.0" } }, "node_modules/@module-federation/manifest": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-0.17.1.tgz", - "integrity": "sha512-0EM6hAB9E++MHDKBsFA8HmIUKHUjxVGZZTIaQNdmeCBNvL1KMp2eDuqrPaurlcrtrqpD7C7xwjmbIyYp5Us1xw==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-0.18.4.tgz", + "integrity": "sha512-1+sfldRpYmJX/SDqG3gWeeBbPb0H0eKyQcedf77TQGwFypVAOJwI39qV0yp3FdjutD7GdJ2TGPBHnGt7AbEvKA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/dts-plugin": "0.17.1", - "@module-federation/managers": "0.17.1", - "@module-federation/sdk": "0.17.1", + "@module-federation/dts-plugin": "0.18.4", + "@module-federation/managers": "0.18.4", + "@module-federation/sdk": "0.18.4", "chalk": "3.0.0", "find-pkg": "2.0.0" } @@ -7050,15 +7121,15 @@ } }, "node_modules/@module-federation/node": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@module-federation/node/-/node-2.7.14.tgz", - "integrity": "sha512-QUUObkCZO+l8Fh6gK4/I9D2AkWqU5X8UZ+5yB0d5iQA/FgjXVQv8o4JLSeSoyh3qy3Mzr952h46/PWzlFODAeQ==", + "version": "2.7.17", + "resolved": "https://registry.npmjs.org/@module-federation/node/-/node-2.7.17.tgz", + "integrity": "sha512-v4wHkotaGU+6GYvMie9PPcQTvq5dHGjuPKAJOtuH9mjKcg45iAIM3ffbq7VDdU4jw33Iqqqp8anfqwm/71KBxA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/enhanced": "0.18.3", - "@module-federation/runtime": "0.18.3", - "@module-federation/sdk": "0.18.3", + "@module-federation/enhanced": "0.19.1", + "@module-federation/runtime": "0.19.1", + "@module-federation/sdk": "0.19.1", "btoa": "1.2.1", "encoding": "^0.1.13", "node-fetch": "2.7.0" @@ -7081,27 +7152,27 @@ } }, "node_modules/@module-federation/node/node_modules/@module-federation/bridge-react-webpack-plugin": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.18.3.tgz", - "integrity": "sha512-6+zMzCnfMU6jSJ8fnT1yt5KkhdFwQpH7B3FkBCvdZVomwOJ4P9avAaQjjvplNo/ty7rqsrJfwX+SpE333KR2Rg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.19.1.tgz", + "integrity": "sha512-D+iFESodr/ohaXjmTOWBSFdjAz/WfN5Y5lIKB5Axh19FBUxvCy6Pj/We7C5JXc8CD9puqxXFOBNysJ7KNB89iw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "0.18.3", + "@module-federation/sdk": "0.19.1", "@types/semver": "7.5.8", "semver": "7.6.3" } }, "node_modules/@module-federation/node/node_modules/@module-federation/cli": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-0.18.3.tgz", - "integrity": "sha512-HdcFPXx4mTY+2eqLJknJYn9ke4Ua+QCiP5Ey0T4+m73HQe8SBoRUAXR4uQbCI8gIQaLzwFqfCa8SN4FYIFu0Tg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/cli/-/cli-0.19.1.tgz", + "integrity": "sha512-WHEnqGLLtK3jFdAhhW5WMqF5TO4FUfgp6+ujuZLrB1iOnjJXwg/+3F/qjWQtfUPIUCJSAC+58TSKXo8FjNcxPA==", "dev": true, "license": "MIT", "dependencies": { "@modern-js/node-bundle-require": "2.68.2", - "@module-federation/dts-plugin": "0.18.3", - "@module-federation/sdk": "0.18.3", + "@module-federation/dts-plugin": "0.19.1", + "@module-federation/sdk": "0.19.1", "chalk": "3.0.0", "commander": "11.1.0" }, @@ -7113,14 +7184,14 @@ } }, "node_modules/@module-federation/node/node_modules/@module-federation/data-prefetch": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-0.18.3.tgz", - "integrity": "sha512-8nwoYRE7y2SAVOmoCifF9nHUDG2PU+Eh6D/vef1tZIlKFP8jFEN5FA1BIyWvfSz/MzewnVK0VIDh92yrda8BYg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/data-prefetch/-/data-prefetch-0.19.1.tgz", + "integrity": "sha512-EXtEhYBw5XSHmtLp8Nu0sK2MMkdBtmvWQFfWmLDjPGGTeJHNE+fIHmef9hDbqXra8RpCyyZgwfTCUMZcwAGvzQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.18.3", - "@module-federation/sdk": "0.18.3", + "@module-federation/runtime": "0.19.1", + "@module-federation/sdk": "0.19.1", "fs-extra": "9.1.0" }, "peerDependencies": { @@ -7129,16 +7200,16 @@ } }, "node_modules/@module-federation/node/node_modules/@module-federation/dts-plugin": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-0.18.3.tgz", - "integrity": "sha512-nw7d8qdLl2All9oQfHabxKVJUeRiBMRtePEAcCZ2KD83sHp6dBVG+xMLTnQV3D/tU8ylbjvJ9SHyReM6trAmsQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/dts-plugin/-/dts-plugin-0.19.1.tgz", + "integrity": "sha512-/MV5gbEsiQiDwPmEq8WS24P/ibDtRwM7ejRKwZ+vWqv11jg75FlxHdzl71CMt5AatoPiUkrsPDQDO1EmKz/NXQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.18.3", - "@module-federation/managers": "0.18.3", - "@module-federation/sdk": "0.18.3", - "@module-federation/third-party-dts-extractor": "0.18.3", + "@module-federation/error-codes": "0.19.1", + "@module-federation/managers": "0.19.1", + "@module-federation/sdk": "0.19.1", + "@module-federation/third-party-dts-extractor": "0.19.1", "adm-zip": "^0.5.10", "ansi-colors": "^4.1.3", "axios": "^1.11.0", @@ -7163,23 +7234,23 @@ } }, "node_modules/@module-federation/node/node_modules/@module-federation/enhanced": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-0.18.3.tgz", - "integrity": "sha512-whjh2fw8E+R4C2QlHNoSw/ltYyF5Tu7UYG2dR7vIG+MuKuCUiJKmigv5s0zv6AaqNdO7ft9xLfVoWwrI8TJNNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "0.18.3", - "@module-federation/cli": "0.18.3", - "@module-federation/data-prefetch": "0.18.3", - "@module-federation/dts-plugin": "0.18.3", - "@module-federation/error-codes": "0.18.3", - "@module-federation/inject-external-runtime-core-plugin": "0.18.3", - "@module-federation/managers": "0.18.3", - "@module-federation/manifest": "0.18.3", - "@module-federation/rspack": "0.18.3", - "@module-federation/runtime-tools": "0.18.3", - "@module-federation/sdk": "0.18.3", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/enhanced/-/enhanced-0.19.1.tgz", + "integrity": "sha512-cSNbV5IFZRECpKEdIhIGNW9dNPjyDmSFlPIV0OG7aP4zAmUtz/oizpYtEE5r7hLAGxzWwBnj7zQIIxvmKgrSAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@module-federation/bridge-react-webpack-plugin": "0.19.1", + "@module-federation/cli": "0.19.1", + "@module-federation/data-prefetch": "0.19.1", + "@module-federation/dts-plugin": "0.19.1", + "@module-federation/error-codes": "0.19.1", + "@module-federation/inject-external-runtime-core-plugin": "0.19.1", + "@module-federation/managers": "0.19.1", + "@module-federation/manifest": "0.19.1", + "@module-federation/rspack": "0.19.1", + "@module-federation/runtime-tools": "0.19.1", + "@module-federation/sdk": "0.19.1", "btoa": "^1.2.1", "schema-utils": "^4.3.0", "upath": "2.0.1" @@ -7205,62 +7276,62 @@ } }, "node_modules/@module-federation/node/node_modules/@module-federation/error-codes": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.3.tgz", - "integrity": "sha512-ZSSOFvi5iwJdveRQrCIQJHv+clAXKR6APyf+yJq3oLm4EiV70OjVUC8JAG6o5oEwJT4L38U29HbziqZCBA55Yg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.19.1.tgz", + "integrity": "sha512-XtrOfaYPBD9UbdWb7O+gk295/5EFfC2/R6JmhbQmM2mt2axlrwUoy29LAEMSpyMkAD0NfRfQ3HaOsJQiUIy+Qg==", "dev": true, "license": "MIT" }, "node_modules/@module-federation/node/node_modules/@module-federation/inject-external-runtime-core-plugin": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-0.18.3.tgz", - "integrity": "sha512-FEohbuO79uefVUS5jSPlN69IxEcxBTcbFhVYvErbXnbk3gz2HB4OVaYJ9g/FrOhlh1mpEzjKRWoF/8MiaXc4+Q==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/inject-external-runtime-core-plugin/-/inject-external-runtime-core-plugin-0.19.1.tgz", + "integrity": "sha512-yOErRSKR60H4Zyk4nUqsc7u7eLaZ5KX3FXAyKxdGwIJ1B8jJJS+xRiQM8bwRansoF23rv7XWO62K5w/qONiTuQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@module-federation/runtime-tools": "0.18.3" + "@module-federation/runtime-tools": "0.19.1" } }, "node_modules/@module-federation/node/node_modules/@module-federation/managers": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-0.18.3.tgz", - "integrity": "sha512-2njxM9lSGySTYSdVkUGfjZ5kWPvDyLyYHn4haHBAxVBAiGCyTyIf8wL9SPJu1GrUPonC50GNQEDNlX/C/Xi4BA==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/managers/-/managers-0.19.1.tgz", + "integrity": "sha512-bZwiRqc0Cy76xSgKw8dFpVc0tpu6EG+paL0bAtHU5Kj9SBRGyCZ1JQY2W+S8z5tS/7M+gDNl9iIgQim+Kq6isg==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/sdk": "0.18.3", + "@module-federation/sdk": "0.19.1", "find-pkg": "2.0.0", "fs-extra": "9.1.0" } }, "node_modules/@module-federation/node/node_modules/@module-federation/manifest": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-0.18.3.tgz", - "integrity": "sha512-Z+wxfdMC/INrk1/3flWS+6Cel3SUqrS6JMAdaAzUy6SQ7q/TO804zjdAlGU6/bfH+xyADm5VN8kTOJAVgDgB4g==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/manifest/-/manifest-0.19.1.tgz", + "integrity": "sha512-6QruFQRpedVpHq2JpsYFMrFQvSbqe4QcGjk6zYWQCx+kcUvxYuKwfRzhyJt/Sorqz2rW92I2ckmlHKufCLOmTg==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/dts-plugin": "0.18.3", - "@module-federation/managers": "0.18.3", - "@module-federation/sdk": "0.18.3", + "@module-federation/dts-plugin": "0.19.1", + "@module-federation/managers": "0.19.1", + "@module-federation/sdk": "0.19.1", "chalk": "3.0.0", "find-pkg": "2.0.0" } }, "node_modules/@module-federation/node/node_modules/@module-federation/rspack": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-0.18.3.tgz", - "integrity": "sha512-nF6AzprO9vWJ6Xa8i/o00qI1WtO6Z+c7JiJnCM0Fn5HU1mLCsj2kMV2jbaUv2CSXj53kTXVu5aYqkDUNpTxX1w==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-0.19.1.tgz", + "integrity": "sha512-H/bmdHhK91JIar9juyxdGQkjk5fLwbfugoBwFzxCx0PybwKObs+ZHW7yZ1ZoVBsRkYmvV79R2Squgtn/aGReCA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "0.18.3", - "@module-federation/dts-plugin": "0.18.3", - "@module-federation/inject-external-runtime-core-plugin": "0.18.3", - "@module-federation/managers": "0.18.3", - "@module-federation/manifest": "0.18.3", - "@module-federation/runtime-tools": "0.18.3", - "@module-federation/sdk": "0.18.3", + "@module-federation/bridge-react-webpack-plugin": "0.19.1", + "@module-federation/dts-plugin": "0.19.1", + "@module-federation/inject-external-runtime-core-plugin": "0.19.1", + "@module-federation/managers": "0.19.1", + "@module-federation/manifest": "0.19.1", + "@module-federation/runtime-tools": "0.19.1", + "@module-federation/sdk": "0.19.1", "btoa": "1.2.1" }, "peerDependencies": { @@ -7278,50 +7349,50 @@ } }, "node_modules/@module-federation/node/node_modules/@module-federation/runtime": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.18.3.tgz", - "integrity": "sha512-zuPvCs51CFu3efSl7hl8MIEhc1nwYQyJlENWM7qaeWK85yfftLIvYA7iy4+y9CZORTmtEg6RwwlsUmhv62YlLA==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.19.1.tgz", + "integrity": "sha512-eSXexdGGPpZnhiWCVfRlVLNWj7gHKp65beC4b8wddTvMBIrxnsdl9ae1ebwcIpbe9gOGDbaXBFtc3r5MH6l6Jg==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.18.3", - "@module-federation/runtime-core": "0.18.3", - "@module-federation/sdk": "0.18.3" + "@module-federation/error-codes": "0.19.1", + "@module-federation/runtime-core": "0.19.1", + "@module-federation/sdk": "0.19.1" } }, "node_modules/@module-federation/node/node_modules/@module-federation/runtime-core": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.18.3.tgz", - "integrity": "sha512-Xk5w+Z+r8f19p/4xLMJTxUxOF0aE/0VEV2yV77dAb4CZ2zPCs2xPqa9Su43+LYlVAkIvcpOgxFCMLQEaxajLPg==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.19.1.tgz", + "integrity": "sha512-NLSlPnIzO2RoF6W1xq/x3t1j7jcglMaPSv2EIVOFvs5/ah7BeJmRhtH494tmjIwV0q+j1QEGGhijHxXZLK1HMQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.18.3", - "@module-federation/sdk": "0.18.3" + "@module-federation/error-codes": "0.19.1", + "@module-federation/sdk": "0.19.1" } }, "node_modules/@module-federation/node/node_modules/@module-federation/runtime-tools": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.18.3.tgz", - "integrity": "sha512-G00xsEx4CzhvhutJi+7yvmnHepOeGd1o+BBqRzAjZS4iwp7zS5h3CCxxEGeQgJdP9BA3/m0HATPSwepL7Bwd0Q==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.19.1.tgz", + "integrity": "sha512-WjLZcuP7U5pSQobMEvaMH9pFrvfV3Kk2dfOUNza0tpj6vYtAxk6FU6TQ8WDjqG7yuglyAzq0bVEKVrdIB4Vd9Q==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.18.3", - "@module-federation/webpack-bundler-runtime": "0.18.3" + "@module-federation/runtime": "0.19.1", + "@module-federation/webpack-bundler-runtime": "0.19.1" } }, "node_modules/@module-federation/node/node_modules/@module-federation/sdk": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.18.3.tgz", - "integrity": "sha512-tlBgF5pKXoiZ5hGRgafOpsktt0iafdjoH2O85ywPqvDGVK0DzfP8hs4qdUBJlKulP5PZoBtgTe7UiqyTbKJ7YQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.19.1.tgz", + "integrity": "sha512-0JTkYaa4qNLtYGc6ZQQ50BinWh4bAOgT8t17jB/6BqcWiza6fKz647wN0AK+VX3rtl6kvGAjhtqqZtRBc8aeiw==", "dev": true, "license": "MIT" }, "node_modules/@module-federation/node/node_modules/@module-federation/third-party-dts-extractor": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-0.18.3.tgz", - "integrity": "sha512-hxGrTrU1C71dW2cFANoUGzYO5ovGXL5wDTu5nwwNQ81ao9DfhjNkYnCfkvHDHh5648N4wUhnuLjerUc8F8ZJxA==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-0.19.1.tgz", + "integrity": "sha512-XBuujPLWgJjljm/QfShtI0pErqRL28iiJ7AsUpFsNbSRJiBlcXTDPKqFWiZXmp/lGmJigLV2wDgyK0cyKqoWcg==", "dev": true, "license": "MIT", "dependencies": { @@ -7331,14 +7402,14 @@ } }, "node_modules/@module-federation/node/node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.18.3", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.18.3.tgz", - "integrity": "sha512-Ul9sdfFNHc5/qUDerD1IKivaAdGo0BjG5hBX4hzrD75c+9P9kw9seBQBBx3kMj+W56ALabN65p243GI67CQWtw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.19.1.tgz", + "integrity": "sha512-pr9kgwvBoe8tvXELDCqu8ihvLJYwS+cfwJmvk99MTbespzK0nuOepkeRCy2gOpeATDNiWdy/2DJcw34qeAmhJw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.18.3", - "@module-federation/sdk": "0.18.3" + "@module-federation/runtime": "0.19.1", + "@module-federation/sdk": "0.19.1" } }, "node_modules/@module-federation/node/node_modules/chalk": { @@ -7355,108 +7426,20 @@ "node": ">=8" } }, - "node_modules/@module-federation/node/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@module-federation/node/node_modules/koa": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/koa/-/koa-3.0.1.tgz", - "integrity": "sha512-oDxVkRwPOHhGlxKIDiDB2h+/l05QPtefD7nSqRgDfZt8P+QVYFWjfeK8jANf5O2YXjk8egd7KntvXKYx82wOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "accepts": "^1.3.8", - "content-disposition": "~0.5.4", - "content-type": "^1.0.5", - "cookies": "~0.9.1", - "delegates": "^1.0.0", - "destroy": "^1.2.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.5.0", - "http-errors": "^2.0.0", - "koa-compose": "^4.1.0", - "mime-types": "^3.0.1", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@module-federation/node/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@module-federation/node/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@module-federation/node/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@module-federation/node/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/@module-federation/rspack": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-0.17.1.tgz", - "integrity": "sha512-TMLaMcQjRTjVPzOi5USFDkf3Js3vHIlQm1wgzbe4Ok70vW9gHUQ+7LHFDWTt5byKoHeZJbzEr4c5zJCo6WBTKA==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/rspack/-/rspack-0.18.4.tgz", + "integrity": "sha512-gnvXKtk/w0ML15JHueWej5/8Lkoho7EoYUxvO77nBCnGOlXNqVYqLZ3REy2SS/8SQ4vQK156eSiyUkth2OYQqw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/bridge-react-webpack-plugin": "0.17.1", - "@module-federation/dts-plugin": "0.17.1", - "@module-federation/inject-external-runtime-core-plugin": "0.17.1", - "@module-federation/managers": "0.17.1", - "@module-federation/manifest": "0.17.1", - "@module-federation/runtime-tools": "0.17.1", - "@module-federation/sdk": "0.17.1", + "@module-federation/bridge-react-webpack-plugin": "0.18.4", + "@module-federation/dts-plugin": "0.18.4", + "@module-federation/inject-external-runtime-core-plugin": "0.18.4", + "@module-federation/managers": "0.18.4", + "@module-federation/manifest": "0.18.4", + "@module-federation/runtime-tools": "0.18.4", + "@module-federation/sdk": "0.18.4", "btoa": "1.2.1" }, "peerDependencies": { @@ -7474,50 +7457,50 @@ } }, "node_modules/@module-federation/runtime": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.17.1.tgz", - "integrity": "sha512-vKEN32MvUbpeuB/s6UXfkHDZ9N5jFyDDJnj83UTJ8n4N1jHIJu9VZ6Yi4/Ac8cfdvU8UIK9bIbfVXWbUYZUDsw==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.18.4.tgz", + "integrity": "sha512-2et6p7pjGRHzpmrW425jt/BiAU7QHgkZtbQB7pj01eQ8qx6SloFEBk9ODnV8/ztSm9H2T3d8GxXA6/9xVOslmQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.17.1", - "@module-federation/runtime-core": "0.17.1", - "@module-federation/sdk": "0.17.1" + "@module-federation/error-codes": "0.18.4", + "@module-federation/runtime-core": "0.18.4", + "@module-federation/sdk": "0.18.4" } }, "node_modules/@module-federation/runtime-core": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.17.1.tgz", - "integrity": "sha512-LCtIFuKgWPQ3E+13OyrVpuTPOWBMI/Ggwsq1Q874YeT8Px28b8tJRCj09DjyRFyhpSPyV/uG80T6iXPAUoLIfQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.18.4.tgz", + "integrity": "sha512-LGGlFXlNeTbIGBFDiOvg0zz4jBWCGPqQatXdKx7mylXhDij7YmwbuW19oenX+P1fGhmoBUBM5WndmR87U66qWA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.17.1", - "@module-federation/sdk": "0.17.1" + "@module-federation/error-codes": "0.18.4", + "@module-federation/sdk": "0.18.4" } }, "node_modules/@module-federation/runtime-tools": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.17.1.tgz", - "integrity": "sha512-4kr6zTFFwGywJx6whBtxsc84V+COAuuBpEdEbPZN//YLXhNB0iz2IGsy9r9wDl+06h84bD+3dQ05l9euRLgXzQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.18.4.tgz", + "integrity": "sha512-wSGTdx77R8BQX+q6nAcUuHPydYYm0F97gAEP9RTW1UlzXnM/0AFysDHujvtRQf5vyXkhj//HdcH6LIJJCImy2g==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.17.1", - "@module-federation/webpack-bundler-runtime": "0.17.1" + "@module-federation/runtime": "0.18.4", + "@module-federation/webpack-bundler-runtime": "0.18.4" } }, "node_modules/@module-federation/sdk": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.17.1.tgz", - "integrity": "sha512-nlUcN6UTEi+3HWF+k8wPy7gH0yUOmCT+xNatihkIVR9REAnr7BUvHFGlPJmx7WEbLPL46+zJUbtQHvLzXwFhng==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.18.4.tgz", + "integrity": "sha512-dErzOlX+E3HS2Sg1m12Hi9nCnfvQPuIvlq9N47KxrbT2TIU3KKYc9q/Ua+QWqxfTyMVFpbNDwFMJ1R/w/gYf4A==", "dev": true, "license": "MIT" }, "node_modules/@module-federation/third-party-dts-extractor": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-0.17.1.tgz", - "integrity": "sha512-hGvy1Tqathc34G4Tx7WJgpK0203oDFA/qSPIhPpsWg27em3fCWozLczVsq+lOxxCM6llDRgC1kt/EpWeqEK/ng==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/third-party-dts-extractor/-/third-party-dts-extractor-0.18.4.tgz", + "integrity": "sha512-PpiC0jxOegNR/xjhNOkjSYnUqMNJAy1kWsRd10to3Y64ZvGRf7/HF+x3aLIX8MbN7Ioy9F7Gd5oax6rtm+XmNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7527,14 +7510,14 @@ } }, "node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.17.1.tgz", - "integrity": "sha512-Swspdgf4PzcbvS9SNKFlBzfq8h/Qxwqjq/xRSqw1pqAZWondZQzwTTqPXhgrg0bFlz7qWjBS/6a8KuH/gRvGaQ==", + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.18.4.tgz", + "integrity": "sha512-nPHp2wRS4/yfrGRQchZ0cyvdUZk+XgUmD0qWQl95xmeIeXUb90s3JrWFHSmS6Dt1gwMgJOeNpzzZDcBSy2P1VQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.17.1", - "@module-federation/sdk": "0.17.1" + "@module-federation/runtime": "0.18.4", + "@module-federation/sdk": "0.18.4" } }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { @@ -8029,6 +8012,17 @@ } } }, + "node_modules/@nestjs/axios": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", + "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "axios": "^1.3.1", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs-modules/ioredis/node_modules/@nestjs/typeorm": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", @@ -8053,17 +8047,6 @@ "optional": true, "peer": true }, - "node_modules/@nestjs/axios": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", - "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", - "license": "MIT", - "peerDependencies": { - "@nestjs/common": "^10.0.0 || ^11.0.0", - "axios": "^1.3.1", - "rxjs": "^7.0.0" - } - }, "node_modules/@nestjs/bull": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/@nestjs/bull/-/bull-11.0.3.tgz", @@ -8297,23 +8280,6 @@ "node": ">=6.6.0" } }, - "node_modules/@nestjs/platform-express/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@nestjs/platform-express/node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -8915,6 +8881,34 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@npmcli/node-gyp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", @@ -9131,20 +9125,20 @@ } }, "node_modules/@nx/angular": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-21.3.1.tgz", - "integrity": "sha512-v2VogTfmBfR/OLEm7lwWsW0e33al8W3xOyw4hlVmPZg7lgruvaqabKABepukxOizzwd9rDDirQkFAL+zWX9wlA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/angular/-/angular-21.5.2.tgz", + "integrity": "sha512-a3HAMGB+3ZFMb41m4KVPnTe+5WeUOE3XYkUu/j3dj2Tue+etX6c/NPqa03P3pAi/pYim2LQU4GpW3ACAYjlj+Q==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", - "@nx/eslint": "21.3.1", - "@nx/js": "21.3.1", - "@nx/module-federation": "21.3.1", - "@nx/rspack": "21.3.1", - "@nx/web": "21.3.1", - "@nx/webpack": "21.3.1", - "@nx/workspace": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/eslint": "21.5.2", + "@nx/js": "21.5.2", + "@nx/module-federation": "21.5.2", + "@nx/rspack": "21.5.2", + "@nx/web": "21.5.2", + "@nx/webpack": "21.5.2", + "@nx/workspace": "21.5.2", "@phenomnomnominal/tsquery": "~5.0.1", "@typescript-eslint/type-utils": "^8.0.0", "enquirer": "~2.3.6", @@ -9177,9 +9171,9 @@ } }, "node_modules/@nx/angular/node_modules/@jest/schemas": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", - "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { @@ -9190,9 +9184,9 @@ } }, "node_modules/@nx/angular/node_modules/@nx/devkit": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.3.1.tgz", - "integrity": "sha512-0FA6uVIJ5tOrUz6kJEeyX6KKPmXBs9kWQbf08yXogEzMimz9cVpoDS8MGmK14e13UwnY68vqXgknHAL+ATs3Zg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.5.2.tgz", + "integrity": "sha512-coNOyRBHeB6XHbEYeJ8bd/vhPqGx1+KhXojEsQv9vN9sgONqgWEUk0p/XnIplIvI0E7M/hm8zheydhZNYC9xSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9206,21 +9200,21 @@ "yargs-parser": "21.1.1" }, "peerDependencies": { - "nx": "21.3.1" + "nx": ">= 20 <= 22" } }, "node_modules/@nx/angular/node_modules/@nx/eslint": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-21.3.1.tgz", - "integrity": "sha512-Vjb0tk6nd0khljt57RZLTZhH7sUGkefZhkVdM5AVTDYVz4S1U2xuowwBnbIf0VbdTZ9xmxYIoWsPewEmSA5kXQ==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/eslint/-/eslint-21.5.2.tgz", + "integrity": "sha512-IpSgLc5PRWCTiiH2Kue9d/RS8Od6loHyfNbeUrSaJlN2Jq+WoxsGFtjsBHxJyQADu7MmlGZn8XutsbDQ8dbVKw==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", - "@nx/js": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/js": "21.5.2", "semver": "^7.5.3", "tslib": "^2.3.0", - "typescript": "~5.8.2" + "typescript": "~5.9.2" }, "peerDependencies": { "@zkochan/js-yaml": "0.0.7", @@ -9232,10 +9226,24 @@ } } }, + "node_modules/@nx/angular/node_modules/@nx/eslint/node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@nx/angular/node_modules/@nx/js": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.3.1.tgz", - "integrity": "sha512-zc+3t3NOBWdnqPL94gsYspYhkiKYd3fflvDNdiOpf3XKhXZCE89YsbcZGoxDnW0A0bKAk4Z7wwh9txnG4A2jZA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.5.2.tgz", + "integrity": "sha512-zJDzdN0xY5dTw5fPR+IRVpKnRf/hl2WjyBGM42Jkda6vV9aR/sRkz8evXza781FWZ3o2P/wTZhQRMiO5O1fy4w==", "dev": true, "license": "MIT", "dependencies": { @@ -9246,8 +9254,8 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "21.3.1", - "@nx/workspace": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/workspace": "21.5.2", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", @@ -9279,9 +9287,9 @@ } }, "node_modules/@nx/angular/node_modules/@nx/nx-darwin-arm64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.3.1.tgz", - "integrity": "sha512-DND5/CRN1rP7qMt4xkDjklzf3OoA3JcweN+47xZCfiQlu/VobvnS04OC6tLZc+Nymi73whk4lserpUG9biQjCA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.5.2.tgz", + "integrity": "sha512-PrfZbV2blRHoWLor+xDVwPY/dk46kbsmuTXCZRYlNAwko521Y9dCAJT0UOROic3zoUasQ+TwqsQextIcKCotIA==", "cpu": [ "arm64" ], @@ -9293,9 +9301,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-darwin-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.3.1.tgz", - "integrity": "sha512-iFR/abYakCwrFFIfb6gRXN6/gytle/8jj2jwEol0EFrkBIrBi/YrSyuRpsbnxuDB7MhuZ9zwvxlkE6mEJt9UoQ==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.5.2.tgz", + "integrity": "sha512-YaLY2Cqbjrl+pDddHV7GFtokn81GLvoqg+i9k0Eiid8B0dDLBZpJ3VQKr4RkTzxBX38UuHbJUwrZc8L9z8vqEw==", "cpu": [ "x64" ], @@ -9307,9 +9315,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-freebsd-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.3.1.tgz", - "integrity": "sha512-e/cx0cR8sLBX3b2JRLqwXj8z4ENhgDwJ5CF7hbcNRkMncKz1J2MZSsqHQHKUfls+HT4Mmmzwyf86laj879cs7Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.5.2.tgz", + "integrity": "sha512-2z/Wd42/KHFyT0zRVxWHlaRBQz12Fd1A0FCGJzuWI8G2meh9tYt4MN96gQ4q/rLQ0fmfFEEECq6pmOfCi8t9Mg==", "cpu": [ "x64" ], @@ -9321,9 +9329,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.3.1.tgz", - "integrity": "sha512-JvDfLVZhxzKfetcA1r5Ak+re5Qfks6JdgQD165wMysgAyZDdeM1GFn78xo5CqRPShlE8f/nhF4aX405OL6HYPw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.5.2.tgz", + "integrity": "sha512-lY2O1py8x+l39XAFFuplKlzouPC9K/gERYEB/b5jHGf7PGfNj0BX2MDmUztgTty6kKUnkRele39aSoQqWok0gA==", "cpu": [ "arm" ], @@ -9335,9 +9343,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-linux-arm64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.3.1.tgz", - "integrity": "sha512-J+LkCHzFCgZw4ZMgIuahprjaabWTDmsqJdsYLPFm/pw7TR6AyidXzUEZPfEgBK5WTO1PQr1LJp+Ps8twpf+iBg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.5.2.tgz", + "integrity": "sha512-gcpkXXPpWaf8wB0FZUaKmk8Jdv+QMHLiOcQuuXYi1X0vbgotVTl/y+dccwG1EZml6V5JIRGtg2YDM61a7Olp1Q==", "cpu": [ "arm64" ], @@ -9349,9 +9357,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-linux-arm64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.3.1.tgz", - "integrity": "sha512-VCiwPf4pj6WZWmPl30UpcKyp8DWb7Axs0pvU0/dsJz6Ye7bhKnsEZ/Ehp4laVZkck+MVEMFMHavikUdgNzWx3g==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.5.2.tgz", + "integrity": "sha512-oCSUwT0hORgFJWIGjwl6x4/2mVusw+3YAcSrvDePAXadjPSEMLZlJEE+4HExzqLFFBTxc+ucvyOIk08P4BtNJg==", "cpu": [ "arm64" ], @@ -9363,9 +9371,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-linux-x64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.3.1.tgz", - "integrity": "sha512-Erxxqir8zZDgfrTgOOeaByn22e8vbOTxWNif5i0brH2tQpdr6+2f3v1qNrRlP9CWjqwypPDmkU781U38u+qHHg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.5.2.tgz", + "integrity": "sha512-rgJTQk0iaidxEIMOuRQJS36Sk4+qcpJP0uwymvgyoTpZyBdkX38NHH3D+E6sudPSFWsiVxJpkCzYE4ScSKF8Ew==", "cpu": [ "x64" ], @@ -9377,9 +9385,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-linux-x64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.3.1.tgz", - "integrity": "sha512-dG5djRnC3zzOjEzOAzVX8u1sZSn0TSmvVUKKH+WorxI8QKpxHVHbzpvvyLXpiAbtSk0reIPC1c9iRw+MR0GAvw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.5.2.tgz", + "integrity": "sha512-KeS36526VruYO9HzhFGqhE5tbps7e94DV0b4j5wfPr7V51EfPzvjAiMWllsQDARv67wdbQ80c0Wg516XTlekgA==", "cpu": [ "x64" ], @@ -9391,9 +9399,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-win32-arm64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.3.1.tgz", - "integrity": "sha512-mkV6HERTtP2uY6aq0AhxbkL6KSsvomobPOypdEdrnfUsc2Rvd+U/pWl/flZHFkk8V6aXzEG56lWCZqXVyGUD1Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.5.2.tgz", + "integrity": "sha512-jlRTycYKOiSqc0fcqvabOH/HZX9BOG0S8EGsLmqEr2OkJLZc25ByD1n22P486R2n+m8GQwL6pX+L1LPpOPmz0A==", "cpu": [ "arm64" ], @@ -9405,9 +9413,9 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/nx-win32-x64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.3.1.tgz", - "integrity": "sha512-9cQZDiLT9bD1ixZ+WwNHVPRrxuszGHc30xzLzVgQ2l/IwOHJX6oRxMZa1IfgUYv846K0TSKWis+S41tcxUy80Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.5.2.tgz", + "integrity": "sha512-Ur8GPdz52kLS5uE9IQf0wBtGyvQm4Y3M1ZDjRkR+oGf26aVGNTK6C0+kMJPuggR4Z6lurmHYA34ViGi2hHPPpA==", "cpu": [ "x64" ], @@ -9419,53 +9427,127 @@ ] }, "node_modules/@nx/angular/node_modules/@nx/workspace": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.3.1.tgz", - "integrity": "sha512-MiS0x/Wl4vv+4oFWvsZLFsRR9E3tDh002ZeGjvGJbiZw8eUAMfX1mYhU7URxHSr8yoW1qCAKEvgAjGTLRz9Kkw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.5.2.tgz", + "integrity": "sha512-7IDa5xqVwGgZXrFGqyMzZTOq0Okxc0KH6M0mLfHJy1393iEUJjLByfkQ0nDyjsRZsLqo11WMOldapBDwy6MlaQ==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", + "@nx/devkit": "21.5.2", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "21.3.1", + "nx": "21.5.2", "picomatch": "4.0.2", + "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" } }, - "node_modules/@nx/angular/node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@nx/angular/node_modules/@typescript-eslint/scope-manager": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", - "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", + "node_modules/@nx/angular/node_modules/@nx/workspace/node_modules/nx": { + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-21.5.2.tgz", + "integrity": "sha512-hvq3W6mWsNuXzO1VWXpVcbGuF3e4cx0PyPavy8RgZUinbnh3Gk+f+2DGXyjKEyAG3Ql0Nl3V4RJERZzXEVl7EA==", "dev": true, + "hasInstallScript": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.26.0", - "@typescript-eslint/visitor-keys": "8.26.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@nx/angular/node_modules/@typescript-eslint/type-utils": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", - "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", - "dev": true, - "license": "MIT", + "@napi-rs/wasm-runtime": "0.2.4", + "@yarnpkg/lockfile": "^1.1.0", + "@yarnpkg/parsers": "3.0.2", + "@zkochan/js-yaml": "0.0.7", + "axios": "^1.8.3", + "chalk": "^4.1.0", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^8.0.1", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "enquirer": "~2.3.6", + "figures": "3.2.0", + "flat": "^5.0.2", + "front-matter": "^4.0.2", + "ignore": "^5.0.4", + "jest-diff": "^30.0.2", + "jsonc-parser": "3.2.0", + "lines-and-columns": "2.0.3", + "minimatch": "9.0.3", + "node-machine-id": "1.1.12", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "ora": "5.3.0", + "resolve.exports": "2.0.3", + "semver": "^7.5.3", + "string-width": "^4.2.3", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tree-kill": "^1.2.2", + "tsconfig-paths": "^4.1.2", + "tslib": "^2.3.0", + "yaml": "^2.6.0", + "yargs": "^17.6.2", + "yargs-parser": "21.1.1" + }, + "bin": { + "nx": "bin/nx.js", + "nx-cloud": "bin/nx-cloud.js" + }, + "optionalDependencies": { + "@nx/nx-darwin-arm64": "21.5.2", + "@nx/nx-darwin-x64": "21.5.2", + "@nx/nx-freebsd-x64": "21.5.2", + "@nx/nx-linux-arm-gnueabihf": "21.5.2", + "@nx/nx-linux-arm64-gnu": "21.5.2", + "@nx/nx-linux-arm64-musl": "21.5.2", + "@nx/nx-linux-x64-gnu": "21.5.2", + "@nx/nx-linux-x64-musl": "21.5.2", + "@nx/nx-win32-arm64-msvc": "21.5.2", + "@nx/nx-win32-x64-msvc": "21.5.2" + }, + "peerDependencies": { + "@swc-node/register": "^1.8.0", + "@swc/core": "^1.3.85" + }, + "peerDependenciesMeta": { + "@swc-node/register": { + "optional": true + }, + "@swc/core": { + "optional": true + } + } + }, + "node_modules/@nx/angular/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nx/angular/node_modules/@typescript-eslint/scope-manager": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", + "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@nx/angular/node_modules/@typescript-eslint/type-utils": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", + "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", + "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "8.26.0", "@typescript-eslint/utils": "8.26.0", @@ -9649,16 +9731,16 @@ } }, "node_modules/@nx/angular/node_modules/jest-diff": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -9671,79 +9753,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@nx/angular/node_modules/nx": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-21.3.1.tgz", - "integrity": "sha512-lOMDktM4CUcVa/yUmiAXGNxbNo6SC0T8/alRml1sgaOG1QHUpH6XyA1/nR4M3DNjlmON4wD06pZQUDKFb8kd8w==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@napi-rs/wasm-runtime": "0.2.4", - "@yarnpkg/lockfile": "^1.1.0", - "@yarnpkg/parsers": "3.0.2", - "@zkochan/js-yaml": "0.0.7", - "axios": "^1.8.3", - "chalk": "^4.1.0", - "cli-cursor": "3.1.0", - "cli-spinners": "2.6.1", - "cliui": "^8.0.1", - "dotenv": "~16.4.5", - "dotenv-expand": "~11.0.6", - "enquirer": "~2.3.6", - "figures": "3.2.0", - "flat": "^5.0.2", - "front-matter": "^4.0.2", - "ignore": "^5.0.4", - "jest-diff": "^30.0.2", - "jsonc-parser": "3.2.0", - "lines-and-columns": "2.0.3", - "minimatch": "9.0.3", - "node-machine-id": "1.1.12", - "npm-run-path": "^4.0.1", - "open": "^8.4.0", - "ora": "5.3.0", - "resolve.exports": "2.0.3", - "semver": "^7.5.3", - "string-width": "^4.2.3", - "tar-stream": "~2.2.0", - "tmp": "~0.2.1", - "tree-kill": "^1.2.2", - "tsconfig-paths": "^4.1.2", - "tslib": "^2.3.0", - "yaml": "^2.6.0", - "yargs": "^17.6.2", - "yargs-parser": "21.1.1" - }, - "bin": { - "nx": "bin/nx.js", - "nx-cloud": "bin/nx-cloud.js" - }, - "optionalDependencies": { - "@nx/nx-darwin-arm64": "21.3.1", - "@nx/nx-darwin-x64": "21.3.1", - "@nx/nx-freebsd-x64": "21.3.1", - "@nx/nx-linux-arm-gnueabihf": "21.3.1", - "@nx/nx-linux-arm64-gnu": "21.3.1", - "@nx/nx-linux-arm64-musl": "21.3.1", - "@nx/nx-linux-x64-gnu": "21.3.1", - "@nx/nx-linux-x64-musl": "21.3.1", - "@nx/nx-win32-arm64-msvc": "21.3.1", - "@nx/nx-win32-x64-msvc": "21.3.1" - }, - "peerDependencies": { - "@swc-node/register": "^1.8.0", - "@swc/core": "^1.3.85" - }, - "peerDependenciesMeta": { - "@swc-node/register": { - "optional": true - }, - "@swc/core": { - "optional": true - } - } - }, "node_modules/@nx/angular/node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -9786,13 +9795,13 @@ } }, "node_modules/@nx/angular/node_modules/pretty-format": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.1", + "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" }, @@ -10047,30 +10056,30 @@ } }, "node_modules/@nx/module-federation": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-21.3.1.tgz", - "integrity": "sha512-LSQ0iOIYESjIWH0ShykfV5dnyOVlBHlmUnC6xpLVNjghbs1qp5t/HXto3SRhKMomwkj/XN5ZQBsvY9uD9/yo4w==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/module-federation/-/module-federation-21.5.2.tgz", + "integrity": "sha512-x04ARDiZaUl/3rH2RSRQlykNZ3LD3/scTdljRunCWRZFWvaXe7IX75Tv+fqRCuOYPrTP6ArxgcRwupgVK/Y2Dw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/enhanced": "^0.17.0", - "@module-federation/node": "^2.7.9", - "@module-federation/sdk": "^0.17.0", - "@nx/devkit": "21.3.1", - "@nx/js": "21.3.1", - "@nx/web": "21.3.1", + "@module-federation/enhanced": "^0.18.0", + "@module-federation/node": "^2.7.11", + "@module-federation/sdk": "^0.18.0", + "@nx/devkit": "21.5.2", + "@nx/js": "21.5.2", + "@nx/web": "21.5.2", "@rspack/core": "^1.3.8", "express": "^4.21.2", - "http-proxy-middleware": "^3.0.3", + "http-proxy-middleware": "^3.0.5", "picocolors": "^1.1.0", "tslib": "^2.3.0", - "webpack": "^5.88.0" + "webpack": "^5.101.3" } }, "node_modules/@nx/module-federation/node_modules/@jest/schemas": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", - "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { @@ -10081,9 +10090,9 @@ } }, "node_modules/@nx/module-federation/node_modules/@nx/devkit": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.3.1.tgz", - "integrity": "sha512-0FA6uVIJ5tOrUz6kJEeyX6KKPmXBs9kWQbf08yXogEzMimz9cVpoDS8MGmK14e13UwnY68vqXgknHAL+ATs3Zg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.5.2.tgz", + "integrity": "sha512-coNOyRBHeB6XHbEYeJ8bd/vhPqGx1+KhXojEsQv9vN9sgONqgWEUk0p/XnIplIvI0E7M/hm8zheydhZNYC9xSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10097,13 +10106,13 @@ "yargs-parser": "21.1.1" }, "peerDependencies": { - "nx": "21.3.1" + "nx": ">= 20 <= 22" } }, "node_modules/@nx/module-federation/node_modules/@nx/js": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.3.1.tgz", - "integrity": "sha512-zc+3t3NOBWdnqPL94gsYspYhkiKYd3fflvDNdiOpf3XKhXZCE89YsbcZGoxDnW0A0bKAk4Z7wwh9txnG4A2jZA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.5.2.tgz", + "integrity": "sha512-zJDzdN0xY5dTw5fPR+IRVpKnRf/hl2WjyBGM42Jkda6vV9aR/sRkz8evXza781FWZ3o2P/wTZhQRMiO5O1fy4w==", "dev": true, "license": "MIT", "dependencies": { @@ -10114,8 +10123,8 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "21.3.1", - "@nx/workspace": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/workspace": "21.5.2", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", @@ -10147,9 +10156,9 @@ } }, "node_modules/@nx/module-federation/node_modules/@nx/nx-darwin-arm64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.3.1.tgz", - "integrity": "sha512-DND5/CRN1rP7qMt4xkDjklzf3OoA3JcweN+47xZCfiQlu/VobvnS04OC6tLZc+Nymi73whk4lserpUG9biQjCA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.5.2.tgz", + "integrity": "sha512-PrfZbV2blRHoWLor+xDVwPY/dk46kbsmuTXCZRYlNAwko521Y9dCAJT0UOROic3zoUasQ+TwqsQextIcKCotIA==", "cpu": [ "arm64" ], @@ -10161,9 +10170,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-darwin-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.3.1.tgz", - "integrity": "sha512-iFR/abYakCwrFFIfb6gRXN6/gytle/8jj2jwEol0EFrkBIrBi/YrSyuRpsbnxuDB7MhuZ9zwvxlkE6mEJt9UoQ==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.5.2.tgz", + "integrity": "sha512-YaLY2Cqbjrl+pDddHV7GFtokn81GLvoqg+i9k0Eiid8B0dDLBZpJ3VQKr4RkTzxBX38UuHbJUwrZc8L9z8vqEw==", "cpu": [ "x64" ], @@ -10175,9 +10184,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-freebsd-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.3.1.tgz", - "integrity": "sha512-e/cx0cR8sLBX3b2JRLqwXj8z4ENhgDwJ5CF7hbcNRkMncKz1J2MZSsqHQHKUfls+HT4Mmmzwyf86laj879cs7Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.5.2.tgz", + "integrity": "sha512-2z/Wd42/KHFyT0zRVxWHlaRBQz12Fd1A0FCGJzuWI8G2meh9tYt4MN96gQ4q/rLQ0fmfFEEECq6pmOfCi8t9Mg==", "cpu": [ "x64" ], @@ -10189,9 +10198,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.3.1.tgz", - "integrity": "sha512-JvDfLVZhxzKfetcA1r5Ak+re5Qfks6JdgQD165wMysgAyZDdeM1GFn78xo5CqRPShlE8f/nhF4aX405OL6HYPw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.5.2.tgz", + "integrity": "sha512-lY2O1py8x+l39XAFFuplKlzouPC9K/gERYEB/b5jHGf7PGfNj0BX2MDmUztgTty6kKUnkRele39aSoQqWok0gA==", "cpu": [ "arm" ], @@ -10203,9 +10212,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-linux-arm64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.3.1.tgz", - "integrity": "sha512-J+LkCHzFCgZw4ZMgIuahprjaabWTDmsqJdsYLPFm/pw7TR6AyidXzUEZPfEgBK5WTO1PQr1LJp+Ps8twpf+iBg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.5.2.tgz", + "integrity": "sha512-gcpkXXPpWaf8wB0FZUaKmk8Jdv+QMHLiOcQuuXYi1X0vbgotVTl/y+dccwG1EZml6V5JIRGtg2YDM61a7Olp1Q==", "cpu": [ "arm64" ], @@ -10217,9 +10226,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-linux-arm64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.3.1.tgz", - "integrity": "sha512-VCiwPf4pj6WZWmPl30UpcKyp8DWb7Axs0pvU0/dsJz6Ye7bhKnsEZ/Ehp4laVZkck+MVEMFMHavikUdgNzWx3g==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.5.2.tgz", + "integrity": "sha512-oCSUwT0hORgFJWIGjwl6x4/2mVusw+3YAcSrvDePAXadjPSEMLZlJEE+4HExzqLFFBTxc+ucvyOIk08P4BtNJg==", "cpu": [ "arm64" ], @@ -10231,9 +10240,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-linux-x64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.3.1.tgz", - "integrity": "sha512-Erxxqir8zZDgfrTgOOeaByn22e8vbOTxWNif5i0brH2tQpdr6+2f3v1qNrRlP9CWjqwypPDmkU781U38u+qHHg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.5.2.tgz", + "integrity": "sha512-rgJTQk0iaidxEIMOuRQJS36Sk4+qcpJP0uwymvgyoTpZyBdkX38NHH3D+E6sudPSFWsiVxJpkCzYE4ScSKF8Ew==", "cpu": [ "x64" ], @@ -10245,9 +10254,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-linux-x64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.3.1.tgz", - "integrity": "sha512-dG5djRnC3zzOjEzOAzVX8u1sZSn0TSmvVUKKH+WorxI8QKpxHVHbzpvvyLXpiAbtSk0reIPC1c9iRw+MR0GAvw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.5.2.tgz", + "integrity": "sha512-KeS36526VruYO9HzhFGqhE5tbps7e94DV0b4j5wfPr7V51EfPzvjAiMWllsQDARv67wdbQ80c0Wg516XTlekgA==", "cpu": [ "x64" ], @@ -10259,9 +10268,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-win32-arm64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.3.1.tgz", - "integrity": "sha512-mkV6HERTtP2uY6aq0AhxbkL6KSsvomobPOypdEdrnfUsc2Rvd+U/pWl/flZHFkk8V6aXzEG56lWCZqXVyGUD1Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.5.2.tgz", + "integrity": "sha512-jlRTycYKOiSqc0fcqvabOH/HZX9BOG0S8EGsLmqEr2OkJLZc25ByD1n22P486R2n+m8GQwL6pX+L1LPpOPmz0A==", "cpu": [ "arm64" ], @@ -10273,9 +10282,9 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/nx-win32-x64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.3.1.tgz", - "integrity": "sha512-9cQZDiLT9bD1ixZ+WwNHVPRrxuszGHc30xzLzVgQ2l/IwOHJX6oRxMZa1IfgUYv846K0TSKWis+S41tcxUy80Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.5.2.tgz", + "integrity": "sha512-Ur8GPdz52kLS5uE9IQf0wBtGyvQm4Y3M1ZDjRkR+oGf26aVGNTK6C0+kMJPuggR4Z6lurmHYA34ViGi2hHPPpA==", "cpu": [ "x64" ], @@ -10287,26 +10296,34 @@ ] }, "node_modules/@nx/module-federation/node_modules/@nx/workspace": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.3.1.tgz", - "integrity": "sha512-MiS0x/Wl4vv+4oFWvsZLFsRR9E3tDh002ZeGjvGJbiZw8eUAMfX1mYhU7URxHSr8yoW1qCAKEvgAjGTLRz9Kkw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.5.2.tgz", + "integrity": "sha512-7IDa5xqVwGgZXrFGqyMzZTOq0Okxc0KH6M0mLfHJy1393iEUJjLByfkQ0nDyjsRZsLqo11WMOldapBDwy6MlaQ==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", + "@nx/devkit": "21.5.2", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "21.3.1", + "nx": "21.5.2", "picomatch": "4.0.2", + "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" } }, "node_modules/@nx/module-federation/node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nx/module-federation/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -10349,6 +10366,30 @@ "url": "https://dotenvx.com" } }, + "node_modules/@nx/module-federation/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nx/module-federation/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@nx/module-federation/node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -10363,21 +10404,28 @@ } }, "node_modules/@nx/module-federation/node_modules/jest-diff": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@nx/module-federation/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/@nx/module-federation/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -10386,9 +10434,9 @@ "license": "MIT" }, "node_modules/@nx/module-federation/node_modules/nx": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-21.3.1.tgz", - "integrity": "sha512-lOMDktM4CUcVa/yUmiAXGNxbNo6SC0T8/alRml1sgaOG1QHUpH6XyA1/nR4M3DNjlmON4wD06pZQUDKFb8kd8w==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-21.5.2.tgz", + "integrity": "sha512-hvq3W6mWsNuXzO1VWXpVcbGuF3e4cx0PyPavy8RgZUinbnh3Gk+f+2DGXyjKEyAG3Ql0Nl3V4RJERZzXEVl7EA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -10434,16 +10482,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "21.3.1", - "@nx/nx-darwin-x64": "21.3.1", - "@nx/nx-freebsd-x64": "21.3.1", - "@nx/nx-linux-arm-gnueabihf": "21.3.1", - "@nx/nx-linux-arm64-gnu": "21.3.1", - "@nx/nx-linux-arm64-musl": "21.3.1", - "@nx/nx-linux-x64-gnu": "21.3.1", - "@nx/nx-linux-x64-musl": "21.3.1", - "@nx/nx-win32-arm64-msvc": "21.3.1", - "@nx/nx-win32-x64-msvc": "21.3.1" + "@nx/nx-darwin-arm64": "21.5.2", + "@nx/nx-darwin-x64": "21.5.2", + "@nx/nx-freebsd-x64": "21.5.2", + "@nx/nx-linux-arm-gnueabihf": "21.5.2", + "@nx/nx-linux-arm64-gnu": "21.5.2", + "@nx/nx-linux-arm64-musl": "21.5.2", + "@nx/nx-linux-x64-gnu": "21.5.2", + "@nx/nx-linux-x64-musl": "21.5.2", + "@nx/nx-win32-arm64-msvc": "21.5.2", + "@nx/nx-win32-x64-msvc": "21.5.2" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -10500,13 +10548,13 @@ } }, "node_modules/@nx/module-federation/node_modules/pretty-format": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.1", + "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" }, @@ -10535,6 +10583,55 @@ "source-map": "^0.6.0" } }, + "node_modules/@nx/module-federation/node_modules/webpack": { + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/@nx/nest": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/@nx/nest/-/nest-21.2.0.tgz", @@ -10707,26 +10804,26 @@ ] }, "node_modules/@nx/rspack": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-21.3.1.tgz", - "integrity": "sha512-9AFTbPLZVVC+IO13onllU/wliWS8z+Uv/i4J4zBVM5Z3mK9BlPyV8hOOJ7zHar76/2O2o2dhAYZhwxyC+GwEDw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/rspack/-/rspack-21.5.2.tgz", + "integrity": "sha512-3r2hMR6HpCFNdiqyDGfwsGtI7PP60RjKjGNlTD5v8xltcXjvHzzofxWRd7TrL5HzOWJkVRlfR1xUiKJrCiu9aw==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", - "@nx/js": "21.3.1", - "@nx/module-federation": "21.3.1", - "@nx/web": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/js": "21.5.2", + "@nx/module-federation": "21.5.2", + "@nx/web": "21.5.2", "@phenomnomnominal/tsquery": "~5.0.1", - "@rspack/core": "^1.3.8", - "@rspack/dev-server": "^1.1.1", + "@rspack/core": "^1.5.0", + "@rspack/dev-server": "^1.1.4", "@rspack/plugin-react-refresh": "^1.0.0", "autoprefixer": "^10.4.9", "browserslist": "^4.21.4", "css-loader": "^6.4.0", "enquirer": "~2.3.6", "express": "^4.21.2", - "http-proxy-middleware": "^3.0.3", + "http-proxy-middleware": "^3.0.5", "less-loader": "^11.1.0", "license-webpack-plugin": "^4.0.2", "loader-utils": "^2.0.3", @@ -10742,18 +10839,18 @@ "style-loader": "^3.3.0", "ts-checker-rspack-plugin": "^1.1.1", "tslib": "^2.3.0", - "webpack": "^5.80.0", + "webpack": "^5.101.3", "webpack-node-externals": "^3.0.0" }, "peerDependencies": { - "@module-federation/enhanced": "^0.17.0", - "@module-federation/node": "^2.7.9" + "@module-federation/enhanced": "^0.18.0", + "@module-federation/node": "^2.7.11" } }, "node_modules/@nx/rspack/node_modules/@jest/schemas": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", - "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { @@ -10764,9 +10861,9 @@ } }, "node_modules/@nx/rspack/node_modules/@nx/devkit": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.3.1.tgz", - "integrity": "sha512-0FA6uVIJ5tOrUz6kJEeyX6KKPmXBs9kWQbf08yXogEzMimz9cVpoDS8MGmK14e13UwnY68vqXgknHAL+ATs3Zg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.5.2.tgz", + "integrity": "sha512-coNOyRBHeB6XHbEYeJ8bd/vhPqGx1+KhXojEsQv9vN9sgONqgWEUk0p/XnIplIvI0E7M/hm8zheydhZNYC9xSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10780,13 +10877,13 @@ "yargs-parser": "21.1.1" }, "peerDependencies": { - "nx": "21.3.1" + "nx": ">= 20 <= 22" } }, "node_modules/@nx/rspack/node_modules/@nx/js": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.3.1.tgz", - "integrity": "sha512-zc+3t3NOBWdnqPL94gsYspYhkiKYd3fflvDNdiOpf3XKhXZCE89YsbcZGoxDnW0A0bKAk4Z7wwh9txnG4A2jZA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.5.2.tgz", + "integrity": "sha512-zJDzdN0xY5dTw5fPR+IRVpKnRf/hl2WjyBGM42Jkda6vV9aR/sRkz8evXza781FWZ3o2P/wTZhQRMiO5O1fy4w==", "dev": true, "license": "MIT", "dependencies": { @@ -10797,8 +10894,8 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "21.3.1", - "@nx/workspace": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/workspace": "21.5.2", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", @@ -10830,9 +10927,9 @@ } }, "node_modules/@nx/rspack/node_modules/@nx/nx-darwin-arm64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.3.1.tgz", - "integrity": "sha512-DND5/CRN1rP7qMt4xkDjklzf3OoA3JcweN+47xZCfiQlu/VobvnS04OC6tLZc+Nymi73whk4lserpUG9biQjCA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.5.2.tgz", + "integrity": "sha512-PrfZbV2blRHoWLor+xDVwPY/dk46kbsmuTXCZRYlNAwko521Y9dCAJT0UOROic3zoUasQ+TwqsQextIcKCotIA==", "cpu": [ "arm64" ], @@ -10844,9 +10941,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-darwin-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.3.1.tgz", - "integrity": "sha512-iFR/abYakCwrFFIfb6gRXN6/gytle/8jj2jwEol0EFrkBIrBi/YrSyuRpsbnxuDB7MhuZ9zwvxlkE6mEJt9UoQ==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.5.2.tgz", + "integrity": "sha512-YaLY2Cqbjrl+pDddHV7GFtokn81GLvoqg+i9k0Eiid8B0dDLBZpJ3VQKr4RkTzxBX38UuHbJUwrZc8L9z8vqEw==", "cpu": [ "x64" ], @@ -10858,9 +10955,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-freebsd-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.3.1.tgz", - "integrity": "sha512-e/cx0cR8sLBX3b2JRLqwXj8z4ENhgDwJ5CF7hbcNRkMncKz1J2MZSsqHQHKUfls+HT4Mmmzwyf86laj879cs7Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.5.2.tgz", + "integrity": "sha512-2z/Wd42/KHFyT0zRVxWHlaRBQz12Fd1A0FCGJzuWI8G2meh9tYt4MN96gQ4q/rLQ0fmfFEEECq6pmOfCi8t9Mg==", "cpu": [ "x64" ], @@ -10872,9 +10969,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.3.1.tgz", - "integrity": "sha512-JvDfLVZhxzKfetcA1r5Ak+re5Qfks6JdgQD165wMysgAyZDdeM1GFn78xo5CqRPShlE8f/nhF4aX405OL6HYPw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.5.2.tgz", + "integrity": "sha512-lY2O1py8x+l39XAFFuplKlzouPC9K/gERYEB/b5jHGf7PGfNj0BX2MDmUztgTty6kKUnkRele39aSoQqWok0gA==", "cpu": [ "arm" ], @@ -10886,9 +10983,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-linux-arm64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.3.1.tgz", - "integrity": "sha512-J+LkCHzFCgZw4ZMgIuahprjaabWTDmsqJdsYLPFm/pw7TR6AyidXzUEZPfEgBK5WTO1PQr1LJp+Ps8twpf+iBg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.5.2.tgz", + "integrity": "sha512-gcpkXXPpWaf8wB0FZUaKmk8Jdv+QMHLiOcQuuXYi1X0vbgotVTl/y+dccwG1EZml6V5JIRGtg2YDM61a7Olp1Q==", "cpu": [ "arm64" ], @@ -10900,9 +10997,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-linux-arm64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.3.1.tgz", - "integrity": "sha512-VCiwPf4pj6WZWmPl30UpcKyp8DWb7Axs0pvU0/dsJz6Ye7bhKnsEZ/Ehp4laVZkck+MVEMFMHavikUdgNzWx3g==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.5.2.tgz", + "integrity": "sha512-oCSUwT0hORgFJWIGjwl6x4/2mVusw+3YAcSrvDePAXadjPSEMLZlJEE+4HExzqLFFBTxc+ucvyOIk08P4BtNJg==", "cpu": [ "arm64" ], @@ -10914,9 +11011,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-linux-x64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.3.1.tgz", - "integrity": "sha512-Erxxqir8zZDgfrTgOOeaByn22e8vbOTxWNif5i0brH2tQpdr6+2f3v1qNrRlP9CWjqwypPDmkU781U38u+qHHg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.5.2.tgz", + "integrity": "sha512-rgJTQk0iaidxEIMOuRQJS36Sk4+qcpJP0uwymvgyoTpZyBdkX38NHH3D+E6sudPSFWsiVxJpkCzYE4ScSKF8Ew==", "cpu": [ "x64" ], @@ -10928,9 +11025,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-linux-x64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.3.1.tgz", - "integrity": "sha512-dG5djRnC3zzOjEzOAzVX8u1sZSn0TSmvVUKKH+WorxI8QKpxHVHbzpvvyLXpiAbtSk0reIPC1c9iRw+MR0GAvw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.5.2.tgz", + "integrity": "sha512-KeS36526VruYO9HzhFGqhE5tbps7e94DV0b4j5wfPr7V51EfPzvjAiMWllsQDARv67wdbQ80c0Wg516XTlekgA==", "cpu": [ "x64" ], @@ -10942,9 +11039,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-win32-arm64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.3.1.tgz", - "integrity": "sha512-mkV6HERTtP2uY6aq0AhxbkL6KSsvomobPOypdEdrnfUsc2Rvd+U/pWl/flZHFkk8V6aXzEG56lWCZqXVyGUD1Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.5.2.tgz", + "integrity": "sha512-jlRTycYKOiSqc0fcqvabOH/HZX9BOG0S8EGsLmqEr2OkJLZc25ByD1n22P486R2n+m8GQwL6pX+L1LPpOPmz0A==", "cpu": [ "arm64" ], @@ -10956,9 +11053,9 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/nx-win32-x64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.3.1.tgz", - "integrity": "sha512-9cQZDiLT9bD1ixZ+WwNHVPRrxuszGHc30xzLzVgQ2l/IwOHJX6oRxMZa1IfgUYv846K0TSKWis+S41tcxUy80Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.5.2.tgz", + "integrity": "sha512-Ur8GPdz52kLS5uE9IQf0wBtGyvQm4Y3M1ZDjRkR+oGf26aVGNTK6C0+kMJPuggR4Z6lurmHYA34ViGi2hHPPpA==", "cpu": [ "x64" ], @@ -10970,26 +11067,34 @@ ] }, "node_modules/@nx/rspack/node_modules/@nx/workspace": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.3.1.tgz", - "integrity": "sha512-MiS0x/Wl4vv+4oFWvsZLFsRR9E3tDh002ZeGjvGJbiZw8eUAMfX1mYhU7URxHSr8yoW1qCAKEvgAjGTLRz9Kkw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.5.2.tgz", + "integrity": "sha512-7IDa5xqVwGgZXrFGqyMzZTOq0Okxc0KH6M0mLfHJy1393iEUJjLByfkQ0nDyjsRZsLqo11WMOldapBDwy6MlaQ==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", + "@nx/devkit": "21.5.2", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "21.3.1", + "nx": "21.5.2", "picomatch": "4.0.2", + "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" } }, "node_modules/@nx/rspack/node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nx/rspack/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -11068,6 +11173,30 @@ "url": "https://dotenvx.com" } }, + "node_modules/@nx/rspack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nx/rspack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@nx/rspack/node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -11082,21 +11211,28 @@ } }, "node_modules/@nx/rspack/node_modules/jest-diff": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@nx/rspack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/@nx/rspack/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -11138,9 +11274,9 @@ } }, "node_modules/@nx/rspack/node_modules/nx": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-21.3.1.tgz", - "integrity": "sha512-lOMDktM4CUcVa/yUmiAXGNxbNo6SC0T8/alRml1sgaOG1QHUpH6XyA1/nR4M3DNjlmON4wD06pZQUDKFb8kd8w==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-21.5.2.tgz", + "integrity": "sha512-hvq3W6mWsNuXzO1VWXpVcbGuF3e4cx0PyPavy8RgZUinbnh3Gk+f+2DGXyjKEyAG3Ql0Nl3V4RJERZzXEVl7EA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -11186,16 +11322,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "21.3.1", - "@nx/nx-darwin-x64": "21.3.1", - "@nx/nx-freebsd-x64": "21.3.1", - "@nx/nx-linux-arm-gnueabihf": "21.3.1", - "@nx/nx-linux-arm64-gnu": "21.3.1", - "@nx/nx-linux-arm64-musl": "21.3.1", - "@nx/nx-linux-x64-gnu": "21.3.1", - "@nx/nx-linux-x64-musl": "21.3.1", - "@nx/nx-win32-arm64-msvc": "21.3.1", - "@nx/nx-win32-x64-msvc": "21.3.1" + "@nx/nx-darwin-arm64": "21.5.2", + "@nx/nx-darwin-x64": "21.5.2", + "@nx/nx-freebsd-x64": "21.5.2", + "@nx/nx-linux-arm-gnueabihf": "21.5.2", + "@nx/nx-linux-arm64-gnu": "21.5.2", + "@nx/nx-linux-arm64-musl": "21.5.2", + "@nx/nx-linux-x64-gnu": "21.5.2", + "@nx/nx-linux-x64-musl": "21.5.2", + "@nx/nx-win32-arm64-msvc": "21.5.2", + "@nx/nx-win32-x64-msvc": "21.5.2" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -11259,13 +11395,13 @@ "license": "MIT" }, "node_modules/@nx/rspack/node_modules/pretty-format": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.1", + "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" }, @@ -11294,25 +11430,74 @@ "source-map": "^0.6.0" } }, - "node_modules/@nx/web": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/web/-/web-21.3.1.tgz", - "integrity": "sha512-z1BqpHPHf1PQXy5Npr3vpdJIUZqfq5VXdSIanAXaLJNPgP18AfVap2ozlHcQytLQErNVKHK1JTzUuHJFipSl4A==", + "node_modules/@nx/rspack/node_modules/webpack": { + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", - "@nx/js": "21.3.1", - "detect-port": "^1.5.1", - "http-server": "^14.1.0", - "picocolors": "^1.1.0", - "tslib": "^2.3.0" + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@nx/web": { + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/web/-/web-21.5.2.tgz", + "integrity": "sha512-+3oBFCncou+Pxfieo+lSJazNhwJ7SW5dLQHdBImSiO25RUEQdBEBv9Zg+/qGJNMEWHmNw2a1txVGj1Ka+azx2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nx/devkit": "21.5.2", + "@nx/js": "21.5.2", + "detect-port": "^1.5.1", + "http-server": "^14.1.0", + "picocolors": "^1.1.0", + "tslib": "^2.3.0" } }, "node_modules/@nx/web/node_modules/@jest/schemas": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", - "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { @@ -11323,9 +11508,9 @@ } }, "node_modules/@nx/web/node_modules/@nx/devkit": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.3.1.tgz", - "integrity": "sha512-0FA6uVIJ5tOrUz6kJEeyX6KKPmXBs9kWQbf08yXogEzMimz9cVpoDS8MGmK14e13UwnY68vqXgknHAL+ATs3Zg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.5.2.tgz", + "integrity": "sha512-coNOyRBHeB6XHbEYeJ8bd/vhPqGx1+KhXojEsQv9vN9sgONqgWEUk0p/XnIplIvI0E7M/hm8zheydhZNYC9xSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11339,13 +11524,13 @@ "yargs-parser": "21.1.1" }, "peerDependencies": { - "nx": "21.3.1" + "nx": ">= 20 <= 22" } }, "node_modules/@nx/web/node_modules/@nx/js": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.3.1.tgz", - "integrity": "sha512-zc+3t3NOBWdnqPL94gsYspYhkiKYd3fflvDNdiOpf3XKhXZCE89YsbcZGoxDnW0A0bKAk4Z7wwh9txnG4A2jZA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.5.2.tgz", + "integrity": "sha512-zJDzdN0xY5dTw5fPR+IRVpKnRf/hl2WjyBGM42Jkda6vV9aR/sRkz8evXza781FWZ3o2P/wTZhQRMiO5O1fy4w==", "dev": true, "license": "MIT", "dependencies": { @@ -11356,8 +11541,8 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "21.3.1", - "@nx/workspace": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/workspace": "21.5.2", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", @@ -11389,9 +11574,9 @@ } }, "node_modules/@nx/web/node_modules/@nx/nx-darwin-arm64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.3.1.tgz", - "integrity": "sha512-DND5/CRN1rP7qMt4xkDjklzf3OoA3JcweN+47xZCfiQlu/VobvnS04OC6tLZc+Nymi73whk4lserpUG9biQjCA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.5.2.tgz", + "integrity": "sha512-PrfZbV2blRHoWLor+xDVwPY/dk46kbsmuTXCZRYlNAwko521Y9dCAJT0UOROic3zoUasQ+TwqsQextIcKCotIA==", "cpu": [ "arm64" ], @@ -11403,9 +11588,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-darwin-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.3.1.tgz", - "integrity": "sha512-iFR/abYakCwrFFIfb6gRXN6/gytle/8jj2jwEol0EFrkBIrBi/YrSyuRpsbnxuDB7MhuZ9zwvxlkE6mEJt9UoQ==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.5.2.tgz", + "integrity": "sha512-YaLY2Cqbjrl+pDddHV7GFtokn81GLvoqg+i9k0Eiid8B0dDLBZpJ3VQKr4RkTzxBX38UuHbJUwrZc8L9z8vqEw==", "cpu": [ "x64" ], @@ -11417,9 +11602,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-freebsd-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.3.1.tgz", - "integrity": "sha512-e/cx0cR8sLBX3b2JRLqwXj8z4ENhgDwJ5CF7hbcNRkMncKz1J2MZSsqHQHKUfls+HT4Mmmzwyf86laj879cs7Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.5.2.tgz", + "integrity": "sha512-2z/Wd42/KHFyT0zRVxWHlaRBQz12Fd1A0FCGJzuWI8G2meh9tYt4MN96gQ4q/rLQ0fmfFEEECq6pmOfCi8t9Mg==", "cpu": [ "x64" ], @@ -11431,9 +11616,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.3.1.tgz", - "integrity": "sha512-JvDfLVZhxzKfetcA1r5Ak+re5Qfks6JdgQD165wMysgAyZDdeM1GFn78xo5CqRPShlE8f/nhF4aX405OL6HYPw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.5.2.tgz", + "integrity": "sha512-lY2O1py8x+l39XAFFuplKlzouPC9K/gERYEB/b5jHGf7PGfNj0BX2MDmUztgTty6kKUnkRele39aSoQqWok0gA==", "cpu": [ "arm" ], @@ -11445,9 +11630,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-linux-arm64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.3.1.tgz", - "integrity": "sha512-J+LkCHzFCgZw4ZMgIuahprjaabWTDmsqJdsYLPFm/pw7TR6AyidXzUEZPfEgBK5WTO1PQr1LJp+Ps8twpf+iBg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.5.2.tgz", + "integrity": "sha512-gcpkXXPpWaf8wB0FZUaKmk8Jdv+QMHLiOcQuuXYi1X0vbgotVTl/y+dccwG1EZml6V5JIRGtg2YDM61a7Olp1Q==", "cpu": [ "arm64" ], @@ -11459,9 +11644,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-linux-arm64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.3.1.tgz", - "integrity": "sha512-VCiwPf4pj6WZWmPl30UpcKyp8DWb7Axs0pvU0/dsJz6Ye7bhKnsEZ/Ehp4laVZkck+MVEMFMHavikUdgNzWx3g==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.5.2.tgz", + "integrity": "sha512-oCSUwT0hORgFJWIGjwl6x4/2mVusw+3YAcSrvDePAXadjPSEMLZlJEE+4HExzqLFFBTxc+ucvyOIk08P4BtNJg==", "cpu": [ "arm64" ], @@ -11473,9 +11658,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-linux-x64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.3.1.tgz", - "integrity": "sha512-Erxxqir8zZDgfrTgOOeaByn22e8vbOTxWNif5i0brH2tQpdr6+2f3v1qNrRlP9CWjqwypPDmkU781U38u+qHHg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.5.2.tgz", + "integrity": "sha512-rgJTQk0iaidxEIMOuRQJS36Sk4+qcpJP0uwymvgyoTpZyBdkX38NHH3D+E6sudPSFWsiVxJpkCzYE4ScSKF8Ew==", "cpu": [ "x64" ], @@ -11487,9 +11672,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-linux-x64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.3.1.tgz", - "integrity": "sha512-dG5djRnC3zzOjEzOAzVX8u1sZSn0TSmvVUKKH+WorxI8QKpxHVHbzpvvyLXpiAbtSk0reIPC1c9iRw+MR0GAvw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.5.2.tgz", + "integrity": "sha512-KeS36526VruYO9HzhFGqhE5tbps7e94DV0b4j5wfPr7V51EfPzvjAiMWllsQDARv67wdbQ80c0Wg516XTlekgA==", "cpu": [ "x64" ], @@ -11501,9 +11686,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-win32-arm64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.3.1.tgz", - "integrity": "sha512-mkV6HERTtP2uY6aq0AhxbkL6KSsvomobPOypdEdrnfUsc2Rvd+U/pWl/flZHFkk8V6aXzEG56lWCZqXVyGUD1Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.5.2.tgz", + "integrity": "sha512-jlRTycYKOiSqc0fcqvabOH/HZX9BOG0S8EGsLmqEr2OkJLZc25ByD1n22P486R2n+m8GQwL6pX+L1LPpOPmz0A==", "cpu": [ "arm64" ], @@ -11515,9 +11700,9 @@ ] }, "node_modules/@nx/web/node_modules/@nx/nx-win32-x64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.3.1.tgz", - "integrity": "sha512-9cQZDiLT9bD1ixZ+WwNHVPRrxuszGHc30xzLzVgQ2l/IwOHJX6oRxMZa1IfgUYv846K0TSKWis+S41tcxUy80Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.5.2.tgz", + "integrity": "sha512-Ur8GPdz52kLS5uE9IQf0wBtGyvQm4Y3M1ZDjRkR+oGf26aVGNTK6C0+kMJPuggR4Z6lurmHYA34ViGi2hHPPpA==", "cpu": [ "x64" ], @@ -11529,26 +11714,27 @@ ] }, "node_modules/@nx/web/node_modules/@nx/workspace": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.3.1.tgz", - "integrity": "sha512-MiS0x/Wl4vv+4oFWvsZLFsRR9E3tDh002ZeGjvGJbiZw8eUAMfX1mYhU7URxHSr8yoW1qCAKEvgAjGTLRz9Kkw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.5.2.tgz", + "integrity": "sha512-7IDa5xqVwGgZXrFGqyMzZTOq0Okxc0KH6M0mLfHJy1393iEUJjLByfkQ0nDyjsRZsLqo11WMOldapBDwy6MlaQ==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", + "@nx/devkit": "21.5.2", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "21.3.1", + "nx": "21.5.2", "picomatch": "4.0.2", + "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" } }, "node_modules/@nx/web/node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -11605,16 +11791,16 @@ } }, "node_modules/@nx/web/node_modules/jest-diff": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -11628,9 +11814,9 @@ "license": "MIT" }, "node_modules/@nx/web/node_modules/nx": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-21.3.1.tgz", - "integrity": "sha512-lOMDktM4CUcVa/yUmiAXGNxbNo6SC0T8/alRml1sgaOG1QHUpH6XyA1/nR4M3DNjlmON4wD06pZQUDKFb8kd8w==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-21.5.2.tgz", + "integrity": "sha512-hvq3W6mWsNuXzO1VWXpVcbGuF3e4cx0PyPavy8RgZUinbnh3Gk+f+2DGXyjKEyAG3Ql0Nl3V4RJERZzXEVl7EA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -11676,16 +11862,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "21.3.1", - "@nx/nx-darwin-x64": "21.3.1", - "@nx/nx-freebsd-x64": "21.3.1", - "@nx/nx-linux-arm-gnueabihf": "21.3.1", - "@nx/nx-linux-arm64-gnu": "21.3.1", - "@nx/nx-linux-arm64-musl": "21.3.1", - "@nx/nx-linux-x64-gnu": "21.3.1", - "@nx/nx-linux-x64-musl": "21.3.1", - "@nx/nx-win32-arm64-msvc": "21.3.1", - "@nx/nx-win32-x64-msvc": "21.3.1" + "@nx/nx-darwin-arm64": "21.5.2", + "@nx/nx-darwin-x64": "21.5.2", + "@nx/nx-freebsd-x64": "21.5.2", + "@nx/nx-linux-arm-gnueabihf": "21.5.2", + "@nx/nx-linux-arm64-gnu": "21.5.2", + "@nx/nx-linux-arm64-musl": "21.5.2", + "@nx/nx-linux-x64-gnu": "21.5.2", + "@nx/nx-linux-x64-musl": "21.5.2", + "@nx/nx-win32-arm64-msvc": "21.5.2", + "@nx/nx-win32-x64-msvc": "21.5.2" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -11742,13 +11928,13 @@ } }, "node_modules/@nx/web/node_modules/pretty-format": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.1", + "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" }, @@ -11778,15 +11964,15 @@ } }, "node_modules/@nx/webpack": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-21.3.1.tgz", - "integrity": "sha512-Nanso6q137zXR5dJhJM890GLMQBp4HaFKdXIsB5D5rgUMj3m8dQ3duUIiuaqI+dwajRi/e4Za8GENi5y3vl6Sg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/webpack/-/webpack-21.5.2.tgz", + "integrity": "sha512-icBqtmsGhIECGqwCLSqHGVWOqmjBrbnawpnVSb0sF68ziZGmxwXwvsZEWwM3YAwoqgaJ8TO6bumcdpvvnWeYCQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.23.2", - "@nx/devkit": "21.3.1", - "@nx/js": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/js": "21.5.2", "@phenomnomnominal/tsquery": "~5.0.1", "ajv": "^8.12.0", "autoprefixer": "^10.4.9", @@ -11812,22 +11998,20 @@ "sass-loader": "^16.0.4", "source-map-loader": "^5.0.0", "style-loader": "^3.3.0", - "stylus": "^0.64.0", - "stylus-loader": "^7.1.0", "terser-webpack-plugin": "^5.3.3", "ts-loader": "^9.3.1", - "tsconfig-paths-webpack-plugin": "4.0.0", + "tsconfig-paths-webpack-plugin": "4.2.0", "tslib": "^2.3.0", - "webpack": "~5.99.0", + "webpack": "^5.101.3", "webpack-dev-server": "^5.2.1", "webpack-node-externals": "^3.0.0", "webpack-subresource-integrity": "^5.1.0" } }, "node_modules/@nx/webpack/node_modules/@jest/schemas": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.1.tgz", - "integrity": "sha512-+g/1TKjFuGrf1Hh0QPCv0gISwBxJ+MQSNXmG9zjHy7BmFhtoJ9fdNhWJp3qUKRi93AOZHXtdxZgJ1vAtz6z65w==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "license": "MIT", "dependencies": { @@ -11838,9 +12022,9 @@ } }, "node_modules/@nx/webpack/node_modules/@nx/devkit": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.3.1.tgz", - "integrity": "sha512-0FA6uVIJ5tOrUz6kJEeyX6KKPmXBs9kWQbf08yXogEzMimz9cVpoDS8MGmK14e13UwnY68vqXgknHAL+ATs3Zg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/devkit/-/devkit-21.5.2.tgz", + "integrity": "sha512-coNOyRBHeB6XHbEYeJ8bd/vhPqGx1+KhXojEsQv9vN9sgONqgWEUk0p/XnIplIvI0E7M/hm8zheydhZNYC9xSQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11854,13 +12038,13 @@ "yargs-parser": "21.1.1" }, "peerDependencies": { - "nx": "21.3.1" + "nx": ">= 20 <= 22" } }, "node_modules/@nx/webpack/node_modules/@nx/js": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.3.1.tgz", - "integrity": "sha512-zc+3t3NOBWdnqPL94gsYspYhkiKYd3fflvDNdiOpf3XKhXZCE89YsbcZGoxDnW0A0bKAk4Z7wwh9txnG4A2jZA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/js/-/js-21.5.2.tgz", + "integrity": "sha512-zJDzdN0xY5dTw5fPR+IRVpKnRf/hl2WjyBGM42Jkda6vV9aR/sRkz8evXza781FWZ3o2P/wTZhQRMiO5O1fy4w==", "dev": true, "license": "MIT", "dependencies": { @@ -11871,8 +12055,8 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-typescript": "^7.22.5", "@babel/runtime": "^7.22.6", - "@nx/devkit": "21.3.1", - "@nx/workspace": "21.3.1", + "@nx/devkit": "21.5.2", + "@nx/workspace": "21.5.2", "@zkochan/js-yaml": "0.0.7", "babel-plugin-const-enum": "^1.0.1", "babel-plugin-macros": "^3.1.0", @@ -11904,9 +12088,9 @@ } }, "node_modules/@nx/webpack/node_modules/@nx/nx-darwin-arm64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.3.1.tgz", - "integrity": "sha512-DND5/CRN1rP7qMt4xkDjklzf3OoA3JcweN+47xZCfiQlu/VobvnS04OC6tLZc+Nymi73whk4lserpUG9biQjCA==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-arm64/-/nx-darwin-arm64-21.5.2.tgz", + "integrity": "sha512-PrfZbV2blRHoWLor+xDVwPY/dk46kbsmuTXCZRYlNAwko521Y9dCAJT0UOROic3zoUasQ+TwqsQextIcKCotIA==", "cpu": [ "arm64" ], @@ -11918,9 +12102,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-darwin-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.3.1.tgz", - "integrity": "sha512-iFR/abYakCwrFFIfb6gRXN6/gytle/8jj2jwEol0EFrkBIrBi/YrSyuRpsbnxuDB7MhuZ9zwvxlkE6mEJt9UoQ==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-darwin-x64/-/nx-darwin-x64-21.5.2.tgz", + "integrity": "sha512-YaLY2Cqbjrl+pDddHV7GFtokn81GLvoqg+i9k0Eiid8B0dDLBZpJ3VQKr4RkTzxBX38UuHbJUwrZc8L9z8vqEw==", "cpu": [ "x64" ], @@ -11932,9 +12116,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-freebsd-x64": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.3.1.tgz", - "integrity": "sha512-e/cx0cR8sLBX3b2JRLqwXj8z4ENhgDwJ5CF7hbcNRkMncKz1J2MZSsqHQHKUfls+HT4Mmmzwyf86laj879cs7Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-freebsd-x64/-/nx-freebsd-x64-21.5.2.tgz", + "integrity": "sha512-2z/Wd42/KHFyT0zRVxWHlaRBQz12Fd1A0FCGJzuWI8G2meh9tYt4MN96gQ4q/rLQ0fmfFEEECq6pmOfCi8t9Mg==", "cpu": [ "x64" ], @@ -11946,9 +12130,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-linux-arm-gnueabihf": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.3.1.tgz", - "integrity": "sha512-JvDfLVZhxzKfetcA1r5Ak+re5Qfks6JdgQD165wMysgAyZDdeM1GFn78xo5CqRPShlE8f/nhF4aX405OL6HYPw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-21.5.2.tgz", + "integrity": "sha512-lY2O1py8x+l39XAFFuplKlzouPC9K/gERYEB/b5jHGf7PGfNj0BX2MDmUztgTty6kKUnkRele39aSoQqWok0gA==", "cpu": [ "arm" ], @@ -11960,9 +12144,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-linux-arm64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.3.1.tgz", - "integrity": "sha512-J+LkCHzFCgZw4ZMgIuahprjaabWTDmsqJdsYLPFm/pw7TR6AyidXzUEZPfEgBK5WTO1PQr1LJp+Ps8twpf+iBg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-21.5.2.tgz", + "integrity": "sha512-gcpkXXPpWaf8wB0FZUaKmk8Jdv+QMHLiOcQuuXYi1X0vbgotVTl/y+dccwG1EZml6V5JIRGtg2YDM61a7Olp1Q==", "cpu": [ "arm64" ], @@ -11974,9 +12158,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-linux-arm64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.3.1.tgz", - "integrity": "sha512-VCiwPf4pj6WZWmPl30UpcKyp8DWb7Axs0pvU0/dsJz6Ye7bhKnsEZ/Ehp4laVZkck+MVEMFMHavikUdgNzWx3g==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-21.5.2.tgz", + "integrity": "sha512-oCSUwT0hORgFJWIGjwl6x4/2mVusw+3YAcSrvDePAXadjPSEMLZlJEE+4HExzqLFFBTxc+ucvyOIk08P4BtNJg==", "cpu": [ "arm64" ], @@ -11988,9 +12172,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-linux-x64-gnu": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.3.1.tgz", - "integrity": "sha512-Erxxqir8zZDgfrTgOOeaByn22e8vbOTxWNif5i0brH2tQpdr6+2f3v1qNrRlP9CWjqwypPDmkU781U38u+qHHg==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-21.5.2.tgz", + "integrity": "sha512-rgJTQk0iaidxEIMOuRQJS36Sk4+qcpJP0uwymvgyoTpZyBdkX38NHH3D+E6sudPSFWsiVxJpkCzYE4ScSKF8Ew==", "cpu": [ "x64" ], @@ -12002,9 +12186,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-linux-x64-musl": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.3.1.tgz", - "integrity": "sha512-dG5djRnC3zzOjEzOAzVX8u1sZSn0TSmvVUKKH+WorxI8QKpxHVHbzpvvyLXpiAbtSk0reIPC1c9iRw+MR0GAvw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-21.5.2.tgz", + "integrity": "sha512-KeS36526VruYO9HzhFGqhE5tbps7e94DV0b4j5wfPr7V51EfPzvjAiMWllsQDARv67wdbQ80c0Wg516XTlekgA==", "cpu": [ "x64" ], @@ -12016,9 +12200,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-win32-arm64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.3.1.tgz", - "integrity": "sha512-mkV6HERTtP2uY6aq0AhxbkL6KSsvomobPOypdEdrnfUsc2Rvd+U/pWl/flZHFkk8V6aXzEG56lWCZqXVyGUD1Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-21.5.2.tgz", + "integrity": "sha512-jlRTycYKOiSqc0fcqvabOH/HZX9BOG0S8EGsLmqEr2OkJLZc25ByD1n22P486R2n+m8GQwL6pX+L1LPpOPmz0A==", "cpu": [ "arm64" ], @@ -12030,9 +12214,9 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/nx-win32-x64-msvc": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.3.1.tgz", - "integrity": "sha512-9cQZDiLT9bD1ixZ+WwNHVPRrxuszGHc30xzLzVgQ2l/IwOHJX6oRxMZa1IfgUYv846K0TSKWis+S41tcxUy80Q==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-21.5.2.tgz", + "integrity": "sha512-Ur8GPdz52kLS5uE9IQf0wBtGyvQm4Y3M1ZDjRkR+oGf26aVGNTK6C0+kMJPuggR4Z6lurmHYA34ViGi2hHPPpA==", "cpu": [ "x64" ], @@ -12044,26 +12228,34 @@ ] }, "node_modules/@nx/webpack/node_modules/@nx/workspace": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.3.1.tgz", - "integrity": "sha512-MiS0x/Wl4vv+4oFWvsZLFsRR9E3tDh002ZeGjvGJbiZw8eUAMfX1mYhU7URxHSr8yoW1qCAKEvgAjGTLRz9Kkw==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.5.2.tgz", + "integrity": "sha512-7IDa5xqVwGgZXrFGqyMzZTOq0Okxc0KH6M0mLfHJy1393iEUJjLByfkQ0nDyjsRZsLqo11WMOldapBDwy6MlaQ==", "dev": true, "license": "MIT", "dependencies": { - "@nx/devkit": "21.3.1", + "@nx/devkit": "21.5.2", "@zkochan/js-yaml": "0.0.7", "chalk": "^4.1.0", "enquirer": "~2.3.6", - "nx": "21.3.1", + "nx": "21.5.2", "picomatch": "4.0.2", + "semver": "^7.6.3", "tslib": "^2.3.0", "yargs-parser": "21.1.1" } }, "node_modules/@nx/webpack/node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nx/webpack/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -12180,6 +12372,30 @@ "url": "https://dotenvx.com" } }, + "node_modules/@nx/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@nx/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/@nx/webpack/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -12228,21 +12444,28 @@ } }, "node_modules/@nx/webpack/node_modules/jest-diff": { - "version": "30.0.4", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.4.tgz", - "integrity": "sha512-TSjceIf6797jyd+R64NXqicttROD+Qf98fex7CowmlSn7f8+En0da1Dglwr1AXxDtVizoxXYZBlUQwNhoOXkNw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.2" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@nx/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/@nx/webpack/node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", @@ -12304,9 +12527,9 @@ } }, "node_modules/@nx/webpack/node_modules/nx": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/nx/-/nx-21.3.1.tgz", - "integrity": "sha512-lOMDktM4CUcVa/yUmiAXGNxbNo6SC0T8/alRml1sgaOG1QHUpH6XyA1/nR4M3DNjlmON4wD06pZQUDKFb8kd8w==", + "version": "21.5.2", + "resolved": "https://registry.npmjs.org/nx/-/nx-21.5.2.tgz", + "integrity": "sha512-hvq3W6mWsNuXzO1VWXpVcbGuF3e4cx0PyPavy8RgZUinbnh3Gk+f+2DGXyjKEyAG3Ql0Nl3V4RJERZzXEVl7EA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12352,16 +12575,16 @@ "nx-cloud": "bin/nx-cloud.js" }, "optionalDependencies": { - "@nx/nx-darwin-arm64": "21.3.1", - "@nx/nx-darwin-x64": "21.3.1", - "@nx/nx-freebsd-x64": "21.3.1", - "@nx/nx-linux-arm-gnueabihf": "21.3.1", - "@nx/nx-linux-arm64-gnu": "21.3.1", - "@nx/nx-linux-arm64-musl": "21.3.1", - "@nx/nx-linux-x64-gnu": "21.3.1", - "@nx/nx-linux-x64-musl": "21.3.1", - "@nx/nx-win32-arm64-msvc": "21.3.1", - "@nx/nx-win32-x64-msvc": "21.3.1" + "@nx/nx-darwin-arm64": "21.5.2", + "@nx/nx-darwin-x64": "21.5.2", + "@nx/nx-freebsd-x64": "21.5.2", + "@nx/nx-linux-arm-gnueabihf": "21.5.2", + "@nx/nx-linux-arm64-gnu": "21.5.2", + "@nx/nx-linux-arm64-musl": "21.5.2", + "@nx/nx-linux-x64-gnu": "21.5.2", + "@nx/nx-linux-x64-musl": "21.5.2", + "@nx/nx-win32-arm64-msvc": "21.5.2", + "@nx/nx-win32-x64-msvc": "21.5.2" }, "peerDependencies": { "@swc-node/register": "^1.8.0", @@ -12448,13 +12671,13 @@ } }, "node_modules/@nx/webpack/node_modules/pretty-format": { - "version": "30.0.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.2.tgz", - "integrity": "sha512-yC5/EBSOrTtqhCKfLHqoUIAXVRZnukHPwWBJWR7h84Q3Be1DRQZLncwcfLoPA5RPQ65qfiCMqgYwdUuQ//eVpg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/schemas": "30.0.1", + "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" }, @@ -12496,6 +12719,55 @@ "source-map": "^0.6.0" } }, + "node_modules/@nx/webpack/node_modules/webpack": { + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, "node_modules/@nx/workspace": { "version": "21.2.0", "resolved": "https://registry.npmjs.org/@nx/workspace/-/workspace-21.2.0.tgz", @@ -13192,27 +13464,28 @@ ] }, "node_modules/@rspack/binding": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.3.15.tgz", - "integrity": "sha512-utNPuJglLO5lW9XbwIqjB7+2ilMo6JkuVLTVdnNVKU94FW7asn9F/qV+d+MgjUVqU1QPCGm0NuGO9xhbgeJ7pg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.5.4.tgz", + "integrity": "sha512-HtLF5uxbf77hDarB/Wl26XgaTyWkhMogDPUOC1mLU+YPke1vYem8p8yr+McUkRtbhYoqtFMcVcT3S8jKJPP3+g==", "dev": true, "license": "MIT", "optionalDependencies": { - "@rspack/binding-darwin-arm64": "1.3.15", - "@rspack/binding-darwin-x64": "1.3.15", - "@rspack/binding-linux-arm64-gnu": "1.3.15", - "@rspack/binding-linux-arm64-musl": "1.3.15", - "@rspack/binding-linux-x64-gnu": "1.3.15", - "@rspack/binding-linux-x64-musl": "1.3.15", - "@rspack/binding-win32-arm64-msvc": "1.3.15", - "@rspack/binding-win32-ia32-msvc": "1.3.15", - "@rspack/binding-win32-x64-msvc": "1.3.15" + "@rspack/binding-darwin-arm64": "1.5.4", + "@rspack/binding-darwin-x64": "1.5.4", + "@rspack/binding-linux-arm64-gnu": "1.5.4", + "@rspack/binding-linux-arm64-musl": "1.5.4", + "@rspack/binding-linux-x64-gnu": "1.5.4", + "@rspack/binding-linux-x64-musl": "1.5.4", + "@rspack/binding-wasm32-wasi": "1.5.4", + "@rspack/binding-win32-arm64-msvc": "1.5.4", + "@rspack/binding-win32-ia32-msvc": "1.5.4", + "@rspack/binding-win32-x64-msvc": "1.5.4" } }, "node_modules/@rspack/binding-darwin-arm64": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.3.15.tgz", - "integrity": "sha512-f+DnVRENRdVe+ufpZeqTtWAUDSTnP48jVo7x9KWsXf8XyJHUi+eHKEPrFoy1HvL1/k5yJ3HVnFBh1Hb9cNIwSg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.5.4.tgz", + "integrity": "sha512-qD+n4D8KOOSoWdngK87iXl6lqbx1J63f6/xZFLPVIstzxIUbNyo9V9tpJYsoT3gYpnLkPVqA+KwQI0ozgYEXvw==", "cpu": [ "arm64" ], @@ -13224,9 +13497,9 @@ ] }, "node_modules/@rspack/binding-darwin-x64": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.3.15.tgz", - "integrity": "sha512-TfUvEIBqYUT2OK01BYXb2MNcZeZIhAnJy/5aj0qV0uy4KlvwW63HYcKWa1sFd4Ac7bnGShDkanvP3YEuHOFOyg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.5.4.tgz", + "integrity": "sha512-g75qkrLLa28kVp7pkWAjUADwr+0GumEF134VWHuL+TAm7VCw4IXRKnZhquE8K5kcqRpLcLX4guRqZzK9OEu/hg==", "cpu": [ "x64" ], @@ -13238,9 +13511,9 @@ ] }, "node_modules/@rspack/binding-linux-arm64-gnu": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.3.15.tgz", - "integrity": "sha512-D/YjYk9snKvYm1Elotq8/GsEipB4ZJWVv/V8cZ+ohhFNOPzygENi6JfyI06TryBTQiN0/JDZqt/S9RaWBWnMqw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.5.4.tgz", + "integrity": "sha512-O3zSTz/dy1EJHd7YS8zzmAG2zxewEZJi7QlYiU+YhFuqjP2ab6ZFWLHkglvrSy4aHyC8fx9OkSjioYtHUcCSdQ==", "cpu": [ "arm64" ], @@ -13252,9 +13525,9 @@ ] }, "node_modules/@rspack/binding-linux-arm64-musl": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.3.15.tgz", - "integrity": "sha512-lJbBsPMOiR0hYPCSM42yp7QiZjfo0ALtX7ws2wURpsQp3BMfRVAmXU3Ixpo2XCRtG1zj8crHaCmAWOJTS0smsA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.5.4.tgz", + "integrity": "sha512-ki84vbRY1gbf1T3BHiKAdi3m0hQFmqiAIYvFuLGA9Vop1R+W2C3Mzh8Q5YL6TnWOP0eiwizuigztz4/07fPf6Q==", "cpu": [ "arm64" ], @@ -13266,9 +13539,9 @@ ] }, "node_modules/@rspack/binding-linux-x64-gnu": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.3.15.tgz", - "integrity": "sha512-qGB8ucHklrzNg6lsAS36VrBsCbOw0acgpQNqTE5cuHWrp1Pu3GFTRiFEogenxEmzoRbohMZt0Ev5grivrcgKBQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.5.4.tgz", + "integrity": "sha512-SJVQSgR1JqDEnURI79SRcn/gcdG+yFb2mLUYV/TSPUTxMIlu44p5+fnOY6+6qMtjQhO6J4C2+UyV00U/yjlikA==", "cpu": [ "x64" ], @@ -13280,9 +13553,9 @@ ] }, "node_modules/@rspack/binding-linux-x64-musl": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.3.15.tgz", - "integrity": "sha512-qRn6e40fLQP+N2rQD8GAj/h4DakeTIho32VxTIaHRVuzw68ZD7VmKkwn55ssN370ejmey35ZdoNFNE12RBrMZA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.5.4.tgz", + "integrity": "sha512-UL1xw3yLsFH6UD/ubXXbRaDRNl+qI22QgugKYuqmpDGfOcVlv4fGpf3faPwYJasqPjhDWvcoyd8OqI+ftWKWEA==", "cpu": [ "x64" ], @@ -13293,10 +13566,48 @@ "linux" ] }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.5.4.tgz", + "integrity": "sha512-VPGhik1M87SZQzmX2sRvXrO6KgycSbmJ/bLqVuXHYGjsLkYqw4auKCJrkZcKa1GVsSvpVNC3FlTUk2QxjpmNSA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.1" + } + }, + "node_modules/@rspack/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.5.tgz", + "integrity": "sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@rspack/binding-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rspack/binding-win32-arm64-msvc": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.3.15.tgz", - "integrity": "sha512-7uJ7dWhO1nWXJiCss6Rslz8hoAxAhFpwpbWja3eHgRb7O4NPHg6MWw63AQSI2aFVakreenfu9yXQqYfpVWJ2dA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.5.4.tgz", + "integrity": "sha512-YxhK8dTv/6ff//C5Djm87TkiePuvGRoxLgsHgwR7C0rnA8lS5gLNwrNY9FjAY1x6WamnGGirFK97rigaeTDn+g==", "cpu": [ "arm64" ], @@ -13308,9 +13619,9 @@ ] }, "node_modules/@rspack/binding-win32-ia32-msvc": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.3.15.tgz", - "integrity": "sha512-UsaWTYCjDiSCB0A0qETgZk4QvhwfG8gCrO4SJvA+QSEWOmgSai1YV70prFtLLIiyT9mDt1eU3tPWl1UWPRU/EQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.5.4.tgz", + "integrity": "sha512-SU4EyAo1BI1zV/sSDF2cqoN+Qq6iIHLwtq0RJI5WQ4Yjn/mhhRFxNoerPCJUpPiiCxvG/IrpGzGi90MwFnMtNQ==", "cpu": [ "ia32" ], @@ -13322,9 +13633,9 @@ ] }, "node_modules/@rspack/binding-win32-x64-msvc": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.3.15.tgz", - "integrity": "sha512-ZnDIc9Es8EF94MirPDN+hOMt7tkb8nMEbRJFKLMmNd0ElNPgsql+1cY5SqyGRH1hsKB87KfSUQlhFiKZvzbfIg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.5.4.tgz", + "integrity": "sha512-xEgOCnD2FCUcxRgg3X5etq81vvf8rWwvPASfrG234diSduvU6zRiuiyYFMLTMDwQNEzZEFGHp7wIZNCKHudbng==", "cpu": [ "x64" ], @@ -13336,18 +13647,18 @@ ] }, "node_modules/@rspack/core": { - "version": "1.3.15", - "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.3.15.tgz", - "integrity": "sha512-QuElIC8jXSKWAp0LSx18pmbhA7NiA5HGoVYesmai90UVxz98tud0KpMxTVCg+0lrLrnKZfCWN9kwjCxM5pGnrA==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.5.4.tgz", + "integrity": "sha512-s/bVG+KRZjIpPP2f4TOQkJ/D+rql7HAV0MFEWoqoyeNnln/p6I28RYbw5zYF+Qg4J0swR8Qk2pbn7qlIdGusLQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime-tools": "0.14.3", - "@rspack/binding": "1.3.15", + "@module-federation/runtime-tools": "0.18.0", + "@rspack/binding": "1.5.4", "@rspack/lite-tapable": "1.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.12.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" @@ -13359,68 +13670,68 @@ } }, "node_modules/@rspack/core/node_modules/@module-federation/error-codes": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.14.3.tgz", - "integrity": "sha512-sBJ3XKU9g5Up31jFeXPFsD8AgORV7TLO/cCSMuRewSfgYbG/3vSKLJmfHrO6+PvjZSb9VyV2UaF02ojktW65vw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.18.0.tgz", + "integrity": "sha512-Woonm8ehyVIUPXChmbu80Zj6uJkC0dD9SJUZ/wOPtO8iiz/m+dkrOugAuKgoiR6qH4F+yorWila954tBz4uKsQ==", "dev": true, "license": "MIT" }, "node_modules/@rspack/core/node_modules/@module-federation/runtime": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.14.3.tgz", - "integrity": "sha512-7ZHpa3teUDVhraYdxQGkfGHzPbjna4LtwbpudgzAxSLLFxLDNanaxCuSeIgSM9c+8sVUNC9kvzUgJEZB0krPJw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.18.0.tgz", + "integrity": "sha512-+C4YtoSztM7nHwNyZl6dQKGUVJdsPrUdaf3HIKReg/GQbrt9uvOlUWo2NXMZ8vDAnf/QRrpSYAwXHmWDn9Obaw==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.14.3", - "@module-federation/runtime-core": "0.14.3", - "@module-federation/sdk": "0.14.3" + "@module-federation/error-codes": "0.18.0", + "@module-federation/runtime-core": "0.18.0", + "@module-federation/sdk": "0.18.0" } }, "node_modules/@rspack/core/node_modules/@module-federation/runtime-core": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.14.3.tgz", - "integrity": "sha512-xMFQXflLVW/AJTWb4soAFP+LB4XuhE7ryiLIX8oTyUoBBgV6U2OPghnFljPjeXbud72O08NYlQ1qsHw1kN/V8Q==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.18.0.tgz", + "integrity": "sha512-ZyYhrDyVAhUzriOsVfgL6vwd+5ebYm595Y13KeMf6TKDRoUHBMTLGQ8WM4TDj8JNsy7LigncK8C03fn97of0QQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/error-codes": "0.14.3", - "@module-federation/sdk": "0.14.3" + "@module-federation/error-codes": "0.18.0", + "@module-federation/sdk": "0.18.0" } }, "node_modules/@rspack/core/node_modules/@module-federation/runtime-tools": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.14.3.tgz", - "integrity": "sha512-QBETX7iMYXdSa3JtqFlYU+YkpymxETZqyIIRiqg0gW+XGpH3jgU68yjrme2NBJp7URQi/CFZG8KWtfClk0Pjgw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.18.0.tgz", + "integrity": "sha512-fSga9o4t1UfXNV/Kh6qFvRyZpPp3EHSPRISNeyT8ZoTpzDNiYzhtw0BPUSSD8m6C6XQh2s/11rI4g80UY+d+hA==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.14.3", - "@module-federation/webpack-bundler-runtime": "0.14.3" + "@module-federation/runtime": "0.18.0", + "@module-federation/webpack-bundler-runtime": "0.18.0" } }, "node_modules/@rspack/core/node_modules/@module-federation/sdk": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.14.3.tgz", - "integrity": "sha512-THJZMfbXpqjQOLblCQ8jjcBFFXsGRJwUWE9l/Q4SmuCSKMgAwie7yLT0qSGrHmyBYrsUjAuy+xNB4nfKP0pnGw==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.18.0.tgz", + "integrity": "sha512-Lo/Feq73tO2unjmpRfyyoUkTVoejhItXOk/h5C+4cistnHbTV8XHrW/13fD5e1Iu60heVdAhhelJd6F898Ve9A==", "dev": true, "license": "MIT" }, "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.14.3.tgz", - "integrity": "sha512-hIyJFu34P7bY2NeMIUHAS/mYUHEY71VTAsN0A0AqEJFSVPszheopu9VdXq0VDLrP9KQfuXT8SDxeYeJXyj0mgA==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.18.0.tgz", + "integrity": "sha512-TEvErbF+YQ+6IFimhUYKK3a5wapD90d90sLsNpcu2kB3QGT7t4nIluE25duXuZDVUKLz86tEPrza/oaaCWTpvQ==", "dev": true, "license": "MIT", "dependencies": { - "@module-federation/runtime": "0.14.3", - "@module-federation/sdk": "0.14.3" + "@module-federation/runtime": "0.18.0", + "@module-federation/sdk": "0.18.0" } }, "node_modules/@rspack/dev-server": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rspack/dev-server/-/dev-server-1.1.3.tgz", - "integrity": "sha512-jWPeyiZiGpbLYGhwHvwxhaa4rsr8CQvsWkWslqeMLb2uXwmyy3UWjUR1q+AhAPnf0gs3lZoFZ1hjBQVecHKUvg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rspack/dev-server/-/dev-server-1.1.4.tgz", + "integrity": "sha512-kGHYX2jYf3ZiHwVl0aUEPBOBEIG1aWleCDCAi+Jg32KUu3qr/zDUpCEd0wPuHfLEgk0X0xAEYCS6JMO7nBStNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13581,28 +13892,6 @@ } } }, - "node_modules/@rspack/dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/@rspack/lite-tapable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", @@ -13614,9 +13903,9 @@ } }, "node_modules/@rspack/plugin-react-refresh": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.4.3.tgz", - "integrity": "sha512-wZx4vWgy5oMEvgyNGd/oUKcdnKaccYWHCRkOqTdAPJC3WcytxhTX+Kady8ERurSBiLyQpoMiU3Iyd+F1Y2Arbw==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.5.1.tgz", + "integrity": "sha512-GT3KV1GSmIXO8dQg6taNf9AuZ8XHEs8cZqRn5mC2GT6DPCvUA/ZKezIGsHTyH+HMEbJnJ/T8yYeJnvnzuUcqAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13902,29 +14191,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/@tokenizer/inflate/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@tokenizer/inflate/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -14974,9 +15240,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", "bin": { @@ -14996,6 +15262,19 @@ "acorn-walk": "^8.0.2" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -15067,6 +15346,33 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -15224,6 +15530,13 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, "node_modules/archiver": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", @@ -15277,6 +15590,36 @@ "node": ">= 6" } }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -15501,9 +15844,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -16342,20 +16685,6 @@ "node": ">=18" } }, - "node_modules/cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -16577,7 +16906,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -16629,6 +16957,16 @@ "validator": "^13.9.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -16929,6 +17267,16 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -17170,6 +17518,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -18061,11 +18416,12 @@ "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -18076,6 +18432,12 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -18227,7 +18589,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/denque": { @@ -18431,6 +18793,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -20722,6 +21085,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -20884,6 +21268,23 @@ "node": ">= 6" } }, + "node_modules/glob-to-regex.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz", + "integrity": "sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -21132,6 +21533,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -21427,24 +21835,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/http-proxy-middleware/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/http-proxy-middleware/node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -21455,13 +21845,6 @@ "node": ">=0.10.0" } }, - "node_modules/http-proxy-middleware/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/http-server": { "version": "14.1.1", "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", @@ -21512,6 +21895,16 @@ "node": ">=10.17.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -21679,6 +22072,23 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -21943,25 +22353,6 @@ "node": ">=6" } }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -22018,6 +22409,13 @@ "node": ">=8" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -23392,9 +23790,9 @@ "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -23574,38 +23972,33 @@ } }, "node_modules/koa": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", - "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.0.1.tgz", + "integrity": "sha512-oDxVkRwPOHhGlxKIDiDB2h+/l05QPtefD7nSqRgDfZt8P+QVYFWjfeK8jANf5O2YXjk8egd7KntvXKYx82wOag==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.9.0", - "debug": "^4.3.2", + "accepts": "^1.3.8", + "content-disposition": "~0.5.4", + "content-type": "^1.0.5", + "cookies": "~0.9.1", "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", + "http-assert": "^1.5.0", + "http-errors": "^2.0.0", "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1", + "type-is": "^2.0.1", "vary": "^1.1.2" }, "engines": { - "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + "node": ">= 18" } }, "node_modules/koa-compose": { @@ -23615,53 +24008,60 @@ "dev": true, "license": "MIT" }, - "node_modules/koa-convert": { + "node_modules/koa/node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", - "dependencies": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - }, "engines": { - "node": ">= 10" + "node": ">= 0.8" } }, - "node_modules/koa/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "node_modules/koa/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/koa/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, "engines": { "node": ">= 0.6" } }, - "node_modules/koa/node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "node_modules/koa/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "dev": true, "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, "engines": { "node": ">= 0.6" } }, - "node_modules/koa/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "node_modules/koa/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, "engines": { "node": ">= 0.6" } @@ -25166,7 +25566,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, + "devOptional": true, "engines": { "node": ">= 0.6" } @@ -25676,6 +26076,23 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -26017,12 +26434,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", - "dev": true - }, "node_modules/open": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", @@ -26719,9 +27130,9 @@ } }, "node_modules/portfinder": { - "version": "1.0.37", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", - "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", "dev": true, "license": "MIT", "dependencies": { @@ -26732,31 +27143,6 @@ "node": ">= 10.12" } }, - "node_modules/portfinder/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/portfinder/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -27549,6 +27935,13 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, "node_modules/promise-retry": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", @@ -27754,9 +28147,9 @@ } }, "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "dev": true, "license": "MIT", "peer": true, @@ -27765,9 +28158,9 @@ } }, "node_modules/react-dom": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", - "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "dev": true, "license": "MIT", "peer": true, @@ -27775,7 +28168,7 @@ "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.1.1" } }, "node_modules/react-is": { @@ -28166,7 +28559,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, + "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -28233,44 +28626,21 @@ "node": ">= 18" } }, - "node_modules/router/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "node_modules/rslog": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rslog/-/rslog-1.2.11.tgz", + "integrity": "sha512-YgMMzQf6lL9q4rD9WS/lpPWxVNJ1ttY9+dOXJ0+7vJrKCAOT4GH0EiRnBi9mKOitcHiOwjqJPV1n/HRqqgZmOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/rslog": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rslog/-/rslog-1.2.11.tgz", - "integrity": "sha512-YgMMzQf6lL9q4rD9WS/lpPWxVNJ1ttY9+dOXJ0+7vJrKCAOT4GH0EiRnBi9mKOitcHiOwjqJPV1n/HRqqgZmOQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-applescript": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -29019,6 +29389,13 @@ "node": ">= 0.8" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -29208,7 +29585,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "devOptional": true }, "node_modules/sigstore": { "version": "3.1.0", @@ -29479,91 +29856,458 @@ "spdx-license-ids": "^3.0.0" } }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/sqlite3/node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sqlite3/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/sqlite3/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/sqlite3/node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sqlite3/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sqlite3/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlite3/node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sqlite3/node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/sqlite3/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/sqlite3/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, + "node_modules/sqlite3/node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", "license": "MIT", + "optional": true, "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" } }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "license": "MIT", + "node_modules/sqlite3/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": ">=6.0.0" + "node": ">=6" } }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, + "node_modules/sqlite3/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "license": "MIT", + "optional": true, "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, + "node_modules/sqlite3/node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", "license": "MIT", + "optional": true, "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" }, "engines": { - "node": ">= 6" + "node": ">= 10" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "node_modules/sqlite3/node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, "engines": { - "node": ">= 10.x" + "node": ">= 8" } }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "license": "BSD-3-Clause" + "node_modules/sqlite3/node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/sqlite3/node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/sqlite3/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true }, "node_modules/ssri": { "version": "12.0.0", @@ -29894,88 +30638,6 @@ "postcss": "^8.4.31" } }, - "node_modules/stylus": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.64.0.tgz", - "integrity": "sha512-ZIdT8eUv8tegmqy1tTIdJv9We2DumkNZFdCF5mz/Kpq3OcTaxSuCAYZge6HKK2CmNC02G1eJig2RV7XTw5hQrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "~4.3.3", - "debug": "^4.3.2", - "glob": "^10.4.5", - "sax": "~1.4.1", - "source-map": "^0.7.3" - }, - "bin": { - "stylus": "bin/stylus" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://opencollective.com/stylus" - } - }, - "node_modules/stylus-loader": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-7.1.3.tgz", - "integrity": "sha512-TY0SKwiY7D2kMd3UxaWKSf3xHF0FFN/FAfsSqfrhxRT/koXTwffq2cgEWDkLQz7VojMu7qEEHt5TlMjkPx9UDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.12", - "normalize-path": "^3.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "stylus": ">=0.52.4", - "webpack": "^5.0.0" - } - }, - "node_modules/stylus/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/stylus/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -30087,7 +30749,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, "license": "ISC", "dependencies": { "chownr": "^2.0.0", @@ -30102,9 +30763,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -30153,7 +30814,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -30166,7 +30826,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -30179,7 +30838,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=8" @@ -30189,7 +30847,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^3.0.0", @@ -30203,7 +30860,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -30216,7 +30872,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -30229,7 +30884,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC" }, "node_modules/tcp-port-used": { @@ -30416,14 +31070,18 @@ } }, "node_modules/thingies": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", - "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", + "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", "dev": true, - "license": "Unlicense", + "license": "MIT", "engines": { "node": ">=10.18" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, "peerDependencies": { "tslib": "^2" } @@ -30595,9 +31253,9 @@ } }, "node_modules/tree-dump": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", - "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -30635,17 +31293,17 @@ } }, "node_modules/ts-checker-rspack-plugin": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/ts-checker-rspack-plugin/-/ts-checker-rspack-plugin-1.1.4.tgz", - "integrity": "sha512-lDpKuAubxUlsonUE1LpZS5fw7tfjutNb0lwjAo0k8OcxpWv/q18ytaD6eZXdjrFdTEFNIHtKp9dNkUKGky8SgA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ts-checker-rspack-plugin/-/ts-checker-rspack-plugin-1.1.5.tgz", + "integrity": "sha512-jla7C8ENhRP87i2iKo8jLMOvzyncXou12odKe0CPTkCaI9l8Eaiqxflk/ML3+1Y0j+gKjMk2jb6swHYtlpdRqg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.16.7", - "@rspack/lite-tapable": "^1.0.0", - "chokidar": "^3.5.3", + "@babel/code-frame": "^7.27.1", + "@rspack/lite-tapable": "^1.0.1", + "chokidar": "^3.6.0", "is-glob": "^4.0.3", - "memfs": "^4.14.0", + "memfs": "^4.28.0", "minimatch": "^9.0.5", "picocolors": "^1.1.1" }, @@ -30688,15 +31346,17 @@ } }, "node_modules/ts-checker-rspack-plugin/node_modules/memfs": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", - "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.39.0.tgz", + "integrity": "sha512-tFRr2IkSXl2B6IAJsxjHIMTOsfLt9W+8+t2uNxCeQcz4tFqgQR8DYk8hlLH2HsucTctLuoHq3U0G08atyBE3yw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/json-pack": "^1.0.3", - "@jsonjoy.com/util": "^1.3.0", - "tree-dump": "^1.0.1", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", "tslib": "^2.0.0" }, "engines": { @@ -30793,9 +31453,9 @@ } }, "node_modules/ts-loader": { - "version": "9.5.2", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz", - "integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==", + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", "dev": true, "license": "MIT", "dependencies": { @@ -30872,15 +31532,16 @@ } }, "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-fw/7265mIWukrSHd0i+wSwx64kYUSAKPfxRDksjKIYTxSAp9W9/xcZVBF4Kl0eqQd5eBpAQ/oQrc5RyM/0c1GQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.2.0.tgz", + "integrity": "sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", - "tsconfig-paths": "^4.0.0" + "tapable": "^2.2.1", + "tsconfig-paths": "^4.1.2" }, "engines": { "node": ">=10.13.0" @@ -30926,31 +31587,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/tuf-js/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/tuf-js/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -32057,10 +32693,11 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -32239,6 +32876,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -32450,16 +33097,6 @@ "node": ">=12" } }, - "node_modules/ylru": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", - "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index d55f408fd..959c4e3ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coding-box", - "version": "1.0.0", + "version": "1.1.0", "author": "IQB - Institut zur Qualitätsentwicklung im Bildungswesen", "license": "MIT", "scripts": { @@ -30,9 +30,10 @@ "@angular/platform-browser": "20.0.3", "@angular/platform-browser-dynamic": "20.0.3", "@angular/router": "20.0.3", - "@iqb/responses": "^3.6.0", - "@iqbspecs/response": "1.4.0", + "@iqb/responses": "^4.0.2", + "@iqbspecs/response": "1.5.1", "@iqbspecs/variable-info": "1.3.0", + "@iqbspecs/coding-scheme": "^3.2.0", "@nestjs-modules/ioredis": "^2.0.2", "@nestjs/axios": "^4.0.1", "@nestjs/bull": "^11.0.3", @@ -54,12 +55,13 @@ "adm-zip": "^0.5.9", "ajv": "^8.17.1", "ajv-keywords": "^5.1.0", - "axios": "^1.3.1", + "axios": "^1.12.2", "bull": "^4.16.5", "cheerio": "^1.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "docx": "^9.5.1", + "domhandler": "^5.0.3", "exceljs": "^4.4.0", "fast-csv": "^5.0.1", "file-saver-es": "^2.0.5", @@ -74,6 +76,7 @@ "pg": "^8.11.5", "reflect-metadata": "^0.2.0", "rxjs": "~7.8.0", + "sqlite3": "^5.1.7", "stream": "^0.0.2", "timers": "^0.1.1", "tslib": "^2.3.0", @@ -89,7 +92,7 @@ "@angular/compiler-cli": "20.0.3", "@golevelup/ts-jest": "^0.5.0", "@iqb/eslint-config": "^2.1.1", - "@nx/angular": "21.3.1", + "@nx/angular": "21.5.2", "@nx/cypress": "21.2.0", "@nx/esbuild": "21.2.0", "@nx/eslint": "21.2.0",