From 3712bcf0dc61b806cc1242f406717e16f7e78ded Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:12:30 +0200 Subject: [PATCH 1/5] Add deletion options for units and responses --- .../test-results/test-results.component.html | 19 ++- .../test-results/test-results.component.scss | 153 +++++++++++++++--- .../test-results/test-results.component.ts | 134 +++++++++++++++ 3 files changed, 277 insertions(+), 29 deletions(-) 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 ecf07e64b..f1e185cb3 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 @@ -215,6 +215,13 @@

Aufgaben

} + } } @@ -262,9 +269,15 @@

Antworten

{{ response.variableid }} {{ response.status }} - +
+ + +
@if (response.expanded) {
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 dafa77fb7..0cc98a6ea 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 @@ -2,10 +2,11 @@ .container { margin: 20px; width: 100%; - max-width: 1600px; height: auto; - min-height: 60vh; + max-height: 80vh; animation: fadeIn 0.3s ease-in-out; + display: flex; + flex-direction: column; } @keyframes fadeIn { @@ -69,19 +70,28 @@ flex-direction: column; gap: 20px; width: 100%; + flex: 1; + height: 100%; + min-height: calc(100vh - 200px); /* Account for margins and header */ @media (min-width: 1200px) { flex-direction: row; - align-items: flex-start; + align-items: stretch; /* Changed from flex-start to stretch */ .data-card { flex: 1; - max-width: 60%; + max-width: 40%; + display: flex; + flex-direction: column; + height: auto; + min-height: calc(100vh - 200px); } .results-section { flex: 1; - max-width: 40%; + max-width: 60%; + height: auto; + min-height: calc(100vh - 200px); } } } @@ -91,38 +101,46 @@ display: flex; flex-direction: column; gap: 20px; + flex: 1; @media (min-width: 992px) { flex-direction: row; - align-items: flex-start; + align-items: stretch; /* Changed from flex-start to stretch */ .booklets-card { flex: 0 0 45%; margin-right: 20px; display: flex; flex-direction: column; - max-height: calc(100vh - 200px); /* Adjust based on your layout */ + height: auto; + min-height: calc(100vh - 200px); /* Adjust based on your layout */ overflow: hidden; /* Hide overflow at the card level */ .accordion { - max-height: 500px; /* Taller height for side-by-side layout */ + max-height: calc(100vh - 300px); /* Dynamic height based on viewport */ overflow-y: auto; /* Enable vertical scrolling */ flex: 1; /* Take remaining space */ @media (min-height: 1024px) { - max-height: 600px; /* Even taller for large screens */ + max-height: calc(100vh - 250px); /* More space for larger screens */ } } } .responses-card, .logs-card { flex: 1; + display: flex; + flex-direction: column; + height: auto; + min-height: calc(100vh - 200px); .var-list, .log-list { - max-height: 500px; /* Taller height for side-by-side layout */ + max-height: calc(100vh - 350px); /* Dynamic height based on viewport */ + overflow-y: auto; + flex: 1; /* Take remaining space */ @media (min-height: 1024px) { - max-height: 600px; /* Even taller for large screens */ + max-height: calc(100vh - 300px); /* More space for larger screens */ } } } @@ -150,6 +168,10 @@ // Table section styles .table-section { width: 100%; + display: flex; + flex-direction: column; + flex: 1; + height: 100%; } // Search container styles @@ -198,11 +220,14 @@ position: relative; overflow-x: auto; overflow-y: auto; - max-height: 400px; /* Default max height for smaller screens */ + max-height: calc(100vh - 350px); /* Dynamic height based on viewport */ border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.08); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); background-color: white; + flex: 1; /* Take remaining space */ + display: flex; + flex-direction: column; .search-loading-indicator { position: absolute; @@ -248,11 +273,11 @@ /* Responsive max-height adjustments */ @media (min-height: 768px) { - max-height: 500px; /* Medium screens */ + max-height: calc(100vh - 300px); /* Medium screens */ } @media (min-height: 1024px) { - max-height: 600px; /* Larger screens */ + max-height: calc(100vh - 250px); /* Larger screens */ } } @@ -417,7 +442,8 @@ .accordion { width: 100%; overflow-y: auto; - max-height: 300px; /* Default max height for smaller screens */ + max-height: calc(100vh - 350px); /* Dynamic height based on viewport */ + flex: 1; /* Take remaining space */ /* Custom scrollbar styling */ &::-webkit-scrollbar { @@ -441,11 +467,11 @@ /* Responsive max-height adjustments */ @media (min-height: 768px) { - max-height: 350px; /* Medium screens */ + max-height: calc(100vh - 300px); /* Medium screens */ } @media (min-height: 1024px) { - max-height: 400px; /* Larger screens */ + max-height: calc(100vh - 250px); /* Larger screens */ } /* Ensure accordion scrolls properly */ @@ -552,7 +578,7 @@ padding: 0; border-radius: 8px; overflow-y: auto; - max-height: 300px; /* Default max height for smaller screens */ + max-height: 350px; /* Default max height for smaller screens - increased */ border: 1px solid rgba(0, 0, 0, 0.05); flex: 1; /* Take remaining space in flex container */ @@ -578,11 +604,11 @@ /* Responsive max-height adjustments */ @media (min-height: 768px) { - max-height: 350px; /* Medium screens */ + max-height: 400px; /* Medium screens - increased */ } @media (min-height: 1024px) { - max-height: 400px; /* Larger screens */ + max-height: 500px; /* Larger screens - increased */ } .unit-item { @@ -591,6 +617,7 @@ border-radius: 0; height: 48px; border-bottom: 1px solid rgba(0, 0, 0, 0.05); + position: relative; &:last-child { border-bottom: none; @@ -601,6 +628,26 @@ padding-left: 4px; } + .delete-unit-button { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + opacity: 0.7; + transition: all 0.2s ease; + + &:hover { + opacity: 1; + background-color: rgba(244, 67, 54, 0.1); + } + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + } + .unit-icon { margin-right: 10px; color: #1976d2; @@ -828,10 +875,11 @@ padding: 0; border-radius: 8px; overflow-y: auto; - max-height: 300px; /* Default max height for smaller screens */ + max-height: calc(100vh - 350px); /* Dynamic height based on viewport */ border: 1px solid rgba(0, 0, 0, 0.05); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04); background-color: white; + flex: 1; /* Take remaining space */ /* Custom scrollbar styling */ &::-webkit-scrollbar { @@ -855,11 +903,11 @@ /* Responsive max-height adjustments */ @media (min-height: 768px) { - max-height: 350px; /* Medium screens */ + max-height: calc(100vh - 300px); /* Medium screens */ } @media (min-height: 1024px) { - max-height: 400px; /* Larger screens */ + max-height: calc(100vh - 250px); /* Larger screens */ } .response-item { @@ -868,6 +916,8 @@ transition: all 0.2s ease; border-radius: 6px; margin-bottom: 4px; + width: 100%; + box-sizing: border-box; &:hover { background-color: #f9fafc; @@ -882,6 +932,8 @@ display: flex; justify-content: space-between; align-items: center; + width: 100%; + box-sizing: border-box; } .response-content { @@ -889,6 +941,31 @@ align-items: center; gap: 14px; flex: 1; + overflow: hidden; /* Hide overflow */ + min-width: 0; /* Allow flex items to shrink below their minimum content size */ + } + + .response-buttons { + display: flex; + align-items: center; + } + + .delete-response-button { + color: #f44336; + opacity: 0.7; + transition: all 0.2s ease; + margin-right: 4px; + + &:hover { + opacity: 1; + background-color: rgba(244, 67, 54, 0.1); + } + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } } .expand-button { @@ -915,6 +992,10 @@ color: #333; font-size: 15px; letter-spacing: 0.2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 40%; /* Limit width to prevent overflow */ } .response-status { @@ -925,6 +1006,10 @@ border-radius: 12px; font-weight: 500; border: 1px solid rgba(25, 118, 210, 0.1); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 30%; /* Limit width to prevent overflow */ } .response-details { @@ -934,6 +1019,8 @@ border-radius: 6px; border: 1px solid rgba(0, 0, 0, 0.05); animation: fadeIn 0.2s ease-in-out; + width: 100%; + box-sizing: border-box; } @keyframes fadeIn { @@ -946,6 +1033,9 @@ margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px dashed rgba(0, 0, 0, 0.05); + width: 100%; + box-sizing: border-box; + flex-wrap: wrap; /* Allow wrapping if content is too wide */ &:last-child { margin-bottom: 0; @@ -970,8 +1060,9 @@ border-radius: 4px; font-size: 13px; flex: 1; - min-width: 300px; - max-width: 600px; + width: calc(100% - 120px); /* Subtract the width of detail-label */ + box-sizing: border-box; + overflow-x: auto; /* Allow horizontal scrolling if needed */ } } } @@ -1067,10 +1158,11 @@ display: flex; flex-direction: column; gap: 14px; - max-height: 400px; + max-height: calc(100vh - 350px); /* Dynamic height based on viewport */ overflow-y: auto; padding: 4px; margin-top: 8px; + flex: 1; /* Take remaining space */ &::-webkit-scrollbar { width: 8px; @@ -1090,6 +1182,15 @@ background: #a3c0e0; } + /* Responsive max-height adjustments */ + @media (min-height: 768px) { + max-height: calc(100vh - 300px); /* Medium screens */ + } + + @media (min-height: 1024px) { + max-height: calc(100vh - 250px); /* Larger screens */ + } + .log-item { padding: 14px 16px; background-color: #f9fafc; 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 db9ec8347..0870f109d 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 @@ -902,4 +902,138 @@ export class TestResultsComponent implements OnInit, OnDestroy { } }); } + + /** + * Deletes a unit after confirmation + * @param unit The unit to delete + * @param booklet The booklet containing the unit + */ + deleteUnit(unit: Unit, booklet: Booklet): void { + if (!unit.id) { + this.snackBar.open( + 'Diese Unit kann nicht gelöscht werden, da sie keine ID hat.', + 'Fehler', + { duration: 3000 } + ); + return; + } + + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '400px', + data: { + title: 'Unit löschen', + content: `Möchten Sie die Unit "${unit.alias || 'Unbenannte Einheit'}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`, + confirmButtonLabel: 'Löschen', + showCancel: true + } + }); + + dialogRef.afterClosed().subscribe(confirmed => { + if (confirmed) { + this.backendService.deleteUnit( + this.appService.selectedWorkspaceId, + unit.id as number + ).subscribe({ + next: result => { + if (result.success) { + // Remove the unit from the booklet's units array + const unitIndex = booklet.units.findIndex(u => u.id === unit.id); + if (unitIndex !== -1) { + booklet.units.splice(unitIndex, 1); + } + + // If this was the selected unit, clear the selection + if (this.selectedUnit && this.selectedUnit.id === unit.id) { + this.selectedUnit = undefined; + this.responses = []; + this.logs = []; + } + + this.snackBar.open( + `Unit "${unit.alias || 'Unbenannte Einheit'}" wurde erfolgreich gelöscht.`, + 'Erfolg', + { duration: 3000 } + ); + } else { + this.snackBar.open( + `Fehler beim Löschen der Unit: ${result.report.warnings.join(', ')}`, + 'Fehler', + { duration: 3000 } + ); + } + }, + error: () => { + this.snackBar.open( + 'Fehler beim Löschen der Unit. Bitte versuchen Sie es später erneut.', + 'Fehler', + { duration: 3000 } + ); + } + }); + } + }); + } + + /** + * Deletes a response after confirmation + * @param response The response to delete + */ + deleteResponse(response: Response): void { + if (!response.id) { + this.snackBar.open( + 'Diese Antwort kann nicht gelöscht werden, da sie keine ID hat.', + 'Fehler', + { duration: 3000 } + ); + return; + } + + const dialogRef = this.dialog.open(ConfirmDialogComponent, { + width: '400px', + data: { + title: 'Antwort löschen', + content: `Möchten Sie die Antwort für Variable "${response.variableid}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`, + confirmButtonLabel: 'Löschen', + showCancel: true + } + }); + + dialogRef.afterClosed().subscribe(confirmed => { + if (confirmed) { + this.backendService.deleteResponse( + this.appService.selectedWorkspaceId, + response.id as number + ).subscribe({ + next: result => { + if (result.success) { + // Remove the response from the responses array + const responseIndex = this.responses.findIndex(r => r.id === response.id); + if (responseIndex !== -1) { + this.responses.splice(responseIndex, 1); + } + + this.snackBar.open( + `Antwort für Variable "${response.variableid}" wurde erfolgreich gelöscht.`, + 'Erfolg', + { duration: 3000 } + ); + } else { + this.snackBar.open( + `Fehler beim Löschen der Antwort: ${result.report.warnings.join(', ')}`, + 'Fehler', + { duration: 3000 } + ); + } + }, + error: () => { + this.snackBar.open( + 'Fehler beim Löschen der Antwort. Bitte versuchen Sie es später erneut.', + 'Fehler', + { duration: 3000 } + ); + } + }); + } + }); + } } From c63345696f8ba9ac668d30f76e4ca25d69f7a993 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:37:47 +0200 Subject: [PATCH 2/5] Validate token in replay component --- apps/frontend/src/app/app.routes.ts | 10 +-- .../components/replay/replay.component.ts | 63 ++++++++++++++++++- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/apps/frontend/src/app/app.routes.ts b/apps/frontend/src/app/app.routes.ts index b089921c2..eb8ce3f83 100755 --- a/apps/frontend/src/app/app.routes.ts +++ b/apps/frontend/src/app/app.routes.ts @@ -1,7 +1,6 @@ import { Routes } from '@angular/router'; import { canActivateAuth } from './auth/auth.guard'; -import { canActivateWithToken } from './auth/token.guard'; export const routes: Routes = [ { path: '', redirectTo: 'home', pathMatch: 'full' }, @@ -11,22 +10,19 @@ export const routes: Routes = [ }, { path: 'replay/:testPerson/:unitId/:page/:anchor', - canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, { path: 'replay/:testPerson/:unitId/:page', - canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, { path: 'replay/:testPerson/:unitId', - canActivate: [canActivateAuth], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, - { path: 'print-view/:unitId', canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, - { path: 'replay/:testPerson', canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, - { path: 'replay', canActivate: [canActivateWithToken], loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, + { path: 'print-view/:unitId', loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, + { path: 'replay/:testPerson', loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, + { path: 'replay', loadComponent: () => import('./replay/components/replay/replay.component').then(m => m.ReplayComponent) }, { path: 'coding-manual', canActivate: [canActivateAuth], loadComponent: () => import('./coding/coding-management-manual/coding-management-manual.component').then(m => m.CodingManagementManualComponent) }, { path: 'admin', diff --git a/apps/frontend/src/app/replay/components/replay/replay.component.ts b/apps/frontend/src/app/replay/components/replay/replay.component.ts index d1b0b4612..df7fba853 100755 --- a/apps/frontend/src/app/replay/components/replay/replay.component.ts +++ b/apps/frontend/src/app/replay/components/replay/replay.component.ts @@ -35,6 +35,8 @@ interface ErrorMessages { notInList: string; notCurrent: string; unknown: string; + tokenExpired: string; + tokenInvalid: string; } @Component({ @@ -98,14 +100,55 @@ export class ReplayComponent implements OnInit, OnDestroy, OnChanges { return auth; } + private validateToken(token: string): { isValid: boolean; errorType?: 'token_expired' | 'token_invalid' } { + if (!token) { + return { isValid: false, errorType: 'token_invalid' }; + } + + try { + // Decode the token to verify it's a valid JWT + const decoded: JwtPayload & { workspace: string } = jwtDecode(token); + + // Check if the token has expired + const currentTime = Math.floor(Date.now() / 1000); + if (decoded.exp && decoded.exp < currentTime) { + return { isValid: false, errorType: 'token_expired' }; + } + + // Check if the token has the required workspace claim + if (!decoded.workspace) { + return { isValid: false, errorType: 'token_invalid' }; + } + + // Token is valid + return { isValid: true }; + } catch (error) { + // Token is invalid (couldn't be decoded) + return { isValid: false, errorType: 'token_invalid' }; + } + } + private subscribeRouter(): void { this.routerSubscription = this.route.params ?.subscribe(async params => { this.resetSnackBars(); this.resetUnitData(); this.authToken = await this.getAuthToken(); + + if (this.authToken) { + const tokenValidation = this.validateToken(this.authToken); + if (!tokenValidation.isValid) { + this.setIsLoaded(); + if (tokenValidation.errorType === 'token_expired') { + this.openErrorSnackBar(this.getErrorMessages().tokenExpired, 'Schließen'); + } else { + this.openErrorSnackBar(this.getErrorMessages().tokenInvalid, 'Schließen'); + } + return; + } + } + try { - // Check if we're in print-view mode const url = this.route.snapshot.url; this.isPrintMode = url.length > 0 && url[0].path === 'print-view'; @@ -194,6 +237,22 @@ export class ReplayComponent implements OnInit, OnDestroy, OnChanges { } this.resetUnitData(); this.resetSnackBars(); + + // Validate the token if it exists + if (this.authToken) { + const tokenValidation = this.validateToken(this.authToken); + if (!tokenValidation.isValid) { + this.setIsLoaded(); + // Show appropriate error message based on validation result + if (tokenValidation.errorType === 'token_expired') { + this.openErrorSnackBar(this.getErrorMessages().tokenExpired, 'Schließen'); + } else { + this.openErrorSnackBar(this.getErrorMessages().tokenInvalid, 'Schließen'); + } + return Promise.resolve(); + } + } + const { unitIdInput } = changes; try { this.unitId = unitIdInput.currentValue; @@ -326,6 +385,8 @@ export class ReplayComponent implements OnInit, OnDestroy, OnChanges { ResponsesError: `Keine Antworten für Aufgabe "${this.unitId}" von Testperson "${this.testPerson}" gefunden`, notInList: `Keine valide Seite mit ID "${this.page}" gefunden`, notCurrent: `Seite mit ID "${this.page}" kann nicht ausgewählt werden`, + tokenExpired: 'Das Authentisierungs-Token ist abgelaufen', + tokenInvalid: 'Das Authentisierungs-Token ist ungültig', unknown: 'Unbekannter Fehler' }; } From a2c8b18ee827ebc491dd6cff14c5ea9142055fba Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:14:57 +0200 Subject: [PATCH 3/5] Add indexes defined in entities to the database --- .../database/entities/bookletInfo.entity.ts | 3 +- .../app/database/entities/persons.entity.ts | 1 + .../app/database/entities/response.entity.ts | 1 + .../changelog/coding-box.changelog-0.8.2.sql | 31 +++++++++++++++++++ .../changelog/coding-box.changelog-root.xml | 1 + 5 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 database/changelog/coding-box.changelog-0.8.2.sql diff --git a/apps/backend/src/app/database/entities/bookletInfo.entity.ts b/apps/backend/src/app/database/entities/bookletInfo.entity.ts index 0cb0c65e4..10a227621 100644 --- a/apps/backend/src/app/database/entities/bookletInfo.entity.ts +++ b/apps/backend/src/app/database/entities/bookletInfo.entity.ts @@ -1,9 +1,10 @@ import { - Entity, Column, PrimaryGeneratedColumn, Unique + Entity, Column, PrimaryGeneratedColumn, Unique, Index } from 'typeorm'; @Entity('bookletinfo') @Unique('bookletinfo_pk', ['name']) +@Index(['name']) export class BookletInfo { @PrimaryGeneratedColumn() diff --git a/apps/backend/src/app/database/entities/persons.entity.ts b/apps/backend/src/app/database/entities/persons.entity.ts index 685ff91ba..840b33297 100755 --- a/apps/backend/src/app/database/entities/persons.entity.ts +++ b/apps/backend/src/app/database/entities/persons.entity.ts @@ -9,6 +9,7 @@ import { Booklet } from './booklet.entity'; @Unique('persons_pk', ['code', 'group', 'login']) @Index(['workspace_id', 'code']) // Composite index for common query patterns @Index(['workspace_id', 'group']) // Composite index for filtering by group within workspace +@Index(['login', 'code', 'workspace_id']) // Composite index for findUnitResponse query class Persons { @PrimaryGeneratedColumn() diff --git a/apps/backend/src/app/database/entities/response.entity.ts b/apps/backend/src/app/database/entities/response.entity.ts index f2b245d0d..d211a0e03 100644 --- a/apps/backend/src/app/database/entities/response.entity.ts +++ b/apps/backend/src/app/database/entities/response.entity.ts @@ -9,6 +9,7 @@ import { Unit } from './unit.entity'; @Index(['unitid', 'variableid']) // Composite index for common query patterns @Index(['unitid', 'status']) // Composite index for filtering by status @Index(['codedstatus']) // Index for filtering by coded status +@Index(['value']) // Index for searching by value export class ResponseEntity { @PrimaryGeneratedColumn() id: number; diff --git a/database/changelog/coding-box.changelog-0.8.2.sql b/database/changelog/coding-box.changelog-0.8.2.sql new file mode 100644 index 000000000..3ad150587 --- /dev/null +++ b/database/changelog/coding-box.changelog-0.8.2.sql @@ -0,0 +1,31 @@ +-- liquibase formatted sql + +-- changeset jurei733:1 +-- Add missing indexes for bookletInfo entity +CREATE INDEX IF NOT EXISTS "idx_bookletinfo_name" ON "public"."bookletinfo" ("name"); +-- rollback DROP INDEX IF EXISTS "idx_bookletinfo_name"; + +-- changeset jurei733:2 +-- Add missing indexes for persons entity +CREATE INDEX IF NOT EXISTS "idx_persons_workspace_code" ON "public"."persons" ("workspace_id", "code"); +CREATE INDEX IF NOT EXISTS "idx_persons_workspace_group" ON "public"."persons" ("workspace_id", "group"); +CREATE INDEX IF NOT EXISTS "idx_persons_login_code_workspace" ON "public"."persons" ("login", "code", "workspace_id"); +-- rollback DROP INDEX IF EXISTS "idx_persons_workspace_code"; DROP INDEX IF EXISTS "idx_persons_workspace_group"; DROP INDEX IF EXISTS "idx_persons_login_code_workspace"; + +-- changeset jurei733:3 +-- Add missing indexes for response entity +CREATE INDEX IF NOT EXISTS "idx_response_unitid_variableid" ON "public"."response" ("unitid", "variableid"); +CREATE INDEX IF NOT EXISTS "idx_response_unitid_status" ON "public"."response" ("unitid", "status"); +CREATE INDEX IF NOT EXISTS "idx_response_codedstatus" ON "public"."response" ("codedstatus"); +CREATE INDEX IF NOT EXISTS "idx_response_value" ON "public"."response" ("value"); +-- rollback DROP INDEX IF EXISTS "idx_response_unitid_variableid"; DROP INDEX IF EXISTS "idx_response_unitid_status"; DROP INDEX IF EXISTS "idx_response_codedstatus"; DROP INDEX IF EXISTS "idx_response_value"; + +-- changeset jurei733:4 +-- Add missing indexes for unit entity +CREATE INDEX IF NOT EXISTS "idx_unit_bookletid_alias" ON "public"."unit" ("bookletid", "alias"); +-- rollback DROP INDEX IF EXISTS "idx_unit_bookletid_alias"; + +-- changeset jurei733:5 +-- Add missing indexes for booklet entity +CREATE INDEX IF NOT EXISTS "idx_booklet_personid_infoid" ON "public"."booklet" ("personid", "infoid"); +-- rollback DROP INDEX IF EXISTS "idx_booklet_personid_infoid"; diff --git a/database/changelog/coding-box.changelog-root.xml b/database/changelog/coding-box.changelog-root.xml index 4b9aad43d..61e669159 100644 --- a/database/changelog/coding-box.changelog-root.xml +++ b/database/changelog/coding-box.changelog-root.xml @@ -13,5 +13,6 @@ + From 359f362f4864359785da8ac8c07579b3193322c0 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:45:00 +0200 Subject: [PATCH 4/5] Show no coding code,score if no coded status --- .../components/test-results/test-results.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f1e185cb3..2e48a92e1 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 @@ -281,13 +281,13 @@

Antworten

@if (response.expanded) {
- @if (response.code) { + @if (response.code && response.codedstatus) {
Code: {{ response.code }}
} - @if (response.score) { + @if (response.score && response.codedstatus) {
Score: {{ response.score }} From cbf8fc8e6e54aa744e7a6283306e6bd06c15db42 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:55:08 +0200 Subject: [PATCH 5/5] Set version to 0.8.2 --- apps/frontend/src/app/components/home/home.component.html | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/app/components/home/home.component.html b/apps/frontend/src/app/components/home/home.component.html index dc4a4c794..c7927dae0 100755 --- a/apps/frontend/src/app/components/home/home.component.html +++ b/apps/frontend/src/app/components/home/home.component.html @@ -9,7 +9,7 @@ [appTitle]="'Web application for coding'" [introHtml]="'appService.appConfig.introHtml'" [appName]="'IQB-Kodierbox'" - [appVersion]="'0.8.1'" + [appVersion]="'0.8.2'" [userName]="authData.userName" [userLongName]="appService.userProfile.firstName + ' ' + appService.userProfile.lastName" [isUserLoggedIn]="Number(authData.userId) > 0" diff --git a/package-lock.json b/package-lock.json index 0765b35bc..1daebaf0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coding-box", - "version": "0.8.1", + "version": "0.8.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coding-box", - "version": "0.8.1", + "version": "0.8.2", "license": "MIT", "dependencies": { "@angular/animations": "20.0.3", diff --git a/package.json b/package.json index a394f8bef..e2dd34ad0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coding-box", - "version": "0.8.1", + "version": "0.8.2", "author": "IQB - Institut zur Qualitätsentwicklung im Bildungswesen", "license": "MIT", "scripts": {