From d4de0184f011742a6ceaca921bc1423a626f847e Mon Sep 17 00:00:00 2001
From: jurei733 <67505990+jurei733@users.noreply.github.com>
Date: Wed, 16 Jul 2025 07:08:15 +0200
Subject: [PATCH 01/21] Modify the `WorkspaceCodingService` to process test
persons in batches
---
.../services/workspace-coding.service.ts | 88 +++++++++++++++----
1 file changed, 69 insertions(+), 19 deletions(-)
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 59941a0ee..4fc70ec48 100644
--- a/apps/backend/src/app/database/services/workspace-coding.service.ts
+++ b/apps/backend/src/app/database/services/workspace-coding.service.ts
@@ -276,29 +276,79 @@ export class WorkspaceCodingService {
job.progress = 0;
await this.jobRepository.save(job);
- // Clone the implementation of codeTestPersons but with progress tracking
- const result = await this.processTestPersonsBatch(workspace_id, personIds, async progress => {
- // Update job progress
- try {
- // Get the latest job status
- const currentJob = await this.jobRepository.findOne({ where: { id: jobId } });
- if (!currentJob) {
- this.logger.error(`Job with ID ${jobId} not found when updating progress`);
- return;
+ // Process test persons in smaller batches to avoid memory issues
+ const BATCH_SIZE = 500;
+ const totalPersons = personIds.length;
+ let processedPersons = 0;
+ const combinedResult: CodingStatistics = { totalResponses: 0, statusCounts: {} };
+
+ this.logger.log(`Processing ${totalPersons} test persons in batches of ${BATCH_SIZE}`);
+
+ // Process each batch sequentially
+ for (let i = 0; i < personIds.length; i += BATCH_SIZE) {
+ const currentJobStatus = await this.jobRepository.findOne({ where: { id: jobId } });
+ if (!currentJobStatus || currentJobStatus.status === 'cancelled' || currentJobStatus.status === 'paused') {
+ this.logger.log(`Job ${jobId} was ${currentJobStatus ? currentJobStatus.status : 'cancelled'} before processing batch ${(i / BATCH_SIZE) + 1}`);
+ return;
+ }
+
+ const batchPersonIds = personIds.slice(i, i + BATCH_SIZE);
+ const batchNumber = (i / BATCH_SIZE) + 1;
+ const totalBatches = Math.ceil(totalPersons / BATCH_SIZE);
+ this.logger.log(`Processing batch ${batchNumber} of ${totalBatches} (${batchPersonIds.length} persons)`);
+
+ // Capture the current processed count for this batch's progress calculation
+ const currentProcessedCount = processedPersons;
+
+ const batchResult = await this.processTestPersonsBatch(workspace_id, batchPersonIds, async progress => {
+ // Calculate overall progress based on completed batches and current batch progress
+ const overallProgress = Math.min(
+ Math.floor(((currentProcessedCount + (batchPersonIds.length * (progress / 100))) / totalPersons) * 100),
+ 99 // Cap at 99% until fully complete
+ );
+
+ // Update job progress
+ try {
+ const currentJob = await this.jobRepository.findOne({ where: { id: jobId } });
+ if (!currentJob) {
+ this.logger.error(`Job with ID ${jobId} not found when updating progress`);
+ return;
+ }
+
+ if (currentJob.status === 'cancelled' || currentJob.status === 'paused') {
+ return;
+ }
+
+ // Update progress
+ currentJob.progress = overallProgress;
+ await this.jobRepository.save(currentJob);
+ } catch (error) {
+ this.logger.error(`Error updating job progress: ${error.message}`, error.stack);
}
+ }, jobId.toString());
- // Don't update if job has been cancelled or paused
- if (currentJob.status === 'cancelled' || currentJob.status === 'paused') {
- return;
+ // Merge batch results into combined results
+ combinedResult.totalResponses += batchResult.totalResponses;
+
+ // Merge status counts
+ Object.entries(batchResult.statusCounts).forEach(([status, count]) => {
+ if (!combinedResult.statusCounts[status]) {
+ combinedResult.statusCounts[status] = 0;
}
+ combinedResult.statusCounts[status] += count;
+ });
- // Update progress
- currentJob.progress = progress;
- await this.jobRepository.save(currentJob);
- } catch (error) {
- this.logger.error(`Error updating job progress: ${error.message}`, error.stack);
+ // Update processed count
+ processedPersons += batchPersonIds.length;
+
+ // Force garbage collection between batches if available
+ if (global.gc) {
+ this.logger.log('Forcing garbage collection between batches');
+ global.gc();
}
- }, jobId.toString());
+ }
+
+ // Use the combined result from all batches
// Check if job was cancelled during processing
const currentJob = await this.jobRepository.findOne({ where: { id: jobId } });
@@ -315,7 +365,7 @@ export class WorkspaceCodingService {
// Update job status to completed with result
currentJob.status = 'completed';
currentJob.progress = 100;
- currentJob.result = JSON.stringify(result);
+ currentJob.result = JSON.stringify(combinedResult);
// Calculate and store job duration if it's a TestPersonCodingJob
if (currentJob.type === 'test-person-coding' && currentJob.created_at) {
From e698f0330cb1361c1c3d619404cfa7d8c3d4d7ad Mon Sep 17 00:00:00 2001
From: jurei733 <67505990+jurei733@users.noreply.github.com>
Date: Wed, 16 Jul 2025 13:21:36 +0200
Subject: [PATCH 02/21] Run validation tasks in background
---
apps/backend/src/app/admin/admin.module.ts | 4 +-
.../workspace/dto/validation-task.dto.ts | 32 +
.../workspace/validation-task.controller.ts | 81 +
.../src/app/database/database.module.ts | 13 +-
.../entities/validation-task.entity.ts | 33 +
.../services/validation-task.service.ts | 182 ++
.../src/app/models/validation-task.dto.ts | 12 +
.../src/app/services/backend.service.ts | 41 +
.../services/validation-task-state.service.ts | 51 +
.../src/app/services/validation.service.ts | 189 +-
.../test-results/test-results.component.html | 8 +-
.../test-results/test-results.component.scss | 15 +
.../test-results/test-results.component.ts | 127 +-
.../validation-dialog.component.html | 114 +
.../validation-dialog.component.ts | 2230 ++++++++++++++++-
.../changelog/coding-box.changelog-0.9.2.sql | 18 +
.../changelog/coding-box.changelog-root.xml | 1 +
17 files changed, 3045 insertions(+), 106 deletions(-)
create mode 100644 apps/backend/src/app/admin/workspace/dto/validation-task.dto.ts
create mode 100644 apps/backend/src/app/admin/workspace/validation-task.controller.ts
create mode 100644 apps/backend/src/app/database/entities/validation-task.entity.ts
create mode 100644 apps/backend/src/app/database/services/validation-task.service.ts
create mode 100644 apps/frontend/src/app/models/validation-task.dto.ts
create mode 100644 apps/frontend/src/app/services/validation-task-state.service.ts
create mode 100644 database/changelog/coding-box.changelog-0.9.2.sql
diff --git a/apps/backend/src/app/admin/admin.module.ts b/apps/backend/src/app/admin/admin.module.ts
index 6b5aa060d..96d427f94 100755
--- a/apps/backend/src/app/admin/admin.module.ts
+++ b/apps/backend/src/app/admin/admin.module.ts
@@ -17,6 +17,7 @@ import { ResourcePackageController } from './resource-packages/resource-package.
import { JournalController } from './workspace/journal.controller';
import { VariableAnalysisController } from './variable-analysis/variable-analysis.controller';
import { JobsController } from './jobs/jobs.controller';
+import { ValidationTaskController } from './workspace/validation-task.controller';
@Module({
imports: [
@@ -39,7 +40,8 @@ import { JobsController } from './jobs/jobs.controller';
ResourcePackageController,
JournalController,
VariableAnalysisController,
- JobsController
+ JobsController,
+ ValidationTaskController
],
providers: []
})
diff --git a/apps/backend/src/app/admin/workspace/dto/validation-task.dto.ts b/apps/backend/src/app/admin/workspace/dto/validation-task.dto.ts
new file mode 100644
index 000000000..853b4b5d7
--- /dev/null
+++ b/apps/backend/src/app/admin/workspace/dto/validation-task.dto.ts
@@ -0,0 +1,32 @@
+import { ValidationTask } from '../../../database/entities/validation-task.entity';
+
+export class ValidationTaskDto {
+ id: number;
+ workspace_id: number;
+ validation_type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'deleteResponses' | 'deleteAllResponses';
+ status: 'pending' | 'processing' | 'completed' | 'failed';
+ progress?: number;
+ error?: string;
+ page?: number;
+ limit?: number;
+ created_at: Date;
+ updated_at: Date;
+
+ /**
+ * Convert a ValidationTask entity to a ValidationTaskDto
+ */
+ static fromEntity(entity: ValidationTask): ValidationTaskDto {
+ const dto = new ValidationTaskDto();
+ dto.id = entity.id;
+ dto.workspace_id = entity.workspace_id;
+ dto.validation_type = entity.validation_type;
+ dto.status = entity.status as 'pending' | 'processing' | 'completed' | 'failed';
+ dto.progress = entity.progress;
+ dto.error = entity.error;
+ dto.page = entity.page;
+ dto.limit = entity.limit;
+ dto.created_at = entity.created_at;
+ dto.updated_at = entity.updated_at;
+ return dto;
+ }
+}
diff --git a/apps/backend/src/app/admin/workspace/validation-task.controller.ts b/apps/backend/src/app/admin/workspace/validation-task.controller.ts
new file mode 100644
index 000000000..01bfa266a
--- /dev/null
+++ b/apps/backend/src/app/admin/workspace/validation-task.controller.ts
@@ -0,0 +1,81 @@
+import {
+ Controller,
+ Post,
+ Get,
+ Param,
+ Query,
+ ParseIntPipe,
+ Logger
+} from '@nestjs/common';
+import {
+ ApiTags,
+ ApiOperation,
+ ApiParam,
+ ApiQuery
+} from '@nestjs/swagger';
+import { ValidationTaskService } from '../../database/services/validation-task.service';
+import { ValidationTaskDto } from './dto/validation-task.dto';
+import { WorkspaceId } from './workspace.decorator';
+
+@ApiTags('Validation Tasks')
+@Controller('admin/workspace/:workspace_id/validation-tasks')
+export class ValidationTaskController {
+ private readonly logger = new Logger(ValidationTaskController.name);
+
+ constructor(private readonly validationTaskService: ValidationTaskService) {}
+
+ @Post()
+ @ApiOperation({ summary: 'Create a new validation task' })
+ @ApiParam({ name: 'workspace_id', description: 'Workspace ID' })
+ @ApiQuery({ name: 'type', description: 'Validation type', required: true })
+ @ApiQuery({ name: 'page', description: 'Page number', required: false })
+ @ApiQuery({ name: 'limit', description: 'Page size', required: false })
+ async createValidationTask(
+ @WorkspaceId() workspaceId: number,
+ @Query('type') type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'deleteResponses' | 'deleteAllResponses',
+ @Query('page') page?: number,
+ @Query('limit') limit?: number
+ ): Promise {
+ this.logger.log(`Creating validation task of type ${type} for workspace ${workspaceId}`);
+ const task = await this.validationTaskService.createValidationTask(
+ workspaceId,
+ type,
+ page,
+ limit
+ );
+ return ValidationTaskDto.fromEntity(task);
+ }
+
+ @Get()
+ @ApiOperation({ summary: 'Get all validation tasks for a workspace' })
+ @ApiParam({ name: 'workspace_id', description: 'Workspace ID' })
+ async getValidationTasks(
+ @WorkspaceId() workspaceId: number
+ ): Promise {
+ const tasks = await this.validationTaskService.getValidationTasks(workspaceId);
+ return tasks.map(task => ValidationTaskDto.fromEntity(task));
+ }
+
+ @Get(':id')
+ @ApiOperation({ summary: 'Get a validation task by ID' })
+ @ApiParam({ name: 'workspace_id', description: 'Workspace ID' })
+ @ApiParam({ name: 'id', description: 'Task ID' })
+ async getValidationTask(
+ @WorkspaceId() workspaceId: number,
+ @Param('id', ParseIntPipe) taskId: number
+ ): Promise {
+ const task = await this.validationTaskService.getValidationTask(taskId, workspaceId);
+ return ValidationTaskDto.fromEntity(task);
+ }
+
+ @Get(':id/results')
+ @ApiOperation({ summary: 'Get the results of a validation task' })
+ @ApiParam({ name: 'workspace_id', description: 'Workspace ID' })
+ @ApiParam({ name: 'id', description: 'Task ID' })
+ async getValidationResults(
+ @WorkspaceId() workspaceId: number,
+ @Param('id', ParseIntPipe) taskId: number
+ ): Promise {
+ return this.validationTaskService.getValidationResults(taskId, workspaceId);
+ }
+}
diff --git a/apps/backend/src/app/database/database.module.ts b/apps/backend/src/app/database/database.module.ts
index 4559abf54..62c5a3af2 100755
--- a/apps/backend/src/app/database/database.module.ts
+++ b/apps/backend/src/app/database/database.module.ts
@@ -40,9 +40,11 @@ import { JournalEntry } from './entities/journal-entry.entity';
import { JournalService } from './services/journal.service';
import { VariableAnalysisService } from './services/variable-analysis.service';
import { JobService } from './services/job.service';
+import { ValidationTaskService } from './services/validation-task.service';
import { Job } from './entities/job.entity';
import { VariableAnalysisJob } from './entities/variable-analysis-job.entity';
import { TestPersonCodingJob } from './entities/test-person-coding-job.entity';
+import { ValidationTask } from './entities/validation-task.entity';
@Module({
imports: [
@@ -72,7 +74,7 @@ import { TestPersonCodingJob } from './entities/test-person-coding-job.entity';
password: configService.get('POSTGRES_PASSWORD'),
database: configService.get('POSTGRES_DB'),
entities: [BookletInfo, Booklet, Session, BookletLog, Unit, UnitLog, UnitLastState, ResponseEntity,
- User, Workspace, WorkspaceAdmin, FileUpload, WorkspaceUser, ResourcePackage, Logs, Persons, ChunkEntity, BookletLog, Session, UnitLog, UnitTag, UnitNote, JournalEntry, Job, VariableAnalysisJob, TestPersonCodingJob
+ User, Workspace, WorkspaceAdmin, FileUpload, WorkspaceUser, ResourcePackage, Logs, Persons, ChunkEntity, BookletLog, Session, UnitLog, UnitTag, UnitNote, JournalEntry, Job, VariableAnalysisJob, TestPersonCodingJob, ValidationTask
],
synchronize: false
}),
@@ -101,7 +103,8 @@ import { TestPersonCodingJob } from './entities/test-person-coding-job.entity';
JournalEntry,
Job,
VariableAnalysisJob,
- TestPersonCodingJob
+ TestPersonCodingJob,
+ ValidationTask
])
],
providers: [
@@ -122,7 +125,8 @@ import { TestPersonCodingJob } from './entities/test-person-coding-job.entity';
ResourcePackageService,
JournalService,
VariableAnalysisService,
- JobService
+ JobService,
+ ValidationTaskService
],
exports: [
User,
@@ -149,7 +153,8 @@ import { TestPersonCodingJob } from './entities/test-person-coding-job.entity';
UnitNoteService,
JournalService,
VariableAnalysisService,
- JobService
+ JobService,
+ ValidationTaskService
]
})
export class DatabaseModule {}
diff --git a/apps/backend/src/app/database/entities/validation-task.entity.ts b/apps/backend/src/app/database/entities/validation-task.entity.ts
new file mode 100644
index 000000000..9a84950e2
--- /dev/null
+++ b/apps/backend/src/app/database/entities/validation-task.entity.ts
@@ -0,0 +1,33 @@
+import {
+ Column,
+ ChildEntity
+} from 'typeorm';
+import { Job } from './job.entity';
+
+/**
+ * Entity for validation tasks
+ */
+@ChildEntity('validation-task')
+export class ValidationTask extends Job {
+ /**
+ * Type of validation to perform
+ * - 'variables': Validate if variables are defined in the Unit.xml
+ * - 'variableTypes': Validate if variable values match their defined types
+ * - 'responseStatus': Validate if response status is valid
+ * - 'testTakers': Validate if test takers exist in TestTakers XML files
+ * - 'groupResponses': Validate if responses exist for all test person groups
+ * - 'deleteResponses': Delete specific invalid responses
+ * - 'deleteAllResponses': Delete all invalid responses of a specific type
+ */
+ @Column()
+ validation_type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'deleteResponses' | 'deleteAllResponses';
+
+ /**
+ * Pagination parameters for paginated results
+ */
+ @Column({ nullable: true })
+ page?: number;
+
+ @Column({ nullable: true })
+ limit?: number;
+}
diff --git a/apps/backend/src/app/database/services/validation-task.service.ts b/apps/backend/src/app/database/services/validation-task.service.ts
new file mode 100644
index 000000000..8df1f7266
--- /dev/null
+++ b/apps/backend/src/app/database/services/validation-task.service.ts
@@ -0,0 +1,182 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { ValidationTask } from '../entities/validation-task.entity';
+import { WorkspaceFilesService } from './workspace-files.service';
+
+@Injectable()
+export class ValidationTaskService {
+ private readonly logger = new Logger(ValidationTaskService.name);
+
+ constructor(
+ @InjectRepository(ValidationTask)
+ private taskRepository: Repository,
+ private validationService: WorkspaceFilesService
+ ) {}
+
+ async createValidationTask(
+ workspaceId: number,
+ validationType: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'deleteResponses' | 'deleteAllResponses',
+ page?: number,
+ limit?: number,
+ additionalData?: Record
+ ): Promise {
+ const task = this.taskRepository.create({
+ workspace_id: workspaceId,
+ validation_type: validationType,
+ page: page,
+ limit: limit,
+ status: 'pending',
+ result: additionalData ? JSON.stringify(additionalData) : undefined
+ });
+
+ const savedTask = await this.taskRepository.save(task);
+ this.logger.log(`Created validation task with ID ${savedTask.id}`);
+
+ this.processValidationTask(savedTask.id).catch(error => {
+ this.logger.error(`Error processing task ${savedTask.id}: ${error.message}`, error.stack);
+ });
+
+ return savedTask;
+ }
+
+ async getValidationTask(taskId: number, workspaceId?: number): Promise {
+ const whereClause: { id: number; workspace_id?: number } = { id: taskId };
+
+ if (workspaceId !== undefined) {
+ whereClause.workspace_id = workspaceId;
+ }
+
+ const task = await this.taskRepository.findOne({ where: whereClause });
+ if (!task) {
+ if (workspaceId !== undefined) {
+ throw new Error(`Task with ID ${taskId} not found in workspace ${workspaceId}`);
+ } else {
+ throw new Error(`Task with ID ${taskId} not found`);
+ }
+ }
+ return task;
+ }
+
+ async getValidationTasks(workspaceId: number): Promise {
+ return this.taskRepository.find({
+ where: { workspace_id: workspaceId },
+ order: { created_at: 'DESC' }
+ });
+ }
+
+ async getValidationResults(taskId: number, workspaceId?: number): Promise {
+ const task = await this.getValidationTask(taskId, workspaceId);
+
+ if (task.status !== 'completed') {
+ throw new Error(`Task with ID ${taskId} is not completed (status: ${task.status})`);
+ }
+
+ if (!task.result) {
+ throw new Error(`Task with ID ${taskId} has no results`);
+ }
+
+ try {
+ return JSON.parse(task.result);
+ } catch (error) {
+ this.logger.error(`Error parsing results for task ${taskId}: ${error.message}`, error.stack);
+ throw new Error(`Error parsing results for task ${taskId}`);
+ }
+ }
+
+ private async processValidationTask(taskId: number): Promise {
+ try {
+ const task = await this.getValidationTask(taskId);
+
+ task.status = 'processing';
+ await this.taskRepository.save(task);
+
+ let result: unknown;
+ let taskData: Record | null = null;
+
+ if (task.result) {
+ try {
+ taskData = JSON.parse(task.result);
+ } catch (error) {
+ this.logger.error(`Error parsing task data for task ${taskId}: ${error.message}`, error.stack);
+ }
+ }
+
+ switch (task.validation_type) {
+ case 'variables':
+ result = await this.validationService.validateVariables(
+ task.workspace_id,
+ task.page || 1,
+ task.limit || 10
+ );
+ break;
+ case 'variableTypes':
+ result = await this.validationService.validateVariableTypes(
+ task.workspace_id,
+ task.page || 1,
+ task.limit || 10
+ );
+ break;
+ case 'responseStatus':
+ result = await this.validationService.validateResponseStatus(
+ task.workspace_id,
+ task.page || 1,
+ task.limit || 10
+ );
+ break;
+ case 'testTakers':
+ result = await this.validationService.validateTestTakers(task.workspace_id);
+ break;
+ case 'groupResponses':
+ result = await this.validationService.validateGroupResponses(
+ task.workspace_id,
+ task.page || 1,
+ task.limit || 10
+ );
+ break;
+ case 'deleteResponses':
+ if (taskData && Array.isArray(taskData.responseIds)) {
+ const responseIds = taskData.responseIds as number[];
+ const deletedCount = await this.validationService.deleteInvalidResponses(
+ task.workspace_id,
+ responseIds
+ );
+ result = { deletedCount };
+ } else {
+ throw new Error('No response IDs provided for deletion');
+ }
+ break;
+ case 'deleteAllResponses':
+ if (taskData && typeof taskData.validationType === 'string') {
+ const validationType = taskData.validationType as 'variables' | 'variableTypes' | 'responseStatus';
+ const deletedCount = await this.validationService.deleteAllInvalidResponses(
+ task.workspace_id,
+ validationType
+ );
+ result = { deletedCount };
+ } else {
+ throw new Error('No validation type provided for deletion');
+ }
+ break;
+ default:
+ throw new Error(`Unknown validation type: ${task.validation_type}`);
+ }
+
+ task.result = JSON.stringify(result);
+ task.status = 'completed';
+ await this.taskRepository.save(task);
+
+ this.logger.log(`Completed validation task with ID ${taskId}`);
+ } catch (error) {
+ try {
+ const task = await this.getValidationTask(taskId);
+ task.error = error.message;
+ task.status = 'failed';
+ await this.taskRepository.save(task);
+ } catch (innerError) {
+ this.logger.error(`Failed to update task ${taskId} with error: ${innerError.message}`, innerError.stack);
+ }
+ this.logger.error(`Failed to process task ${taskId}: ${error.message}`, error.stack);
+ }
+ }
+}
diff --git a/apps/frontend/src/app/models/validation-task.dto.ts b/apps/frontend/src/app/models/validation-task.dto.ts
new file mode 100644
index 000000000..7a685ed17
--- /dev/null
+++ b/apps/frontend/src/app/models/validation-task.dto.ts
@@ -0,0 +1,12 @@
+export interface ValidationTaskDto {
+ id: number;
+ workspace_id: number;
+ validation_type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'deleteResponses' | 'deleteAllResponses';
+ status: 'pending' | 'processing' | 'completed' | 'failed';
+ progress?: number;
+ error?: string;
+ page?: number;
+ limit?: number;
+ created_at: Date;
+ updated_at: Date;
+}
diff --git a/apps/frontend/src/app/services/backend.service.ts b/apps/frontend/src/app/services/backend.service.ts
index fb744fd6c..28e1257af 100755
--- a/apps/frontend/src/app/services/backend.service.ts
+++ b/apps/frontend/src/app/services/backend.service.ts
@@ -26,6 +26,7 @@ import { ImportService } from './import.service';
import { AuthenticationService } from './authentication.service';
import { VariableAnalysisService, VariableAnalysisResultDto } from './variable-analysis.service';
import { VariableAnalysisJobDto } from '../models/variable-analysis-job.dto';
+import { ValidationTaskDto } from '../models/validation-task.dto';
import { FilesDto } from '../../../../../api-dto/files/files.dto';
import { CreateUnitNoteDto } from '../../../../../api-dto/unit-notes/create-unit-note.dto';
import { WorkspaceFullDto } from '../../../../../api-dto/workspaces/workspace-full-dto';
@@ -580,4 +581,44 @@ export class BackendService {
cancelVariableAnalysisJob(workspaceId: number, jobId: number): Observable<{ success: boolean; message: string }> {
return this.variableAnalysisService.cancelJob(workspaceId, jobId);
}
+
+ createValidationTask(
+ workspaceId: number,
+ type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'deleteResponses' | 'deleteAllResponses',
+ page?: number,
+ limit?: number,
+ additionalData?: Record
+ ): Observable {
+ return this.validationService.createValidationTask(workspaceId, type, page, limit, additionalData);
+ }
+
+ createDeleteResponsesTask(
+ workspaceId: number,
+ responseIds: number[]
+ ): Observable {
+ return this.validationService.createDeleteResponsesTask(workspaceId, responseIds);
+ }
+
+ createDeleteAllResponsesTask(
+ workspaceId: number,
+ validationType: 'variables' | 'variableTypes' | 'responseStatus'
+ ): Observable {
+ return this.validationService.createDeleteAllResponsesTask(workspaceId, validationType);
+ }
+
+ getValidationTask(workspaceId: number, taskId: number): Observable {
+ return this.validationService.getValidationTask(workspaceId, taskId);
+ }
+
+ getValidationResults(workspaceId: number, taskId: number): Observable {
+ return this.validationService.getValidationResults(workspaceId, taskId);
+ }
+
+ pollValidationTask(
+ workspaceId: number,
+ taskId: number,
+ pollInterval: number = 2000
+ ): Observable {
+ return this.validationService.pollValidationTask(workspaceId, taskId, pollInterval);
+ }
}
diff --git a/apps/frontend/src/app/services/validation-task-state.service.ts b/apps/frontend/src/app/services/validation-task-state.service.ts
new file mode 100644
index 000000000..f0878cfda
--- /dev/null
+++ b/apps/frontend/src/app/services/validation-task-state.service.ts
@@ -0,0 +1,51 @@
+import { Injectable } from '@angular/core';
+
+// Define interfaces for validation results
+export interface ValidationResult {
+ status: 'success' | 'failed' | 'not-run';
+ timestamp: number;
+ details?: unknown;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class ValidationTaskStateService {
+ // Store task IDs by workspace ID and validation type
+ private activeTasks: Record> = {};
+
+ // Store validation results by workspace ID and validation type
+ private validationResults: Record> = {};
+
+ setTaskId(workspaceId: number, type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses', taskId: number): void {
+ if (!this.activeTasks[workspaceId]) {
+ this.activeTasks[workspaceId] = {};
+ }
+ this.activeTasks[workspaceId][type] = taskId;
+ }
+
+ removeTaskId(workspaceId: number, type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'): void {
+ if (this.activeTasks[workspaceId]) {
+ delete this.activeTasks[workspaceId][type];
+ }
+ }
+
+ getAllTaskIds(workspaceId: number): Record {
+ return this.activeTasks[workspaceId] || {};
+ }
+
+ setValidationResult(
+ workspaceId: number,
+ type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses',
+ result: ValidationResult
+ ): void {
+ if (!this.validationResults[workspaceId]) {
+ this.validationResults[workspaceId] = {};
+ }
+ this.validationResults[workspaceId][type] = result;
+ }
+
+ getAllValidationResults(workspaceId: number): Record {
+ return this.validationResults[workspaceId] || {};
+ }
+}
diff --git a/apps/frontend/src/app/services/validation.service.ts b/apps/frontend/src/app/services/validation.service.ts
index d61ce64fb..a3e83842a 100644
--- a/apps/frontend/src/app/services/validation.service.ts
+++ b/apps/frontend/src/app/services/validation.service.ts
@@ -3,11 +3,17 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import {
catchError,
Observable,
- of
+ of,
+ interval,
+ switchMap,
+ takeWhile,
+ map,
+ forkJoin
} from 'rxjs';
import { InvalidVariableDto } from '../../../../../api-dto/files/variable-validation.dto';
import { TestTakersValidationDto } from '../../../../../api-dto/files/testtakers-validation.dto';
import { SERVER_URL } from '../injection-tokens';
+import { ValidationTaskDto } from '../models/validation-task.dto';
interface PaginatedResponse {
data: T[];
@@ -149,4 +155,185 @@ export class ValidationService {
catchError(() => of(0))
);
}
+
+ createValidationTask(
+ workspaceId: number,
+ type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses' | 'deleteResponses' | 'deleteAllResponses',
+ page?: number,
+ limit?: number,
+ additionalData?: Record
+ ): Observable {
+ let params = new HttpParams().set('type', type);
+
+ if (page) {
+ params = params.set('page', page.toString());
+ }
+
+ if (limit) {
+ params = params.set('limit', limit.toString());
+ }
+
+ // Add additional data as query parameters if provided
+ if (additionalData) {
+ Object.entries(additionalData).forEach(([key, value]) => {
+ if (value !== undefined && value !== null) {
+ if (Array.isArray(value)) {
+ params = params.set(key, value.join(','));
+ } else {
+ params = params.set(key, String(value));
+ }
+ }
+ });
+ }
+
+ return this.http.post(
+ `${this.serverUrl}admin/workspace/${workspaceId}/validation-tasks`,
+ null,
+ { headers: this.authHeader, params }
+ ).pipe(
+ catchError(error => {
+ console.error(`Error creating validation task: ${error.message}`);
+ throw error;
+ })
+ );
+ }
+
+ createDeleteResponsesTask(
+ workspaceId: number,
+ responseIds: number[]
+ ): Observable {
+ return this.createValidationTask(
+ workspaceId,
+ 'deleteResponses',
+ undefined,
+ undefined,
+ { responseIds }
+ );
+ }
+
+ createDeleteAllResponsesTask(
+ workspaceId: number,
+ validationType: 'variables' | 'variableTypes' | 'responseStatus'
+ ): Observable {
+ return this.createValidationTask(
+ workspaceId,
+ 'deleteAllResponses',
+ undefined,
+ undefined,
+ { validationType }
+ );
+ }
+
+ getValidationTask(workspaceId: number, taskId: number): Observable {
+ return this.http.get(
+ `${this.serverUrl}admin/workspace/${workspaceId}/validation-tasks/${taskId}`,
+ { headers: this.authHeader }
+ ).pipe(
+ catchError(error => {
+ console.error(`Error getting validation task: ${error.message}`);
+ throw error;
+ })
+ );
+ }
+
+ getValidationTasks(workspaceId: number): Observable {
+ return this.http.get(
+ `${this.serverUrl}admin/workspace/${workspaceId}/validation-tasks`,
+ { headers: this.authHeader }
+ ).pipe(
+ catchError(error => {
+ console.error(`Error getting validation tasks: ${error.message}`);
+ throw error;
+ })
+ );
+ }
+
+ getValidationResults(workspaceId: number, taskId: number): Observable {
+ return this.http.get(
+ `${this.serverUrl}admin/workspace/${workspaceId}/validation-tasks/${taskId}/results`,
+ { headers: this.authHeader }
+ ).pipe(
+ catchError(error => {
+ console.error(`Error getting validation results: ${error.message}`);
+ throw error;
+ })
+ );
+ }
+
+ pollValidationTask(
+ workspaceId: number,
+ taskId: number,
+ pollInterval: number = 2000
+ ): Observable {
+ return interval(pollInterval).pipe(
+ switchMap(() => this.getValidationTask(workspaceId, taskId)),
+ takeWhile(task => task.status === 'pending' || task.status === 'processing', true)
+ );
+ }
+
+ getLastValidationResults(
+ workspaceId: number
+ ): Observable> {
+ return this.getValidationTasks(workspaceId).pipe(
+ switchMap(tasks => {
+ // Filter completed tasks and group by validation type
+ const completedTasks = tasks.filter(task => task.status === 'completed');
+ const tasksByType: Record = {};
+
+ for (const task of completedTasks) {
+ if (!tasksByType[task.validation_type]) {
+ tasksByType[task.validation_type] = [];
+ }
+ tasksByType[task.validation_type].push(task);
+ }
+
+ // Get the most recent task for each type
+ const latestTasks: Record = {};
+ for (const type in tasksByType) {
+ if (Object.prototype.hasOwnProperty.call(tasksByType, type)) {
+ // Sort by creation date in descending order
+ tasksByType[type].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
+ latestTasks[type] = tasksByType[type][0];
+ }
+ }
+
+ const resultObservables: Array> = [];
+
+ for (const type in latestTasks) {
+ if (Object.prototype.hasOwnProperty.call(latestTasks, type)) {
+ const task = latestTasks[type];
+ resultObservables.push(
+ this.getValidationResults(workspaceId, task.id).pipe(
+ map(
+ result => [type, { task, result }] as [string, { task: ValidationTaskDto; result: unknown }]
+ ),
+ catchError(error => {
+ console.error(`Error getting results for task ${task.id}: ${error.message}`);
+ return of([type, { task, result: null }] as [string, { task: ValidationTaskDto; result: unknown }]);
+ })
+ )
+ );
+ }
+ }
+
+ if (resultObservables.length === 0) {
+ return of>({});
+ }
+
+ return forkJoin<[string, { task: ValidationTaskDto; result: unknown }][]>(resultObservables).pipe(
+ map<[string, { task: ValidationTaskDto; result: unknown }][], Record>(results => {
+ const resultMap: Record = {};
+ for (const [type, data] of results) {
+ resultMap[type] = data;
+ }
+ return resultMap;
+ })
+ );
+ }),
+ catchError(error => {
+ console.error(`Error getting last validation results: ${error.message}`);
+ return of>({});
+ })
+ );
+ }
}
diff --git a/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.html b/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.html
index dddb33c82..acc22aa70 100755
--- a/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.html
+++ b/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.html
@@ -25,9 +25,11 @@
search
Suchen
-
- rule
+
+ sync
+ rule
Validieren
+ Läuft...
analytics
@@ -169,7 +171,7 @@ Testhefte
-
Booklets werden geladen...
+
Testhefte werden geladen...
diff --git a/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.scss b/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.scss
index 0cc98a6ea..ad0c8b581 100755
--- a/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.scss
+++ b/apps/frontend/src/app/ws-admin/components/test-results/test-results.component.scss
@@ -409,6 +409,21 @@
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
transition: all 0.2s ease;
+ .rotating-icon {
+ animation: rotate 2s linear infinite;
+ }
+
+ .validation-status-text {
+ margin-left: 8px;
+ font-size: 12px;
+ opacity: 0.8;
+ }
+
+ @keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+ }
+
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
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 6c8c93780..cbb2a4bfe 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
@@ -53,6 +53,7 @@ import { UnitNoteDto } from '../../../../../../../api-dto/unit-notes/unit-note.d
import { ValidationDialogComponent } from '../validation-dialog/validation-dialog.component';
import { VariableValidationDto } from '../../../../../../../api-dto/files/variable-validation.dto';
import { VariableAnalysisDialogComponent } from '../variable-analysis-dialog/variable-analysis-dialog.component';
+import { ValidationTaskStateService } from '../../../services/validation-task-state.service';
interface BookletLog {
id: number;
@@ -179,6 +180,7 @@ export class TestResultsComponent implements OnInit, OnDestroy {
private router = inject(Router);
private snackBar = inject(MatSnackBar);
private translateService = inject(TranslateService);
+ private validationTaskStateService = inject(ValidationTaskStateService);
private searchSubject = new Subject();
private searchSubscription: Subscription | null = null;
private readonly SEARCH_DEBOUNCE_TIME = 800;
@@ -211,6 +213,11 @@ export class TestResultsComponent implements OnInit, OnDestroy {
variableValidationResult: VariableValidationDto | null = null;
readonly SHORT_PROCESSING_TIME_THRESHOLD_MS: number = 60000;
+ // Interval for checking validation status
+ private validationStatusInterval: number | null = null;
+ // Flag to track if component is initialized
+ private isInitialized: boolean = false;
+
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@@ -223,6 +230,10 @@ export class TestResultsComponent implements OnInit, OnDestroy {
});
this.createTestResultsList(0, this.pageSize);
+
+ // Start interval to check validation status
+ this.startValidationStatusCheck();
+ this.isInitialized = true;
}
ngOnDestroy(): void {
@@ -230,6 +241,112 @@ export class TestResultsComponent implements OnInit, OnDestroy {
this.searchSubscription.unsubscribe();
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);
+ this.validationStatusInterval = null;
+ }
+ }
+
+ /**
+ * Check validation status by querying active tasks
+ */
+ private checkValidationStatus(): void {
+ if (!this.isInitialized || !this.appService.selectedWorkspaceId) {
+ return;
+ }
+
+ 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,
+ type as 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'
+ );
+ }
+ },
+ 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'
+ );
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Check if any validation task is running
+ * @returns True if any validation task is running
+ */
+ isAnyValidationRunning(): boolean {
+ if (!this.appService.selectedWorkspaceId) {
+ return false;
+ }
+
+ const taskIds = this.validationTaskStateService.getAllTaskIds(this.appService.selectedWorkspaceId);
+ 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';
+ }
+
+ if (this.appService.selectedWorkspaceId) {
+ const results = this.validationTaskStateService.getAllValidationResults(this.appService.selectedWorkspaceId);
+
+ if (Object.keys(results).length > 0) {
+ const hasFailedValidation = Object.values(results).some(result => result.status === 'failed');
+ if (hasFailedValidation) {
+ return 'failed';
+ }
+
+ const validationTypes = ['variables', 'variableTypes', 'responseStatus', 'testTakers', 'groupResponses'];
+ const hasAllValidations = validationTypes.every(type => results[type]);
+ if (hasAllValidations) {
+ return 'success';
+ }
+
+ return 'success';
+ }
+ }
+
+ return 'not-run';
}
onRowClick(row: P): void {
@@ -1160,9 +1277,13 @@ export class TestResultsComponent implements OnInit, OnDestroy {
});
dialogRef.afterClosed().subscribe(result => {
- if (result && result.variableValidationResult) {
- this.variableValidationResult = result.variableValidationResult;
- this.isVariableValidationRunning = false;
+ if (result) {
+ if (result.variableValidationResult) {
+ this.variableValidationResult = result.variableValidationResult;
+ this.isVariableValidationRunning = false;
+ }
+ this.checkValidationStatus();
+ this.getOverallValidationStatus();
}
});
}
diff --git a/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.html b/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.html
index c70e63294..8b27d883f 100644
--- a/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.html
+++ b/apps/frontend/src/app/ws-admin/components/validation-dialog/validation-dialog.component.html
@@ -1,4 +1,118 @@
Antworten validieren
+
+ info
+ Validierungen laufen im Hintergrund weiter, auch wenn Sie diesen Dialog schließen.
+
+
+
+
+
Validierungsstatus
+
+
+
+ hourglass_empty
+ error
+ check_circle
+ radio_button_unchecked
+ {{ getValidationLabel('testTakers') }}
+ Läuft...
+
+ {{ testTakersValidationResult.missingPersons.length }} fehlende Testpersonen
+
+ OK
+ Nicht ausgeführt
+
+
+
+
+ hourglass_empty
+ error
+ check_circle
+ radio_button_unchecked
+ {{ getValidationLabel('variables') }}
+ Läuft...
+
+ {{ totalInvalidVariables }} ungültige Variablen
+
+ OK
+ Nicht ausgeführt
+
+
+
+
+ hourglass_empty
+ error
+ check_circle
+ radio_button_unchecked
+ {{ getValidationLabel('variableTypes') }}
+ Läuft...
+
+ {{ totalInvalidTypeVariables }} ungültige Variablentypen
+
+ OK
+ Nicht ausgeführt
+
+
+
+
+ hourglass_empty
+ error
+ check_circle
+ radio_button_unchecked
+ {{ getValidationLabel('responseStatus') }}
+ Läuft...
+
+ {{ totalInvalidStatusVariables }} ungültige Antwortstatus
+
+ OK
+ Nicht ausgeführt
+
+
+
+
+ hourglass_empty
+ error
+ check_circle
+ radio_button_unchecked
+ {{ getValidationLabel('groupResponses') }}
+ Läuft...
+
+ Nicht alle Gruppen haben Antworten
+
+ OK
+ Nicht ausgeführt
+
+
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 2cdda90b0..99f0ea7b5 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
@@ -1,5 +1,5 @@
import {
- Component, Inject, inject, ViewChild, AfterViewInit, OnInit
+ Component, Inject, inject, ViewChild, AfterViewInit, OnInit, OnDestroy
} from '@angular/core';
import {
MAT_DIALOG_DATA, MatDialog, MatDialogModule, MatDialogRef
@@ -14,11 +14,15 @@ import { MatExpansionModule } from '@angular/material/expansion';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatIconModule } from '@angular/material/icon';
+import { Subscription } from 'rxjs';
import { BackendService } from '../../../services/backend.service';
import { AppService } from '../../../services/app.service';
+import { ValidationTaskStateService, ValidationResult } from '../../../services/validation-task-state.service';
+import { ValidationService } from '../../../services/validation.service';
import { InvalidVariableDto } from '../../../../../../../api-dto/files/variable-validation.dto';
import { TestTakersValidationDto, MissingPersonDto } from '../../../../../../../api-dto/files/testtakers-validation.dto';
import { ContentDialogComponent } from '../../../shared/dialogs/content-dialog/content-dialog.component';
+import { ValidationTaskDto } from '../../../models/validation-task.dto';
@Component({
selector: 'coding-box-validation-dialog',
@@ -46,10 +50,6 @@ import { ContentDialogComponent } from '../../../shared/dialogs/content-dialog/c
margin-bottom: 16px;
}
- .mat-expansion-panel {
- margin-bottom: 16px;
- }
-
.mat-spinner {
display: inline-block;
margin-right: 8px;
@@ -81,12 +81,82 @@ import { ContentDialogComponent } from '../../../shared/dialogs/content-dialog/c
border: 1px solid #F44336;
}
+ .validation-running {
+ background-color: rgba(33, 150, 243, 0.1);
+ color: #2196F3;
+ border: 1px solid #2196F3;
+ }
+
+ .validation-not-run {
+ background-color: rgba(158, 158, 158, 0.1);
+ color: #9E9E9E;
+ border: 1px solid #9E9E9E;
+ }
+
.validation-result mat-icon {
margin-right: 8px;
}
+
+ .info-banner {
+ display: flex;
+ align-items: center;
+ margin: 0 0 16px 0;
+ padding: 8px 16px;
+ border-radius: 4px;
+ background-color: rgba(33, 150, 243, 0.1);
+ color: #2196F3;
+ border: 1px solid #2196F3;
+ }
+
+ .info-banner mat-icon {
+ margin-right: 8px;
+ }
+
+ .loading-container {
+ display: flex;
+ align-items: center;
+ margin: 10px 0;
+ }
+
+ .loading-text {
+ margin-left: 8px;
+ }
+
+ .validation-summary {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 16px;
+ padding: 16px;
+ border-radius: 4px;
+ background-color: #f5f5f5;
+ border: 1px solid #e0e0e0;
+ }
+
+ .validation-summary-title {
+ font-size: 16px;
+ font-weight: 500;
+ margin-bottom: 8px;
+ }
+
+ .validation-summary-item {
+ display: flex;
+ align-items: center;
+ padding: 8px;
+ border-radius: 4px;
+ font-weight: 500;
+ }
+
+ .validation-summary-item mat-icon {
+ margin-right: 8px;
+ }
+
+ .validation-summary-item-label {
+ flex: 1;
+ }
`]
})
-export class ValidationDialogComponent implements AfterViewInit, OnInit {
+export class ValidationDialogComponent implements AfterViewInit, OnInit, OnDestroy {
@ViewChild('variablePaginator') variablePaginator!: MatPaginator;
@ViewChild('variableTypePaginator') variableTypePaginator!: MatPaginator;
@ViewChild('statusVariablePaginator') statusVariablePaginator!: MatPaginator;
@@ -95,6 +165,21 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
firstStepCompleted = true;
backendService = inject(BackendService);
appService = inject(AppService);
+ 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[] = [];
@@ -172,6 +257,11 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
) {}
ngOnInit(): void {
+ // Check for existing validation tasks
+ this.checkForExistingTasks();
+
+ // Load previous validation results
+ this.loadPreviousValidationResults();
}
ngAfterViewInit(): void {
@@ -182,6 +272,414 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.paginatedGroupResponses.paginator = this.groupResponsesPaginator;
}
+ 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);
+ }
+
+ if (taskIds.variableTypes) {
+ this.loadExistingTask('variableTypes', taskIds.variableTypes);
+ }
+
+ if (taskIds.responseStatus) {
+ this.loadExistingTask('responseStatus', taskIds.responseStatus);
+ }
+
+ if (taskIds.testTakers) {
+ this.loadExistingTask('testTakers', taskIds.testTakers);
+ }
+
+ if (taskIds.groupResponses) {
+ this.loadExistingTask('groupResponses', taskIds.groupResponses);
+ }
+ }
+
+ /**
+ * 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;
+ break;
+ case 'variableTypes':
+ this.isVariableTypeValidationRunning = true;
+ break;
+ case 'responseStatus':
+ this.isResponseStatusValidationRunning = true;
+ break;
+ case 'testTakers':
+ this.isTestTakersValidationRunning = true;
+ break;
+ case 'groupResponses':
+ 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 => {
+ switch (type) {
+ case 'variables':
+ this.variableValidationTask = task;
+ break;
+ case 'variableTypes':
+ this.variableTypeValidationTask = task;
+ break;
+ case 'responseStatus':
+ this.responseStatusValidationTask = task;
+ break;
+ case 'testTakers':
+ this.testTakersValidationTask = task;
+ break;
+ case 'groupResponses':
+ this.groupResponsesValidationTask = task;
+ break;
+ default:
+ 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;
+ break;
+ case 'variableTypes':
+ this.isVariableTypeValidationRunning = false;
+ break;
+ case 'responseStatus':
+ this.isResponseStatusValidationRunning = false;
+ break;
+ case 'testTakers':
+ this.isTestTakersValidationRunning = false;
+ break;
+ case 'groupResponses':
+ this.isGroupResponsesValidationRunning = false;
+ break;
+ default:
+ break;
+ }
+ }
+ },
+ error: () => {
+ 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;
+ break;
+ case 'variableTypes':
+ this.isVariableTypeValidationRunning = false;
+ break;
+ case 'responseStatus':
+ this.isResponseStatusValidationRunning = false;
+ break;
+ case 'testTakers':
+ this.isTestTakersValidationRunning = false;
+ break;
+ case 'groupResponses':
+ this.isGroupResponsesValidationRunning = false;
+ break;
+ default:
+ break;
+ }
+ }
+ });
+
+ 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
+ ): void {
+ const workspaceId = this.appService.selectedWorkspaceId;
+
+ const pollSubscription = this.backendService.pollValidationTask(
+ workspaceId,
+ 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;
+ break;
+ case 'variableTypes':
+ this.isVariableTypeValidationRunning = false;
+ break;
+ case 'responseStatus':
+ this.isResponseStatusValidationRunning = false;
+ break;
+ case 'testTakers':
+ this.isTestTakersValidationRunning = false;
+ break;
+ case 'groupResponses':
+ this.isGroupResponsesValidationRunning = false;
+ break;
+ default:
+ break;
+ }
+ }
+ },
+ error: () => {
+ 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;
+ break;
+ case 'variableTypes':
+ this.isVariableTypeValidationRunning = false;
+ break;
+ case 'responseStatus':
+ this.isResponseStatusValidationRunning = false;
+ break;
+ case 'testTakers':
+ this.isTestTakersValidationRunning = false;
+ break;
+ case 'groupResponses':
+ this.isGroupResponsesValidationRunning = false;
+ break;
+ default:
+ break;
+ }
+ }
+ });
+
+ 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
+ ): void {
+ const workspaceId = this.appService.selectedWorkspaceId;
+
+ const subscription = this.backendService.getValidationResults(
+ workspaceId,
+ taskId
+ ).subscribe({
+ next: result => {
+ // Define result type interfaces outside of switch
+ interface PaginatedResult {
+ data: InvalidVariableDto[];
+ total: number;
+ page: number;
+ limit: number;
+ }
+
+ interface GroupResponsesResult {
+ testTakersFound: boolean;
+ groupsWithResponses: { group: string; hasResponse: boolean }[];
+ allGroupsHaveResponses: boolean;
+ total: number;
+ page: number;
+ limit: number;
+ }
+
+ // Process results based on type
+ switch (type) {
+ case 'variables': {
+ const typedResult = result as PaginatedResult;
+ this.invalidVariables = typedResult.data;
+ this.totalInvalidVariables = typedResult.total;
+ this.currentVariablePage = typedResult.page;
+ this.variablePageSize = typedResult.limit;
+ this.updatePaginatedVariables();
+ this.isVariableValidationRunning = false;
+ this.validateVariablesWasRun = true;
+
+ // Save validation result to the service
+ this.saveValidationResult(type);
+ break;
+ }
+
+ case 'variableTypes': {
+ const typedResult = result as PaginatedResult;
+ this.invalidTypeVariables = typedResult.data;
+ this.totalInvalidTypeVariables = typedResult.total;
+ this.currentTypeVariablePage = typedResult.page;
+ this.typeVariablePageSize = typedResult.limit;
+ this.updatePaginatedTypeVariables();
+ this.isVariableTypeValidationRunning = false;
+ this.validateVariableTypesWasRun = true;
+
+ // Save validation result to the service
+ this.saveValidationResult(type);
+ break;
+ }
+
+ case 'responseStatus': {
+ const typedResult = result as PaginatedResult;
+ this.invalidStatusVariables = typedResult.data;
+ this.totalInvalidStatusVariables = typedResult.total;
+ this.currentStatusVariablePage = typedResult.page;
+ this.statusVariablePageSize = typedResult.limit;
+ this.updatePaginatedStatusVariables();
+ this.isResponseStatusValidationRunning = false;
+ this.validateResponseStatusWasRun = true;
+
+ // Save validation result to the service
+ this.saveValidationResult(type);
+ break;
+ }
+
+ case 'testTakers': {
+ this.testTakersValidationResult = result as TestTakersValidationDto;
+ this.updatePaginatedMissingPersons();
+ this.isTestTakersValidationRunning = false;
+ this.testTakersValidationWasRun = true;
+
+ // Save validation result to the service
+ this.saveValidationResult(type);
+ break;
+ }
+
+ case 'groupResponses': {
+ const typedResult = result as GroupResponsesResult;
+ this.groupResponsesResult = typedResult;
+ this.totalGroupResponses = typedResult.total;
+ this.updatePaginatedGroupResponses();
+ this.isGroupResponsesValidationRunning = false;
+ this.groupResponsesValidationWasRun = true;
+
+ // Save validation result to the service
+ this.saveValidationResult(type);
+ break;
+ }
+
+ default:
+ 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;
+ break;
+ case 'variableTypes':
+ this.isVariableTypeValidationRunning = false;
+ break;
+ case 'responseStatus':
+ this.isResponseStatusValidationRunning = false;
+ break;
+ case 'testTakers':
+ this.isTestTakersValidationRunning = false;
+ break;
+ case 'groupResponses':
+ this.isGroupResponsesValidationRunning = false;
+ break;
+ default:
+ // No action needed for unknown types
+ break;
+ }
+ }
+ });
+
+ 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);
+ }
+
+ if (this.variableTypeValidationTask && (this.variableTypeValidationTask.status === 'pending' || this.variableTypeValidationTask.status === 'processing')) {
+ this.validationTaskStateService.setTaskId(workspaceId, 'variableTypes', this.variableTypeValidationTask.id);
+ }
+
+ if (this.responseStatusValidationTask && (this.responseStatusValidationTask.status === 'pending' || this.responseStatusValidationTask.status === 'processing')) {
+ this.validationTaskStateService.setTaskId(workspaceId, 'responseStatus', this.responseStatusValidationTask.id);
+ }
+
+ if (this.testTakersValidationTask && (this.testTakersValidationTask.status === 'pending' || this.testTakersValidationTask.status === 'processing')) {
+ this.validationTaskStateService.setTaskId(workspaceId, 'testTakers', this.testTakersValidationTask.id);
+ }
+
+ if (this.groupResponsesValidationTask && (this.groupResponsesValidationTask.status === 'pending' || this.groupResponsesValidationTask.status === 'processing')) {
+ this.validationTaskStateService.setTaskId(workspaceId, 'groupResponses', this.groupResponsesValidationTask.id);
+ }
+ }
+
updatePaginatedVariables(): void {
this.paginatedVariables.data = this.invalidVariables;
}
@@ -203,31 +701,146 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.currentGroupResponsesPage = event.pageIndex + 1;
this.groupResponsesPageSize = event.pageSize;
- // Reload data from server with new pagination parameters
+ // Reload data from server with new pagination parameters using background task
this.isGroupResponsesValidationRunning = true;
- this.backendService.validateGroupResponses(
+
+ // 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',
this.currentGroupResponsesPage,
this.groupResponsesPageSize
- ).subscribe(result => {
- this.groupResponsesResult = result;
- this.totalGroupResponses = result.total;
- this.updatePaginatedGroupResponses();
- this.isGroupResponsesValidationRunning = false;
+ ).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 }[];
+ allGroupsHaveResponses: boolean;
+ total: number;
+ page: number;
+ limit: number;
+ };
+
+ this.groupResponsesResult = typedResult;
+ this.totalGroupResponses = typedResult.total;
+ this.updatePaginatedGroupResponses();
+ this.isGroupResponsesValidationRunning = false;
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isGroupResponsesValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isGroupResponsesValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
});
+
+ this.subscriptions.push(subscription);
}
validateTestTakers(): void {
this.isTestTakersValidationRunning = true;
this.testTakersValidationResult = null;
this.testTakersValidationWasRun = false;
- this.backendService.validateTestTakers(this.appService.selectedWorkspaceId)
- .subscribe(result => {
- this.testTakersValidationResult = result;
- this.updatePaginatedMissingPersons();
- this.isTestTakersValidationRunning = false;
- this.testTakersValidationWasRun = 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,
+ '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(),
+ details: {
+ testTakersFound: this.testTakersValidationResult.testTakersFound,
+ missingPersonsCount: this.testTakersValidationResult.missingPersons.length,
+ hasErrors: hasErrors
+ }
+ };
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(
+ this.appService.selectedWorkspaceId,
+ 'testTakers',
+ validationResult
+ );
+
+ this.updatePaginatedMissingPersons();
+ this.isTestTakersValidationRunning = false;
+ this.testTakersValidationWasRun = true;
+
+ this.saveValidationResult('testTakers');
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isTestTakersValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isTestTakersValidationRunning = false;
+ }
});
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
}
toggleMissingPersonsExpansion(): void {
@@ -243,17 +856,93 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.groupResponsesResult = null;
this.groupResponsesValidationWasRun = false;
this.currentGroupResponsesPage = 1;
- this.backendService.validateGroupResponses(
+
+ // 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',
this.currentGroupResponsesPage,
this.groupResponsesPageSize
- ).subscribe(result => {
- this.groupResponsesResult = result;
- this.totalGroupResponses = result.total;
- this.updatePaginatedGroupResponses();
- this.isGroupResponsesValidationRunning = false;
- this.groupResponsesValidationWasRun = true;
+ ).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 }[];
+ allGroupsHaveResponses: boolean;
+ total: number;
+ 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(),
+ details: {
+ testTakersFound: typedResult.testTakersFound,
+ allGroupsHaveResponses: typedResult.allGroupsHaveResponses,
+ hasErrors: hasErrors
+ }
+ };
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(
+ this.appService.selectedWorkspaceId,
+ 'groupResponses',
+ validationResult
+ );
+
+ this.groupResponsesResult = typedResult;
+ this.totalGroupResponses = typedResult.total;
+ this.updatePaginatedGroupResponses();
+ this.isGroupResponsesValidationRunning = false;
+ this.groupResponsesValidationWasRun = true;
+
+ // Save the validation result to the service
+ this.saveValidationResult('groupResponses');
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isGroupResponsesValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isGroupResponsesValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
});
+
+ this.subscriptions.push(subscription);
}
updatePaginatedTypeVariables(): void {
@@ -270,25 +959,156 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.totalInvalidVariables = 0;
this.validateVariablesWasRun = false;
this.selectedResponses.clear();
- this.backendService.validateVariables(
+
+ // 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',
this.currentVariablePage,
this.variablePageSize
- ).subscribe(result => {
- this.invalidVariables = result.data;
- this.totalInvalidVariables = result.total;
- this.currentVariablePage = result.page;
- this.variablePageSize = result.limit;
- this.updatePaginatedVariables();
- this.isVariableValidationRunning = false;
- this.validateVariablesWasRun = true;
+ ).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;
+ 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(),
+ details: {
+ total: typedResult.total,
+ hasErrors: hasErrors
+ }
+ };
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(
+ this.appService.selectedWorkspaceId,
+ 'variables',
+ validationResult
+ );
+
+ this.invalidVariables = typedResult.data;
+ this.totalInvalidVariables = typedResult.total;
+ this.currentVariablePage = typedResult.page;
+ this.variablePageSize = typedResult.limit;
+ this.updatePaginatedVariables();
+ this.isVariableValidationRunning = false;
+ this.validateVariablesWasRun = true;
+
+ // Save the validation result to the service
+ this.saveValidationResult('variables');
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
});
+
+ this.subscriptions.push(subscription);
}
onVariablePageChange(event: PageEvent): void {
this.currentVariablePage = event.pageIndex + 1;
this.variablePageSize = event.pageSize;
- this.validateVariables();
+
+ // 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',
+ this.currentVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidVariables = typedResult.data;
+ this.totalInvalidVariables = typedResult.total;
+ this.currentVariablePage = typedResult.page;
+ this.variablePageSize = typedResult.limit;
+ this.updatePaginatedVariables();
+ this.isVariableValidationRunning = false;
+ this.validateVariablesWasRun = true;
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
}
toggleResponseSelection(responseId: number | undefined): void {
@@ -326,14 +1146,103 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.isDeletingResponses = true;
const responseIds = Array.from(this.selectedResponses);
- this.backendService.deleteInvalidResponses(this.appService.selectedWorkspaceId, responseIds)
- .subscribe(deletedCount => {
- this.isDeletingResponses = false;
- this.snackBar.open(`${deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 });
+ // Cancel any existing subscription
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ this.subscriptions = [];
- this.validateVariables();
- this.selectedResponses.clear();
+ // 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,
+ updatedTask.id
+ ).subscribe(result => {
+ 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',
+ this.currentVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidVariables = typedValidationResult.data;
+ this.totalInvalidVariables = typedValidationResult.total;
+ this.currentVariablePage = typedValidationResult.page;
+ this.variablePageSize = typedValidationResult.limit;
+ this.updatePaginatedVariables();
+ this.isVariableValidationRunning = false;
+ this.validateVariablesWasRun = true;
+ });
+ } else if (updatedValidationTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedValidationTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(validationPollSubscription);
+ });
+
+ this.subscriptions.push(validationSubscription);
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.isDeletingResponses = false;
+ this.snackBar.open(`Löschen fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ }
+ },
+ error: () => {
+ this.isDeletingResponses = false;
+ this.snackBar.open('Fehler beim Abrufen des Löschstatus', 'Schließen', { duration: 5000 });
+ }
});
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
+ this.selectedResponses.clear();
}
deleteAllResponses(): void {
@@ -357,14 +1266,102 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
if (deleteFromDb) {
this.isDeletingResponses = true;
- this.backendService.deleteAllInvalidResponses(this.appService.selectedWorkspaceId, 'variables')
- .subscribe(deletedCount => {
- this.isDeletingResponses = false;
- this.snackBar.open(`${deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 });
-
- this.validateVariables();
- this.selectedResponses.clear();
+ // 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,
+ updatedTask.id
+ ).subscribe(result => {
+ 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',
+ this.currentVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidVariables = typedValidationResult.data;
+ this.totalInvalidVariables = typedValidationResult.total;
+ this.currentVariablePage = typedValidationResult.page;
+ this.variablePageSize = typedValidationResult.limit;
+ this.updatePaginatedVariables();
+ this.isVariableValidationRunning = false;
+ this.validateVariablesWasRun = true;
+ });
+ } else if (updatedValidationTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedValidationTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(validationPollSubscription);
+ });
+
+ this.subscriptions.push(validationSubscription);
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.isDeletingResponses = false;
+ this.snackBar.open(`Löschen fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ }
+ },
+ error: () => {
+ this.isDeletingResponses = false;
+ this.snackBar.open('Fehler beim Abrufen des Löschstatus', 'Schließen', { duration: 5000 });
+ }
});
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
}
});
}
@@ -379,19 +1376,91 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.totalInvalidTypeVariables = 0;
this.validateVariableTypesWasRun = false;
this.selectedTypeResponses.clear();
- this.backendService.validateVariableTypes(
+
+ // 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',
this.currentTypeVariablePage,
this.typeVariablePageSize
- ).subscribe(result => {
- this.invalidTypeVariables = result.data;
- this.totalInvalidTypeVariables = result.total;
- this.currentTypeVariablePage = result.page;
- this.typeVariablePageSize = result.limit;
- this.updatePaginatedTypeVariables();
- this.isVariableTypeValidationRunning = false;
- this.validateVariableTypesWasRun = true;
+ ).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;
+ 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(),
+ details: {
+ total: typedResult.total,
+ hasErrors: hasErrors
+ }
+ };
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(
+ this.appService.selectedWorkspaceId,
+ 'variableTypes',
+ validationResult
+ );
+
+ this.invalidTypeVariables = typedResult.data;
+ this.totalInvalidTypeVariables = typedResult.total;
+ this.currentTypeVariablePage = typedResult.page;
+ this.typeVariablePageSize = typedResult.limit;
+ this.updatePaginatedTypeVariables();
+ this.isVariableTypeValidationRunning = false;
+ this.validateVariableTypesWasRun = true;
+
+ // Save the validation result to the service
+ this.saveValidationResult('variableTypes');
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
});
+
+ this.subscriptions.push(subscription);
}
validateResponseStatus(): void {
@@ -400,31 +1469,221 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.totalInvalidStatusVariables = 0;
this.validateResponseStatusWasRun = false;
this.selectedStatusResponses.clear();
- this.backendService.validateResponseStatus(
+
+ // 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',
this.currentStatusVariablePage,
this.statusVariablePageSize
- ).subscribe(result => {
- this.invalidStatusVariables = result.data;
- this.totalInvalidStatusVariables = result.total;
- this.currentStatusVariablePage = result.page;
- this.statusVariablePageSize = result.limit;
- this.updatePaginatedStatusVariables();
- this.isResponseStatusValidationRunning = false;
- this.validateResponseStatusWasRun = true;
+ ).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(),
+ details: {
+ total: typedResult.total,
+ hasErrors: hasErrors
+ }
+ };
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(
+ this.appService.selectedWorkspaceId,
+ 'responseStatus',
+ validationResult
+ );
+
+ this.invalidStatusVariables = typedResult.data;
+ this.totalInvalidStatusVariables = typedResult.total;
+ this.currentStatusVariablePage = typedResult.page;
+ this.statusVariablePageSize = typedResult.limit;
+ this.updatePaginatedStatusVariables();
+ this.isResponseStatusValidationRunning = false;
+ this.validateResponseStatusWasRun = true;
+
+ // Save the validation result to the service
+ this.saveValidationResult('responseStatus');
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
});
+
+ this.subscriptions.push(subscription);
}
onTypeVariablePageChange(event: PageEvent): void {
this.currentTypeVariablePage = event.pageIndex + 1;
this.typeVariablePageSize = event.pageSize;
- this.validateVariableTypes();
+
+ // 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',
+ this.currentTypeVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidTypeVariables = typedResult.data;
+ this.totalInvalidTypeVariables = typedResult.total;
+ this.currentTypeVariablePage = typedResult.page;
+ this.typeVariablePageSize = typedResult.limit;
+ this.updatePaginatedTypeVariables();
+ this.isVariableTypeValidationRunning = false;
+ this.validateVariableTypesWasRun = true;
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
}
onStatusVariablePageChange(event: PageEvent): void {
this.currentStatusVariablePage = event.pageIndex + 1;
this.statusVariablePageSize = event.pageSize;
- this.validateResponseStatus();
+
+ // 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',
+ this.currentStatusVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidStatusVariables = typedResult.data;
+ this.totalInvalidStatusVariables = typedResult.total;
+ this.currentStatusVariablePage = typedResult.page;
+ this.statusVariablePageSize = typedResult.limit;
+ this.updatePaginatedStatusVariables();
+ this.isResponseStatusValidationRunning = false;
+ this.validateResponseStatusWasRun = true;
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
}
toggleTypeResponseSelection(responseId: number | undefined): void {
@@ -462,14 +1721,103 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.isDeletingResponses = true;
const responseIds = Array.from(this.selectedTypeResponses);
- this.backendService.deleteInvalidResponses(this.appService.selectedWorkspaceId, responseIds)
- .subscribe(deletedCount => {
- this.isDeletingResponses = false;
- this.snackBar.open(`${deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 });
+ // Cancel any existing subscription
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ this.subscriptions = [];
- this.validateVariableTypes();
- this.selectedTypeResponses.clear();
+ // 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,
+ updatedTask.id
+ ).subscribe(result => {
+ 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',
+ this.currentTypeVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidTypeVariables = typedValidationResult.data;
+ this.totalInvalidTypeVariables = typedValidationResult.total;
+ this.currentTypeVariablePage = typedValidationResult.page;
+ this.typeVariablePageSize = typedValidationResult.limit;
+ this.updatePaginatedTypeVariables();
+ this.isVariableTypeValidationRunning = false;
+ this.validateVariableTypesWasRun = true;
+ });
+ } else if (updatedValidationTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedValidationTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(validationPollSubscription);
+ });
+
+ this.subscriptions.push(validationSubscription);
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.isDeletingResponses = false;
+ this.snackBar.open(`Löschen fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ }
+ },
+ error: () => {
+ this.isDeletingResponses = false;
+ this.snackBar.open('Fehler beim Abrufen des Löschstatus', 'Schließen', { duration: 5000 });
+ }
});
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
+ this.selectedTypeResponses.clear();
}
deleteAllTypeResponses(): void {
@@ -493,14 +1841,102 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
if (deleteFromDb) {
this.isDeletingResponses = true;
- this.backendService.deleteAllInvalidResponses(this.appService.selectedWorkspaceId, 'variableTypes')
- .subscribe(deletedCount => {
- this.isDeletingResponses = false;
- this.snackBar.open(`${deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 });
-
- this.validateVariableTypes();
- this.selectedTypeResponses.clear();
+ // 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,
+ updatedTask.id
+ ).subscribe(result => {
+ 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',
+ this.currentTypeVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidTypeVariables = typedValidationResult.data;
+ this.totalInvalidTypeVariables = typedValidationResult.total;
+ this.currentTypeVariablePage = typedValidationResult.page;
+ this.typeVariablePageSize = typedValidationResult.limit;
+ this.updatePaginatedTypeVariables();
+ this.isVariableTypeValidationRunning = false;
+ this.validateVariableTypesWasRun = true;
+ });
+ } else if (updatedValidationTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedValidationTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isVariableTypeValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(validationPollSubscription);
+ });
+
+ this.subscriptions.push(validationSubscription);
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.isDeletingResponses = false;
+ this.snackBar.open(`Löschen fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ }
+ },
+ error: () => {
+ this.isDeletingResponses = false;
+ this.snackBar.open('Fehler beim Abrufen des Löschstatus', 'Schließen', { duration: 5000 });
+ }
});
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
}
});
}
@@ -544,14 +1980,103 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.isDeletingResponses = true;
const responseIds = Array.from(this.selectedStatusResponses);
- this.backendService.deleteInvalidResponses(this.appService.selectedWorkspaceId, responseIds)
- .subscribe(deletedCount => {
- this.isDeletingResponses = false;
- this.snackBar.open(`${deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 });
+ // Cancel any existing subscription
+ this.subscriptions.forEach(sub => sub.unsubscribe());
+ this.subscriptions = [];
- this.validateResponseStatus();
- this.selectedStatusResponses.clear();
+ // 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,
+ updatedTask.id
+ ).subscribe(result => {
+ 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',
+ this.currentStatusVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidStatusVariables = typedValidationResult.data;
+ this.totalInvalidStatusVariables = typedValidationResult.total;
+ this.currentStatusVariablePage = typedValidationResult.page;
+ this.statusVariablePageSize = typedValidationResult.limit;
+ this.updatePaginatedStatusVariables();
+ this.isResponseStatusValidationRunning = false;
+ this.validateResponseStatusWasRun = true;
+ });
+ } else if (updatedValidationTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedValidationTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(validationPollSubscription);
+ });
+
+ this.subscriptions.push(validationSubscription);
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.isDeletingResponses = false;
+ this.snackBar.open(`Löschen fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ }
+ },
+ error: () => {
+ this.isDeletingResponses = false;
+ this.snackBar.open('Fehler beim Abrufen des Löschstatus', 'Schließen', { duration: 5000 });
+ }
});
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
+ this.selectedStatusResponses.clear();
}
deleteAllStatusResponses(): void {
@@ -575,13 +2100,103 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
if (deleteFromDb) {
this.isDeletingResponses = true;
- this.backendService.deleteAllInvalidResponses(this.appService.selectedWorkspaceId, 'responseStatus')
- .subscribe(deletedCount => {
- this.isDeletingResponses = false;
- this.snackBar.open(`${deletedCount} Antworten gelöscht`, 'Schließen', { duration: 3000 });
- this.validateResponseStatus();
- this.selectedStatusResponses.clear();
+ // 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,
+ updatedTask.id
+ ).subscribe(result => {
+ 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',
+ this.currentStatusVariablePage,
+ 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;
+ page: number;
+ limit: number;
+ };
+
+ this.invalidStatusVariables = typedValidationResult.data;
+ this.totalInvalidStatusVariables = typedValidationResult.total;
+ this.currentStatusVariablePage = typedValidationResult.page;
+ this.statusVariablePageSize = typedValidationResult.limit;
+ this.updatePaginatedStatusVariables();
+ this.isResponseStatusValidationRunning = false;
+ this.validateResponseStatusWasRun = true;
+ });
+ } else if (updatedValidationTask.status === 'failed') {
+ this.snackBar.open(`Validierung fehlgeschlagen: ${updatedValidationTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ },
+ error: () => {
+ this.snackBar.open('Fehler beim Abrufen des Validierungsstatus', 'Schließen', { duration: 5000 });
+ this.isResponseStatusValidationRunning = false;
+ }
+ });
+
+ this.subscriptions.push(validationPollSubscription);
+ });
+
+ this.subscriptions.push(validationSubscription);
+ this.selectedStatusResponses.clear();
+ });
+ } else if (updatedTask.status === 'failed') {
+ this.isDeletingResponses = false;
+ this.snackBar.open(`Löschen fehlgeschlagen: ${updatedTask.error || 'Unbekannter Fehler'}`, 'Schließen', { duration: 5000 });
+ }
+ },
+ error: () => {
+ this.isDeletingResponses = false;
+ this.snackBar.open('Fehler beim Abrufen des Löschstatus', 'Schließen', { duration: 5000 });
+ }
});
+
+ this.subscriptions.push(pollSubscription);
+ });
+
+ this.subscriptions.push(subscription);
}
});
}
@@ -590,7 +2205,434 @@ export class ValidationDialogComponent implements AfterViewInit, OnInit {
this.expandedStatusPanel = !this.expandedStatusPanel;
}
+ isAnyValidationRunning(): boolean {
+ return this.isVariableValidationRunning ||
+ this.isVariableTypeValidationRunning ||
+ this.isResponseStatusValidationRunning ||
+ this.isTestTakersValidationRunning ||
+ this.isGroupResponsesValidationRunning;
+ }
+
+ hasValidationFailed(type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'): boolean {
+ switch (type) {
+ case 'variables':
+ return this.validateVariablesWasRun && this.totalInvalidVariables > 0;
+ case 'variableTypes':
+ return this.validateVariableTypesWasRun && this.totalInvalidTypeVariables > 0;
+ case 'responseStatus':
+ return this.validateResponseStatusWasRun && this.totalInvalidStatusVariables > 0;
+ case 'testTakers':
+ return this.testTakersValidationWasRun &&
+ this.testTakersValidationResult !== null &&
+ (
+ !this.testTakersValidationResult.testTakersFound ||
+ this.testTakersValidationResult.missingPersons.length > 0
+ );
+ case 'groupResponses':
+ return this.groupResponsesValidationWasRun &&
+ this.groupResponsesResult !== null &&
+ (
+ !this.groupResponsesResult.testTakersFound ||
+ !this.groupResponsesResult.allGroupsHaveResponses
+ );
+ default:
+ return false;
+ }
+ }
+
+ hasValidationSucceeded(type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'): boolean {
+ switch (type) {
+ case 'variables':
+ return this.validateVariablesWasRun && this.totalInvalidVariables === 0;
+ case 'variableTypes':
+ return this.validateVariableTypesWasRun && this.totalInvalidTypeVariables === 0;
+ case 'responseStatus':
+ return this.validateResponseStatusWasRun && this.totalInvalidStatusVariables === 0;
+ case 'testTakers':
+ return this.testTakersValidationWasRun &&
+ this.testTakersValidationResult !== null &&
+ this.testTakersValidationResult.testTakersFound &&
+ this.testTakersValidationResult.missingPersons.length === 0;
+ case 'groupResponses':
+ return this.groupResponsesValidationWasRun &&
+ this.groupResponsesResult !== null &&
+ this.groupResponsesResult.testTakersFound &&
+ this.groupResponsesResult.allGroupsHaveResponses;
+ default:
+ return false;
+ }
+ }
+
+ getValidationStatus(type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'): 'running' | 'failed' | 'success' | 'not-run' {
+ switch (type) {
+ case 'variables':
+ if (this.isVariableValidationRunning) return 'running';
+ if (this.hasValidationFailed('variables')) return 'failed';
+ if (this.hasValidationSucceeded('variables')) return 'success';
+ return 'not-run';
+ case 'variableTypes':
+ if (this.isVariableTypeValidationRunning) return 'running';
+ if (this.hasValidationFailed('variableTypes')) return 'failed';
+ if (this.hasValidationSucceeded('variableTypes')) return 'success';
+ return 'not-run';
+ case 'responseStatus':
+ if (this.isResponseStatusValidationRunning) return 'running';
+ if (this.hasValidationFailed('responseStatus')) return 'failed';
+ if (this.hasValidationSucceeded('responseStatus')) return 'success';
+ return 'not-run';
+ case 'testTakers':
+ if (this.isTestTakersValidationRunning) return 'running';
+ if (this.hasValidationFailed('testTakers')) return 'failed';
+ if (this.hasValidationSucceeded('testTakers')) return 'success';
+ return 'not-run';
+ case 'groupResponses':
+ if (this.isGroupResponsesValidationRunning) return 'running';
+ if (this.hasValidationFailed('groupResponses')) return 'failed';
+ if (this.hasValidationSucceeded('groupResponses')) return 'success';
+ return 'not-run';
+ default:
+ return 'not-run';
+ }
+ }
+
+ getValidationLabel(type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'): string {
+ switch (type) {
+ case 'variables':
+ return 'Variable definiert';
+ case 'variableTypes':
+ return 'Variable ist gültig';
+ case 'responseStatus':
+ return 'Status ist gültig';
+ case 'testTakers':
+ return 'Testperson definiert';
+ case 'groupResponses':
+ return 'Antworten für alle Gruppen';
+ default:
+ return '';
+ }
+ }
+
+ private saveValidationResult(type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'): 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);
+ }
+ }
+
+ private getValidationDetails(type: 'variables' | 'variableTypes' | 'responseStatus' | 'testTakers' | 'groupResponses'): unknown {
+ switch (type) {
+ case 'variables':
+ return {
+ total: this.totalInvalidVariables,
+ hasErrors: this.totalInvalidVariables > 0
+ };
+ case 'variableTypes':
+ return {
+ total: this.totalInvalidTypeVariables,
+ hasErrors: this.totalInvalidTypeVariables > 0
+ };
+ case 'responseStatus':
+ return {
+ total: this.totalInvalidStatusVariables,
+ hasErrors: this.totalInvalidStatusVariables > 0
+ };
+ case 'testTakers':
+ if (!this.testTakersValidationResult) return { hasErrors: false };
+ return {
+ testTakersFound: this.testTakersValidationResult.testTakersFound,
+ missingPersonsCount: this.testTakersValidationResult.missingPersons.length,
+ hasErrors: !this.testTakersValidationResult.testTakersFound ||
+ this.testTakersValidationResult.missingPersons.length > 0
+ };
+ case 'groupResponses':
+ if (!this.groupResponsesResult) return { hasErrors: false };
+ return {
+ testTakersFound: this.groupResponsesResult.testTakersFound,
+ allGroupsHaveResponses: this.groupResponsesResult.allGroupsHaveResponses,
+ hasErrors: !this.groupResponsesResult.testTakersFound ||
+ !this.groupResponsesResult.allGroupsHaveResponses
+ };
+ default:
+ return {};
+ }
+ }
+
+ 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);
+ }
+
+ if (inMemoryResults.variableTypes) {
+ this.processVariableTypesResult(inMemoryResults.variableTypes, false);
+ }
+
+ if (inMemoryResults.responseStatus) {
+ this.processResponseStatusResult(inMemoryResults.responseStatus, false);
+ }
+
+ if (inMemoryResults.testTakers) {
+ this.processTestTakersResult(inMemoryResults.testTakers, false);
+ }
+
+ if (inMemoryResults.groupResponses) {
+ 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';
+ if (task.status === 'completed') {
+ status = task.error ? 'failed' : 'success';
+ }
+ const validationResult: ValidationResult = {
+ status,
+ timestamp: new Date(task.created_at).getTime(),
+ details: result
+ };
+ this.processVariablesResult(validationResult, false);
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(workspaceId, 'variables', validationResult);
+ }
+
+ if (results.variableTypes) {
+ const { task, result } = results.variableTypes;
+ let status: 'success' | 'failed' | 'not-run' = 'not-run';
+ if (task.status === 'completed') {
+ status = task.error ? 'failed' : 'success';
+ }
+ const validationResult: ValidationResult = {
+ status,
+ timestamp: new Date(task.created_at).getTime(),
+ details: result
+ };
+ this.processVariableTypesResult(validationResult, false);
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(workspaceId, 'variableTypes', validationResult);
+ }
+
+ if (results.responseStatus) {
+ const { task, result } = results.responseStatus;
+ let status: 'success' | 'failed' | 'not-run' = 'not-run';
+ if (task.status === 'completed') {
+ status = task.error ? 'failed' : 'success';
+ }
+ const validationResult: ValidationResult = {
+ status,
+ timestamp: new Date(task.created_at).getTime(),
+ details: result
+ };
+ this.processResponseStatusResult(validationResult, false);
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(workspaceId, 'responseStatus', validationResult);
+ }
+
+ if (results.testTakers) {
+ const { task, result } = results.testTakers;
+ let status: 'success' | 'failed' | 'not-run' = 'not-run';
+ if (task.status === 'completed') {
+ status = task.error ? 'failed' : 'success';
+ }
+ const validationResult: ValidationResult = {
+ status,
+ timestamp: new Date(task.created_at).getTime(),
+ details: result
+ };
+ this.processTestTakersResult(validationResult, false);
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(workspaceId, 'testTakers', validationResult);
+ }
+
+ if (results.groupResponses) {
+ const { task, result } = results.groupResponses;
+ let status: 'success' | 'failed' | 'not-run' = 'not-run';
+ if (task.status === 'completed') {
+ status = task.error ? 'failed' : 'success';
+ }
+ const validationResult: ValidationResult = {
+ status,
+ timestamp: new Date(task.created_at).getTime(),
+ details: result
+ };
+ this.processGroupResponsesResult(validationResult, false);
+
+ // Store the result in the validation task state service
+ this.validationTaskStateService.setValidationResult(workspaceId, 'groupResponses', validationResult);
+ }
+ },
+ error: error => {
+ console.error('Error loading previous validation results:', error);
+ }
+ });
+
+ this.subscriptions.push(subscription);
+ }
+
+ 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;
+ }
+ this.totalInvalidVariables = 0;
+ 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();
+ }
+ }
+ }
+
+ 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;
+ }
+ this.totalInvalidTypeVariables = 0;
+ 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();
+ }
+ }
+ }
+
+ 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;
+ }
+ this.totalInvalidStatusVariables = 0;
+ 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();
+ }
+ }
+ }
+
+ 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;
+ }
+ this.testTakersValidationResult = {
+ testTakersFound: true,
+ totalGroups: 0,
+ totalLogins: 0,
+ totalBookletCodes: 0,
+ missingPersons: []
+ };
+ 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;
+ }
+ const details = result.details as {
+ testTakersFound: boolean;
+ missingPersonsCount: number;
+ 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();
+ }
+ }
+ }
+
+ 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;
+ }
+ this.groupResponsesResult = {
+ testTakersFound: true,
+ groupsWithResponses: [],
+ allGroupsHaveResponses: true
+ };
+ 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;
+ }
+ const details = result.details as {
+ testTakersFound: boolean;
+ allGroupsHaveResponses: boolean;
+ 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();
+ }
+ }
+ }
+
closeWithResults(): void {
+ this.isClosing = true;
+
+ this.storeRunningTasks();
+
this.dialogRef.close({
invalidVariables: this.invalidVariables,
totalInvalidVariables: this.totalInvalidVariables,
diff --git a/database/changelog/coding-box.changelog-0.9.2.sql b/database/changelog/coding-box.changelog-0.9.2.sql
new file mode 100644
index 000000000..26eb748f8
--- /dev/null
+++ b/database/changelog/coding-box.changelog-0.9.2.sql
@@ -0,0 +1,18 @@
+-- liquibase formatted sql
+
+-- changeset jurei733:1
+ALTER TABLE "public"."job" ADD COLUMN "validation_type" VARCHAR(50);
+ALTER TABLE "public"."job" ADD COLUMN "page" INTEGER;
+ALTER TABLE "public"."job" ADD COLUMN "limit" INTEGER;
+
+CREATE INDEX "idx_job_validation_type" ON "public"."job" ("validation_type");
+
+-- rollback ALTER TABLE "public"."job" DROP COLUMN "validation_type"; ALTER TABLE "public"."job" DROP COLUMN "page"; ALTER TABLE "public"."job" DROP COLUMN "limit"; DROP INDEX IF EXISTS "idx_job_validation_type";
+
+-- changeset jurei733:2
+CREATE INDEX idx_responses_unitid ON response(unitid);
+CREATE INDEX idx_responses_variableid ON response(variableid);
+CREATE INDEX idx_responses_codedstatus ON response(codedstatus);
+
+-- rollback DROP INDEX IF EXISTS idx_responses_unitid; DROP INDEX IF EXISTS idx_responses_variableid; DROP INDEX IF EXISTS idx_responses_codedstatus;
+
diff --git a/database/changelog/coding-box.changelog-root.xml b/database/changelog/coding-box.changelog-root.xml
index 94b76ef44..c71cc69ef 100644
--- a/database/changelog/coding-box.changelog-root.xml
+++ b/database/changelog/coding-box.changelog-root.xml
@@ -16,4 +16,5 @@
+
From e867a63702dbee12f5cd32207cf32e3f91fc6ac6 Mon Sep 17 00:00:00 2001
From: jurei733 <67505990+jurei733@users.noreply.github.com>
Date: Wed, 16 Jul 2025 15:01:15 +0200
Subject: [PATCH 03/21] Fix scrolling in files validation
---
.../files-validation.component.scss | 30 ++++++++++++-------
.../files-validation.component.ts | 12 --------
2 files changed, 20 insertions(+), 22 deletions(-)
diff --git a/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.scss b/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.scss
index 9fecd9469..dd977096d 100644
--- a/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.scss
+++ b/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.scss
@@ -1,4 +1,3 @@
-// Dialog styles
mat-dialog-title {
font-size: 1.8em;
font-weight: 600;
@@ -13,9 +12,6 @@ mat-dialog-content {
padding: 0;
}
-
-
-// Container styles
.scroll-container {
max-height: 600px;
overflow-y: auto;
@@ -47,7 +43,6 @@ mat-dialog-content {
to { opacity: 1; }
}
-// Test taker section
h4 {
font-size: 20px;
font-weight: 600;
@@ -73,14 +68,12 @@ h4 {
}
}
-// Validation container
.validation-container {
display: flex;
flex-direction: column;
gap: 16px;
}
-// Validation card styles
.validation-card {
background-color: white;
border-radius: 12px;
@@ -151,7 +144,6 @@ h4 {
}
}
- // Complete status
.status-complete {
color: #2e7d32;
background-color: rgba(46, 125, 50, 0.1);
@@ -162,7 +154,6 @@ h4 {
}
}
- // Incomplete status
.status-incomplete {
color: #d32f2f;
background-color: rgba(211, 47, 47, 0.1);
@@ -281,9 +272,28 @@ h4 {
transition: all 0.3s ease-in-out;
&.expanded {
- max-height: 500px; // Large enough to accommodate most file lists
+ max-height: 400px;
opacity: 1;
padding: 16px;
+ overflow-y: auto;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 3px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #c1d5e8;
+ border-radius: 3px;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: #a3c0e0;
+ }
}
ul {
diff --git a/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.ts b/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.ts
index 1090c7699..1b5f85531 100644
--- a/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.ts
+++ b/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.ts
@@ -31,7 +31,6 @@ type FilesValidation = {
player: DataValidation;
};
-// Interface to track expanded state of file lists
interface ExpandedFilesLists {
booklets: boolean;
units: boolean;
@@ -74,11 +73,6 @@ export class FilesValidationDialogComponent {
}
}
- /**
- * Toggle the expanded state of a file list
- * @param testTaker The test taker identifier
- * @param section The section to toggle
- */
toggleFilesList(testTaker: string, section: keyof ExpandedFilesLists): void {
const sections = this.expandedFilesLists.get(testTaker);
if (sections) {
@@ -86,12 +80,6 @@ export class FilesValidationDialogComponent {
}
}
- /**
- * Check if a file list is expanded
- * @param testTaker The test taker identifier
- * @param section The section to check
- * @returns True if the file list is expanded, false otherwise
- */
isFilesListExpanded(testTaker: string, section: keyof ExpandedFilesLists): boolean {
const sections = this.expandedFilesLists.get(testTaker);
return sections ? sections[section] : false;
From ecfbe16ab349ec55395c8f9a83d5d5b403e670c2 Mon Sep 17 00:00:00 2001
From: jurei733 <67505990+jurei733@users.noreply.github.com>
Date: Wed, 16 Jul 2025 15:52:24 +0200
Subject: [PATCH 04/21] Check files that each unit is used minimum once in a
booklet
---
.../services/workspace-files.service.ts | 47 +++++++++++++------
.../files-validation.component.html | 26 ++++++++--
.../files-validation.component.scss | 46 +++++++++++++++++-
.../files-validation.component.ts | 1 +
4 files changed, 100 insertions(+), 20 deletions(-)
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 fffcbd0da..be02dc0ae 100644
--- a/apps/backend/src/app/database/services/workspace-files.service.ts
+++ b/apps/backend/src/app/database/services/workspace-files.service.ts
@@ -39,6 +39,7 @@ type FileStatus = {
type DataValidation = {
complete: boolean;
missing: string[];
+ unused?: string[];
files: FileStatus[];
};
@@ -55,6 +56,8 @@ export type ValidationResult = {
allUnitsExist: boolean;
missingUnits: string[];
unitFiles: FileStatus[];
+ allUnitsUsedInBooklets: boolean;
+ unusedUnits: string[];
allCodingSchemesExist: boolean;
allCodingDefinitionsExist: boolean;
missingCodingSchemeRefs: string[];
@@ -224,7 +227,12 @@ export class WorkspaceFilesService {
return [{
testTaker: '',
booklets: { complete: false, missing: [], files: [] },
- units: { complete: false, missing: [], files: [] },
+ units: {
+ complete: false,
+ missing: [],
+ unused: [],
+ files: []
+ },
schemes: { complete: false, missing: [], files: [] },
definitions: { complete: false, missing: [], files: [] },
player: { complete: false, missing: [], files: [] }
@@ -252,6 +260,7 @@ export class WorkspaceFilesService {
allUnitsExist,
missingUnits,
unitFiles,
+ unusedUnits,
missingCodingSchemeRefs,
missingDefinitionRefs,
schemeFiles,
@@ -277,8 +286,9 @@ export class WorkspaceFilesService {
files: bookletFiles
},
units: {
- complete: bookletComplete ? allUnitsExist : false,
+ complete: bookletComplete ? (allUnitsExist) : false,
missing: missingUnits,
+ unused: unusedUnits,
files: unitFiles
},
schemes: {
@@ -687,6 +697,18 @@ export class WorkspaceFilesService {
const allUnitIdsArrays = await Promise.all(unitIdsPromises);
const allUnitIds = Array.from(new Set(allUnitIdsArrays.flat()));
+
+ const allUnitsInWorkspace = await this.fileUploadRepository.findBy({
+ file_type: 'Unit',
+ workspace_id: existingBooklets.length > 0 ? existingBooklets[0].workspace_id : null
+ });
+
+ const unusedUnits = allUnitsInWorkspace
+ .filter(unit => !allUnitIds.includes(unit.file_id.toUpperCase()))
+ .map(unit => unit.file_id);
+
+ const allUnitsUsedInBooklets = unusedUnits.length === 0;
+
const chunkSize = 50;
const unitBatches = [];
@@ -696,7 +718,10 @@ export class WorkspaceFilesService {
}
const unitBatchPromises = unitBatches.map(batch => this.fileUploadRepository.find({
- where: { file_id: In(batch) }
+ where: {
+ file_id: In(batch),
+ workspace_id: existingBooklets.length > 0 ? existingBooklets[0].workspace_id : null
+ }
}));
const unitBatchResults = await Promise.all(unitBatchPromises);
@@ -745,9 +770,10 @@ export class WorkspaceFilesService {
const allDefinitionRefs = Array.from(new Set(allRefs.flatMap(ref => ref.definitionRefs)));
const allPlayerRefs = Array.from(new Set(allRefs.flatMap(ref => ref.playerRefs)));
- // Get all resources in a single query
+ // Get all resources in the current workspace
const existingResources = await this.fileUploadRepository.findBy({
- file_type: 'Resource'
+ file_type: 'Resource',
+ workspace_id: existingBooklets.length > 0 ? existingBooklets[0].workspace_id : null
});
const allResourceIds = existingResources.map(resource => resource.file_id);
@@ -794,6 +820,8 @@ export class WorkspaceFilesService {
allUnitsExist,
missingUnits: uniqueUnits,
unitFiles,
+ allUnitsUsedInBooklets,
+ unusedUnits,
allCodingSchemesExist,
allCodingDefinitionsExist,
missingCodingSchemeRefs,
@@ -1770,13 +1798,11 @@ export class WorkspaceFilesService {
}
if (allUnits.length === 0) {
- // No units found for persons in this group
groupsWithResponses.push({ group, hasResponse: false });
allGroupsHaveResponses = false;
continue;
}
- // Get all unit IDs
const unitIds = allUnits.map(unit => unit.id);
if (unitIds.length === 0) {
@@ -1841,7 +1867,6 @@ export class WorkspaceFilesService {
this.logger.log(`Deleting invalid responses for workspace ${workspaceId}: ${responseIds.join(', ')}`);
- // Verify that the responses belong to units that belong to persons in the workspace
const persons = await this.personsRepository.find({
where: { workspace_id: workspaceId }
});
@@ -1851,10 +1876,8 @@ export class WorkspaceFilesService {
return 0;
}
- // Get all person IDs
const personIds = persons.map(person => person.id);
- // Check if personIds array is empty
if (personIds.length === 0) {
this.logger.warn(`No person IDs found for workspace ${workspaceId}`);
return 0;
@@ -1894,7 +1917,6 @@ export class WorkspaceFilesService {
for (let j = 0; j < unitIds.length; j += batchSize) {
const unitIdsBatch = unitIds.slice(j, j + batchSize);
- // Delete responses that match the given IDs and belong to the units in the workspace
const deleteResult = await this.responseRepository.delete({
id: In(responseIdsBatch),
unitid: In(unitIdsBatch)
@@ -1921,7 +1943,6 @@ export class WorkspaceFilesService {
this.logger.log(`Deleting all invalid responses for workspace ${workspaceId} of type ${validationType}`);
- // Get all invalid responses based on the validation type
let invalidResponses: InvalidVariableDto[] = [];
if (validationType === 'variables') {
@@ -1940,7 +1961,6 @@ export class WorkspaceFilesService {
return 0;
}
- // Extract response IDs
const responseIds = invalidResponses
.filter(variable => variable.responseId !== undefined)
.map(variable => variable.responseId as number);
@@ -1950,7 +1970,6 @@ export class WorkspaceFilesService {
return 0;
}
- // Delete the invalid responses
return await this.deleteInvalidResponses(workspaceId, responseIds);
} catch (error) {
this.logger.error(`Error deleting all invalid responses: ${error.message}`, error.stack);
diff --git a/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.html b/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.html
index 7186494d9..ee294ac22 100644
--- a/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.html
+++ b/apps/frontend/src/app/ws-admin/components/files-validation-result/files-validation.component.html
@@ -68,19 +68,35 @@
}
- @if (!val.units.complete) {
-
+ @if (val.units.unused && val.units.unused.length > 0) {
+
- warning
- Fehlende Dateien:
+ warning
+ Nicht verwendete Units:
- @for (item of val.units.missing; track item) {
+ @for (item of val.units.unused; track item) {
- {{ item }}
}
}
+ @if (!val.units.complete) {
+ @if (val.units.missing && val.units.missing.length > 0) {
+
+
+ warning
+ Fehlende Dateien:
+
+
+ @for (item of val.units.missing; track item) {
+ - {{ item }}
+ }
+
+
+ }
+
+ }