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": {