Repository: SELab-2/Dwengo-3
Pull request #375: FEAT - edit class group page implemented and linked to api
Author: login: RobinMeersman
Created: 2025-05-08T07:49:48Z | Merged: 2025-05-18T18:16:46Z

Closes #308 

Page on desktop:
<img width="1512" alt="image" src="https://github.com/user-attachments/assets/cd1c1ec3-961c-4906-bebc-6bc3b98182c3" />

Page on a smaller device (e.g. mobile):
![image](https://github.com/user-attachments/assets/84790fb4-7c36-4928-8bcb-901ecae983be)


Linked issue #308: Klasgroep - Bewerken (Leerkracht)
Linked issue comments: user: login: martijnheeren
created_at: 2025-04-30T06:39:53Z
body: Volgens mij is dit niet prioritair, en ik zou het naar p2 verplaatsen
author_association: CONTRIBUTOR

Diff:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 023b704d..1b8b498d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,6 +16,10 @@ jobs:
   install:
     runs-on: self-hosted
     steps:
+      - name: Clean workspace
+        run: |
+          rm -rf /home/selab2/actions-runner/_work/Dwengo-3/Dwengo-3/*
+
       - name: Checkout code
         uses: actions/checkout@v4
 
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 77ed47da..da5d14af 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -28,6 +28,7 @@ import ClassAddPage from './views/ClassAddPage.tsx';
 import { AppRoutes } from './util/app.routes.ts';
 import DiscussionCreatePage from './views/DiscussionCreatePage.tsx';
 import ClassDiscussionsPage from './views/ClassDiscussionsPage.tsx';
+import { ClassGroupEditPage } from './views/ClassGroupEditPage.tsx';
 import ProtectContent from './components/ProtectContent.tsx';
 import { NotificationProvider } from './contexts/NotificationContext.tsx';
 import AnnouncementCreatePage from './views/AnnouncemntCreatePage.tsx';
@@ -110,12 +111,15 @@ function App() {
                           path={AppRoutes.classDiscussions(':classId')}
                           element={<ClassDiscussionsPage />}
                         />
+                        <Route
+                          path={AppRoutes.classEdit(':classId')}
+                          element={<ClassGroupEditPage />}
+                        />
                         <Route
                           path={AppRoutes.discussionCreate(':classId')}
                           element={<DiscussionCreatePage />}
                         />
                       </Route>
-
                       {/* Redirect all other routes to an errorpage */}
                       <Route path="*" element={<ErrorPage />} />
                     </Routes>
diff --git a/client/src/components/UserDataTableComponent.tsx b/client/src/components/UserDataTableComponent.tsx
new file mode 100644
index 00000000..3990ec47
--- /dev/null
+++ b/client/src/components/UserDataTableComponent.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import {
+  Box,
+  Button,
+  Paper,
+  Table,
+  TableBody,
+  TableCell,
+  TableContainer,
+  TableHead,
+  TableRow,
+  Typography,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+interface DataType {
+  name: string;
+  surname: string;
+  id: string;
+}
+
+interface Props {
+  data: DataType[];
+  title: string;
+  onActionPressed: (id: string) => Promise<void>;
+}
+
+export function UserDataTableComponent(props: Props) {
+  const { t } = useTranslation();
+
+  return (
+    <Box
+      sx={{
+        width: '100%',
+        display: 'flex',
+        flexDirection: 'column',
+        alignItems: 'center',
+      }}
+    >
+      <Box
+        sx={{
+          width: '80%',
+        }}
+      >
+        <Box
+          sx={{
+            width: '100%',
+            display: 'flex',
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Typography variant={'h4'}>{props.title}</Typography>
+        </Box>
+        <TableContainer component={Paper}>
+          <Table>
+            <TableHead>
+              <TableRow>
+                <TableCell>
+                  <Typography variant={'h6'} align={'center'}>
+                    {t('name')}
+                  </Typography>
+                </TableCell>
+                <TableCell>
+                  <Typography variant={'h6'} align={'center'}>
+                    {t('action')}
+                  </Typography>
+                </TableCell>
+              </TableRow>
+            </TableHead>
+            <TableBody>
+              {props.data.map((item, index) => {
+                return (
+                  <TableRow>
+                    <TableCell>
+                      <Typography>{item.name + ' ' + item.surname}</Typography>
+                    </TableCell>
+                    <TableCell sx={{ textAlign: 'center' }}>
+                      <Button onClick={() => props.onActionPressed(item.id)}>{t('delete')}</Button>
+                    </TableCell>
+                  </TableRow>
+                );
+              })}
+            </TableBody>
+          </Table>
+        </TableContainer>
+      </Box>
+    </Box>
+  );
+}
diff --git a/client/src/hooks/useClass.ts b/client/src/hooks/useClass.ts
index 5dc7efa0..e452c15b 100644
--- a/client/src/hooks/useClass.ts
+++ b/client/src/hooks/useClass.ts
@@ -1,5 +1,12 @@
 import { useMutation, useQuery } from '@tanstack/react-query';
-import { createClass, fetchClassById, fetchClasses, fetchPopulatedClassById } from '../api/class';
+import {
+  createClass,
+  deleteStudentFromClass,
+  deleteTeacherFromClass,
+  fetchClassById,
+  fetchClasses,
+  fetchPopulatedClassById,
+} from '../api/class';
 import { ClassDetail, ClassRoleEnum, PopulatedClass } from '../util/interfaces/class.interfaces';
 import { ApiRoutes } from '../api/api.routes';
 import apiClient from '../api/apiClient';
@@ -261,3 +268,19 @@ export function usePopulatedClassesById(
     refetchOnWindowFocus: false,
   });
 }
+
+export function useDeleteTeacherFromClass() {
+  return useMutation({
+    mutationFn: async ({ classId, teacherId }: { classId: string; teacherId: string }) => {
+      return await deleteTeacherFromClass(classId, teacherId);
+    },
+  });
+}
+
+export function useDeleteStudentFromClass() {
+  return useMutation({
+    mutationFn: async ({ classId, studentId }: { classId: string; studentId: string }) => {
+      return await deleteStudentFromClass(classId, studentId);
+    },
+  });
+}
diff --git a/client/src/util/app.routes.ts b/client/src/util/app.routes.ts
index 1d1b32fe..4223f50c 100644
--- a/client/src/util/app.routes.ts
+++ b/client/src/util/app.routes.ts
@@ -34,6 +34,7 @@ export const AppRoutes = {
   learningThemes: '/learning-themes',
   learningTheme: (id: string) => `/learning-themes/${id}`,
   class: (id: string) => `/class/${id}`,
+  classEdit: (classId: string) => `/class/${classId}/edit`,
   classStudentDetails: (classId: string, studentId: string) =>
     `/class/${classId}/student/${studentId}`,
   classAdd: '/class/add',
diff --git a/client/src/util/interfaces/class.interfaces.ts b/client/src/util/interfaces/class.interfaces.ts
index 65e24cea..30b8dbf4 100644
--- a/client/src/util/interfaces/class.interfaces.ts
+++ b/client/src/util/interfaces/class.interfaces.ts
@@ -15,6 +15,7 @@ export interface ClassShort {
 export interface ClassDetail {
   id: string;
   name: string;
+  description: string;
   students: StudentShort[];
   teachers: TeacherShort[];
   assignments: AssignmentShort[];
@@ -33,4 +34,5 @@ export interface ClassCreate {
 
 export interface ClassUpdate {
   name?: string;
+  description?: string;
 }
diff --git a/client/src/util/locale/en.ts b/client/src/util/locale/en.ts
index 4279b8bf..f83423e5 100644
--- a/client/src/util/locale/en.ts
+++ b/client/src/util/locale/en.ts
@@ -64,6 +64,7 @@ export const en = {
     noDiscussionsFound: 'No discussions found',
     firstMessage: 'First message',
     students: 'Students',
+    teachers: 'Teachers',
     details: 'Details',
     admissionRequests: 'Admission Requests',
     approve: 'Approve',
@@ -88,6 +89,8 @@ export const en = {
     submission: 'Submission',
     back: 'Back',
     score: 'Score',
+    editClass: 'Editing class',
+    classDescription: 'Class description',
     sendNewMessage: 'Send new message',
     newMessage: 'New message',
     noMessegesYet: 'No messages yet',
diff --git a/client/src/util/locale/fr.ts b/client/src/util/locale/fr.ts
index 7ad7c002..3b94194b 100644
--- a/client/src/util/locale/fr.ts
+++ b/client/src/util/locale/fr.ts
@@ -64,6 +64,7 @@ export const fr = {
     noDiscussionsFound: 'Aucune discussion trouvée',
     firstMessage: 'Premier message',
     students: 'Étudiants',
+    teachers: 'Enseignants',
     details: 'Détails',
     admissionRequests: "Demandes d'admission",
     approve: 'Approuver',
@@ -88,6 +89,8 @@ export const fr = {
     submission: 'Soumission',
     back: 'Retourner',
     score: 'Score',
+    editClass: 'Modifier la classe',
+    classDescription: 'Description de la classe',
     sendNewMessage: 'Envoyer un nouveau message',
     newMessage: 'Nouveau message',
     noMessagesYet: 'Pas encore de messages',
diff --git a/client/src/util/locale/nl.ts b/client/src/util/locale/nl.ts
index 277bad05..b7e5f4ee 100644
--- a/client/src/util/locale/nl.ts
+++ b/client/src/util/locale/nl.ts
@@ -61,6 +61,7 @@ export const nl = {
     noDiscussionsFound: 'Geen discussies gevonden',
     firstMessage: 'Eerste bericht',
     students: 'Studenten',
+    teachers: 'Leerkrachten',
     details: 'Details',
     admissionRequests: 'Toelatingsverzoeken',
     approve: 'Goedkeuren',
@@ -85,6 +86,8 @@ export const nl = {
     submission: 'Indiening',
     back: 'Terug',
     score: 'Score',
+    editClass: 'Klas bewerken',
+    classDescription: 'Klasbeschrijving',
     sendNewMessage: 'Verzend nieuw bericht',
     newMessage: 'Nieuw bericht',
     noMessegesYet: 'Nog geen berichten',
diff --git a/client/src/views/ClassDashboardPage.tsx b/client/src/views/ClassDashboardPage.tsx
index 31cd7b53..302b2311 100644
--- a/client/src/views/ClassDashboardPage.tsx
+++ b/client/src/views/ClassDashboardPage.tsx
@@ -190,25 +190,21 @@ function ClassDashboardPage() {
             </List>
 
             <Typography variant="h6" sx={{ mt: 3, fontWeight: 'bold' }}>
-              {t('notes')}
+              {t('classDescription')}
             </Typography>
             <Typography variant="body2" sx={{ color: 'gray' }}>
-              {classData!.notes || // TODO add notes to class
-                'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'}
+              {classData!.description}
             </Typography>
 
-            {teacher && (
-              <Button
-                variant="contained"
-                sx={{ mt: 3, bgcolor: '#424242', color: 'white' }}
-                onClick={() => {
-                  // TODO: add link to edit class page
-                  alert('TODO: Navigate to Edit Class Page');
-                }} // Replace with actual navigation
-              >
-                {t('editClassGroup')}
-              </Button>
-            )}
+            <Button
+              variant="contained"
+              sx={{ mt: 3, bgcolor: '#424242', color: 'white' }}
+              onClick={() => {
+                navigate(AppRoutes.classEdit(classId!));
+              }}
+            >
+              {t('editClassGroup')}
+            </Button>
           </Paper>
         </GridLegacy>
 
diff --git a/client/src/views/ClassGroupEditPage.tsx b/client/src/views/ClassGroupEditPage.tsx
new file mode 100644
index 00000000..3aa5008d
--- /dev/null
+++ b/client/src/views/ClassGroupEditPage.tsx
@@ -0,0 +1,199 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { AppBar, Box, Button, Paper, TextField, Typography } from '@mui/material';
+import {
+  useClassById,
+  useDeleteStudentFromClass,
+  useDeleteTeacherFromClass,
+} from '../hooks/useClass.ts';
+import theme from '../util/theme.ts';
+import { MarginSize } from '../util/size.ts';
+import { AppRoutes } from '../util/app.routes.ts';
+import { UserDataTableComponent } from '../components/UserDataTableComponent.tsx';
+import { updateClass } from '../api/class.ts';
+import { useAuth } from '../hooks/useAuth.ts';
+import { useError } from '../hooks/useError.ts';
+
+export function ClassGroupEditPage() {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const { user } = useAuth();
+
+  const { classId } = useParams<{ classId: string }>();
+  const {
+    data: classData,
+    isLoading: isClassDataLoading,
+    refetch: refetchClassData,
+  } = useClassById(classId!);
+  const error = useError();
+  const [className, setClassName] = useState<string>('');
+  const [classDescription, setClassDescription] = useState<string>('');
+  const deleteStudent = useDeleteStudentFromClass();
+  const deleteTeacher = useDeleteTeacherFromClass();
+
+  useEffect(() => {
+    if (classData) {
+      setClassName(classData.name);
+      setClassDescription(classData.description);
+    }
+  }, [classData]);
+
+  return (
+    <Box sx={{ width: '100vw' }}>
+      <Paper
+        elevation={3}
+        sx={{
+          m: '2% auto',
+          borderRadius: 2,
+          overflow: 'hidden',
+          width: '90%',
+        }}
+      >
+        <AppBar
+          position="static"
+          sx={{
+            p: MarginSize.small,
+            backgroundColor: theme.palette.primary.main,
+            display: 'flex',
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Typography variant="h5" sx={{ fontWeight: 'bold', color: 'white' }}>
+            {t('editClass')}
+          </Typography>
+
+          <Button
+            onClick={() => {
+              navigate(AppRoutes.class(classId!));
+            }}
+            sx={{ color: 'white' }}
+          >
+            {t('cancel')}
+          </Button>
+        </AppBar>
+      </Paper>
+
+      <Box
+        sx={{
+          width: '100%',
+          display: 'flex',
+          flexDirection: { xs: 'column', md: 'row' },
+          justifyContent: { xs: 'center', md: 'space-evenly' },
+          alignItems: 'center',
+        }}
+      >
+        {!isClassDataLoading ? (
+          <>
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 3,
+                m: MarginSize.small,
+                width: { xs: '80%', md: 'calc(100% / 3)' },
+                height: '100%',
+              }}
+            >
+              <TextField
+                label={t('className')}
+                sx={{ width: '100%' }}
+                value={className}
+                onChange={(e: any) => setClassName(e.target.value)}
+              />
+              <TextField
+                multiline={true}
+                rows={6}
+                sx={{ width: '100%' }}
+                label={t('classDescription')}
+                value={classDescription}
+                onChange={(e: any) => setClassDescription(e.target.value)}
+              />
+              <Box sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}>
+                <Button
+                  onClick={async () => {
+                    await updateClass(classId!, {
+                      name: className,
+                      description: classDescription,
+                    });
+                    navigate(AppRoutes.class(classId!));
+                  }}
+                  sx={{
+                    color: theme.palette.primary.main,
+                    border: `1px solid ${theme.palette.primary.main}`,
+                  }}
+                >
+                  {t('save')}
+                </Button>
+              </Box>
+            </Box>
+
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                gap: 5,
+                width: '100%',
+                p: 3,
+              }}
+            >
+              <UserDataTableComponent
+                data={classData!!.students.map((student) => {
+                  return {
+                    id: student.id,
+                    name: student.user.name,
+                    surname: student.user.surname,
+                  };
+                })}
+                title={t('students')}
+                onActionPressed={async (id: string) => {
+                  deleteStudent.mutate(
+                    {
+                      classId: classId!!,
+                      studentId: id,
+                    },
+                    {
+                      onError: (e) => {
+                        error.setError(e.message);
+                      },
+                    },
+                  );
+                }}
+              />
+
+              {/* Teachers */}
+              <UserDataTableComponent
+                data={classData!!.teachers.map((teacher) => {
+                  return {
+                    id: teacher.id,
+                    name: teacher.user.name,
+                    surname: teacher.user.surname,
+                  };
+                })}
+                title={t('teachers')}
+                onActionPressed={async (id: string) => {
+                  deleteTeacher.mutate(
+                    {
+                      classId: classId!!,
+                      teacherId: id,
+                    },
+                    {
+                      onError: (e) => {
+                        error.setError(e.message);
+                      },
+                    },
+                  );
+                }}
+              />
+            </Box>
+          </>
+        ) : (
+          <Typography>{t('loading')}</Typography>
+        )}
+      </Box>
+    </Box>
+  );
+}
diff --git a/db/prisma/migrations/20250516084537_/migration.sql b/db/prisma/migrations/20250518181041_tests_fix/migration.sql
similarity index 99%
rename from db/prisma/migrations/20250516084537_/migration.sql
rename to db/prisma/migrations/20250518181041_tests_fix/migration.sql
index 031e8cc2..4e440ab2 100644
--- a/db/prisma/migrations/20250516084537_/migration.sql
+++ b/db/prisma/migrations/20250518181041_tests_fix/migration.sql
@@ -128,6 +128,7 @@ CREATE TABLE "Teacher" (
 CREATE TABLE "Class" (
     "id" TEXT NOT NULL,
     "name" TEXT NOT NULL DEFAULT 'New class',
+    "description" TEXT NOT NULL DEFAULT '',
 
     CONSTRAINT "Class_pkey" PRIMARY KEY ("id")
 );
diff --git a/db/prisma/schema.prisma b/db/prisma/schema.prisma
index f9f3bd5e..82fa7bb9 100644
--- a/db/prisma/schema.prisma
+++ b/db/prisma/schema.prisma
@@ -153,6 +153,7 @@ model Teacher {
 model Class {
   id                String             @id @default(uuid())
   name              String             @default("New class")
+  description       String             @default("")
   students          Student[]
   teachers          Teacher[]
   assignments       Assignment[]
diff --git a/server/domain/class.domain.ts b/server/domain/class.domain.ts
index 6965bc05..af208278 100644
--- a/server/domain/class.domain.ts
+++ b/server/domain/class.domain.ts
@@ -98,6 +98,10 @@ export class ClassDomain {
       throw new BadRequestError(40035);
     }
 
+    if (teacherId === user.teacher.id) {
+      throw new BadRequestError(40052);
+    }
+
     if (!(await this.classPersistence.isTeacherFromClass(user.teacher.id, classId))) {
       throw new BadRequestError(40035);
     }
diff --git a/server/util/selectInput/select.ts b/server/util/selectInput/select.ts
index 5fe85856..23b5ee68 100644
--- a/server/util/selectInput/select.ts
+++ b/server/util/selectInput/select.ts
@@ -196,6 +196,7 @@ export const assignmentSelectShort = {
 export const classSelectDetail = {
   id: true,
   name: true,
+  description: true,
   teachers: {
     select: teacherSelectShort,
   },
diff --git a/server/util/types/class.types.ts b/server/util/types/class.types.ts
index d40f6153..8843dd15 100644
--- a/server/util/types/class.types.ts
+++ b/server/util/types/class.types.ts
@@ -22,6 +22,7 @@ export type ClassCreateParams = z.infer<typeof ClassCreateSchema>;
 
 export const ClassUpdateSchema = z.object({
   name: z.string().min(1, 'Name must be a non-empty string').trim().optional(),
+  description: z.string().min(1, 'Description must be a non-empty string').trim().optional(),
 });
 
 export type ClassUpdateParams = z.infer<typeof ClassUpdateSchema>;
diff --git a/server/util/types/error.types.ts b/server/util/types/error.types.ts
index 2759d1fa..b82efb9c 100644
--- a/server/util/types/error.types.ts
+++ b/server/util/types/error.types.ts
@@ -209,6 +209,8 @@ export class BadRequestError extends APIError {
         return "Can't fetch info from an assignment you're not a member of...";
       case 40051:
         return 'A group can only have one discussion...';
+      case 40052:
+        return 'You cannot remove yourself from your own class...';
       default:
         return 'Bad request...';
     }


Reviews:
user: login: martijnheeren
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T07:12:12Z
user: login: RobinMeersman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:52:25Z
user: login: martijnheeren
body: Ziet er goed uit, heb juist een paar vragen in de comments gezet. 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T18:15:10Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements the "edit class group" feature and refactors several UI components and API hooks to support file submissions and improved class management. Key changes include the addition of a new ClassGroupEditPage, updates to navigation routes (including a new classEdit route), and various formatting and consistency improvements across components and locale files.

### Reviewed Changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                   | Description                                                      |
| -------------------------------------- | ---------------------------------------------------------------- |
| client/src/views/LearningPathPage.tsx  | Formatting updates and minor refactoring in file submission flow.  |
| client/src/views/ClassStudentDetails.tsx | Minor improvements in navigation callback formatting.             |
| client/src/views/ClassGroupEditPage.tsx | New view for editing class groups with updated UI and API calls.    |
| client/src/views/ClassDashboardPage.tsx | Updated UI texts and navigation for class descriptions and editing. |
| client/src/views/ClassAssignmentPage.tsx  | Formatting improvements in handling deadlines and navigation.       |
| client/src/util/locale/*.{ts}            | Updated locale files with new keys and revised texts.              |
| client/src/util/interfaces/class.interfaces.ts | Added a description field to class-related interfaces.         |
| client/src/util/app.routes.ts           | Added a new route for editing classes.                             |
| client/src/hooks/useAssignmentSubmission.ts | Consistent formatting and code style updates in hooks.             |
| client/src/components/*                 | Various UI component formatting and minor refactoring.             |
| client/src/api/assignmentSubmission.ts   | Minor formatting updates in API for handling file submissions.      |
| client/src/App.tsx                      | Added new route integration for the class group edit page.         |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**client/src/api/assignmentSubmission.ts:68**
* The anchor element 'a' is never appended to the DOM before calling 'a.click()' and attempting to remove it, which may cause a runtime error. Append the element to the DOM (e.g., using document.body.appendChild(a)) before triggering the click, then remove it after use.
```
document.body.removeChild(a);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T18:15:45Z
user: login: RobinMeersman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:36:21Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR implements the edit class group page and links it to the API, allowing users to update class details (including a new description field) and preventing teachers from accidentally removing themselves from a class.  
- Adds a new error code (40052) and corresponding message in the API for teacher self-removal prevention.  
- Updates class types, database schema, and API routes to include a class description.  
- Introduces a new ClassGroupEditPage and updates locale files, interfaces, and navigation routes to support editing.

### Reviewed Changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| server/util/types/error.types.ts | Added a new error message (40052) for self-removal attempt. |
| server/util/types/class.types.ts | Added a new "description" field in the class update schema. |
| server/util/selectInput/select.ts | Included the "description" property in class select details. |
| server/domain/class.domain.ts | Introduced a check to prevent teachers from removing themselves. |
| db/prisma/schema.prisma & migration.sql | Updated the schema to include a description column for classes. |
| client/src/views/ClassGroupEditPage.tsx | Implemented the new edit page with form fields to update class details. |
| client/src/views/ClassDashboardPage.tsx | Updated UI labels and navigation to link to the edit page. |
| client/src/util/locale/* | Updated locale files with new translation keys for teachers, editClass, and classDescription. |
| client/src/util/interfaces/class.interfaces.ts | Added a "description" property to class interfaces. |
| client/src/util/app.routes.ts | Added a new route function for class editing. |
| client/src/hooks/useClass.ts | Added hooks for deleting teachers/students from a class. |
| client/src/components/UserDataTableComponent.tsx | Created a component to list and delete users from a class. |
| client/src/App.tsx | Registered the new route for class editing. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:24:23Z
user: login: martijnheeren
body: De student moet de button om de klas te bewerken niet kunnen zien. For some reason krijg ik als student geen assignments te zien. Kan jij eens testen wat er gebeurt als je een student verwijdert terwijl die een assignment heeft? 
Als je jezelf probeert te verwijderen krijg je ook gewoon een errorcode en geen message die zegt "je kan jezelf niet verwijderen" ofso
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-17T07:33:07Z

Review comments:
diff_hunk: @@ -105,6 +106,59 @@ function App() {
                           element={<ClassDiscussionsPage />}
                         />
                       </Route>
+                      {/* Protected Routes */}
+                      <Route>
path: client/src/App.tsx
user: login: martijnheeren
body: Deze protected routes staan toch al in die file?
created_at: 2025-05-09T07:12:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -105,6 +106,59 @@ function App() {
                           element={<ClassDiscussionsPage />}
                         />
                       </Route>
+                      {/* Protected Routes */}
+                      <Route>
path: client/src/App.tsx
user: login: RobinMeersman
body: Dat zal dan fout gelopen zijn via de merge conflicts. Ik pas dit straks aan :+1: 
created_at: 2025-05-09T09:52:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { AppBar, Box, Button, Paper, TextField, Typography } from '@mui/material';
+import { useClassById } from '../hooks/useClass.ts';
+import theme from '../util/theme.ts';
+import { MarginSize } from '../util/size.ts';
+import { AppRoutes } from '../util/app.routes.ts';
+import { UserDataTableComponent } from '../components/UserDataTableComponent.tsx';
+import { deleteStudentFromClass, deleteTeacherFromClass, updateClass } from '../api/class.ts';
+import { useAuth } from '../hooks/useAuth.ts';
+import { useError } from '../hooks/useError.ts';
+import { LoadingButton } from '@mui/lab';
+
+export function ClassGroupEditPage() {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const { user } = useAuth();
+
+  const { classId } = useParams<{ classId: string }>();
+  const {
+    data: classData,
+    isLoading: isClassDataLoading,
+    refetch: refetchClassData,
+  } = useClassById(classId!);
+  const error = useError();
+  const [className, setClassName] = useState<string>('');
+  const [classDescription, setClassDescription] = useState<string>('');
+
+  useEffect(() => {
+    if (classData) {
+      setClassName(classData.name);
+      setClassDescription(classData.description);
+    }
+  }, [classData]);
+
+  return (
+    <Box sx={{ width: '100vw' }}>
+      <Paper
+        elevation={3}
+        sx={{
+          m: '2% auto',
+          borderRadius: 2,
+          overflow: 'hidden',
+          width: '90%',
+        }}
+      >
+        <AppBar
+          position="static"
+          sx={{
+            p: MarginSize.small,
+            backgroundColor: theme.palette.primary.main,
+            display: 'flex',
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Typography variant="h5" sx={{ fontWeight: 'bold', color: 'white' }}>
+            {t('editClass')}
+          </Typography>
+
+          <Button
+            onClick={() => {
+              navigate(AppRoutes.class(classId!));
+            }}
+            sx={{ color: 'white' }}
+          >
+            {t('cancel')}
+          </Button>
+        </AppBar>
+      </Paper>
+
+      <Box
+        sx={{
+          width: '100%',
+          display: 'flex',
+          flexDirection: { xs: 'column', md: 'row' },
+          justifyContent: { xs: 'center', md: 'space-evenly' },
+          alignItems: 'center',
+        }}
+      >
+        {!isClassDataLoading ? (
+          <>
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 3,
+                m: MarginSize.small,
+                width: { xs: '80%', md: 'calc(100% / 3)' },
+                height: '100%',
+              }}
+            >
+              <TextField
+                label={t('className')}
+                sx={{ width: '100%' }}
+                value={className}
+                onChange={(e: any) => setClassName(e.target.value)}
+              />
+              <TextField
+                multiline={true}
+                rows={6}
+                sx={{ width: '100%' }}
+                label={t('classDescription')}
+                value={classDescription}
+                onChange={(e: any) => setClassDescription(e.target.value)}
+              />
+              <Box sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}>
+                <Button
+                  onClick={async () => {
+                    await updateClass(classId!, {
+                      name: className,
+                      description: classDescription,
+                    });
+                    navigate(AppRoutes.class(classId!));
+                  }}
+                  sx={{
+                    color: theme.palette.primary.main,
+                    border: `1px solid ${theme.palette.primary.main}`,
+                  }}
+                >
+                  {t('save')}
+                </Button>
+              </Box>
+            </Box>
+
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                gap: 5,
+                width: '100%',
+                p: 3,
+              }}
+            >
+              <UserDataTableComponent
+                data={classData!!.students.map((student) => {
+                  return {
+                    id: student.id,
+                    name: student.user.name,
+                    surname: student.user.surname,
+                  };
+                })}
+                title={t('students')}
+                onActionPressed={async (id: string) => await deleteStudentFromClass(classId!!, id)}
path: client/src/views/ClassGroupEditPage.tsx
user: login: martijnheeren
body: Misschien een domme vraag, maar moet je hier geen Hook gebruiken ipv direct de methode? 
Ik zag het in andere pull requests altijd via de hook
created_at: 2025-05-10T17:17:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { AppBar, Box, Button, Paper, TextField, Typography } from '@mui/material';
+import { useClassById } from '../hooks/useClass.ts';
+import theme from '../util/theme.ts';
+import { MarginSize } from '../util/size.ts';
+import { AppRoutes } from '../util/app.routes.ts';
+import { UserDataTableComponent } from '../components/UserDataTableComponent.tsx';
+import { deleteStudentFromClass, deleteTeacherFromClass, updateClass } from '../api/class.ts';
+import { useAuth } from '../hooks/useAuth.ts';
+import { useError } from '../hooks/useError.ts';
+import { LoadingButton } from '@mui/lab';
+
+export function ClassGroupEditPage() {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const { user } = useAuth();
+
+  const { classId } = useParams<{ classId: string }>();
+  const {
+    data: classData,
+    isLoading: isClassDataLoading,
+    refetch: refetchClassData,
+  } = useClassById(classId!);
+  const error = useError();
+  const [className, setClassName] = useState<string>('');
+  const [classDescription, setClassDescription] = useState<string>('');
+
+  useEffect(() => {
+    if (classData) {
+      setClassName(classData.name);
+      setClassDescription(classData.description);
+    }
+  }, [classData]);
+
+  return (
+    <Box sx={{ width: '100vw' }}>
+      <Paper
+        elevation={3}
+        sx={{
+          m: '2% auto',
+          borderRadius: 2,
+          overflow: 'hidden',
+          width: '90%',
+        }}
+      >
+        <AppBar
+          position="static"
+          sx={{
+            p: MarginSize.small,
+            backgroundColor: theme.palette.primary.main,
+            display: 'flex',
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Typography variant="h5" sx={{ fontWeight: 'bold', color: 'white' }}>
+            {t('editClass')}
+          </Typography>
+
+          <Button
+            onClick={() => {
+              navigate(AppRoutes.class(classId!));
+            }}
+            sx={{ color: 'white' }}
+          >
+            {t('cancel')}
+          </Button>
+        </AppBar>
+      </Paper>
+
+      <Box
+        sx={{
+          width: '100%',
+          display: 'flex',
+          flexDirection: { xs: 'column', md: 'row' },
+          justifyContent: { xs: 'center', md: 'space-evenly' },
+          alignItems: 'center',
+        }}
+      >
+        {!isClassDataLoading ? (
+          <>
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 3,
+                m: MarginSize.small,
+                width: { xs: '80%', md: 'calc(100% / 3)' },
+                height: '100%',
+              }}
+            >
+              <TextField
+                label={t('className')}
+                sx={{ width: '100%' }}
+                value={className}
+                onChange={(e: any) => setClassName(e.target.value)}
+              />
+              <TextField
+                multiline={true}
+                rows={6}
+                sx={{ width: '100%' }}
+                label={t('classDescription')}
+                value={classDescription}
+                onChange={(e: any) => setClassDescription(e.target.value)}
+              />
+              <Box sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}>
+                <Button
+                  onClick={async () => {
+                    await updateClass(classId!, {
+                      name: className,
+                      description: classDescription,
+                    });
+                    navigate(AppRoutes.class(classId!));
+                  }}
+                  sx={{
+                    color: theme.palette.primary.main,
+                    border: `1px solid ${theme.palette.primary.main}`,
+                  }}
+                >
+                  {t('save')}
+                </Button>
+              </Box>
+            </Box>
+
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                gap: 5,
+                width: '100%',
+                p: 3,
+              }}
+            >
+              <UserDataTableComponent
+                data={classData!!.students.map((student) => {
+                  return {
+                    id: student.id,
+                    name: student.user.name,
+                    surname: student.user.surname,
+                  };
+                })}
+                title={t('students')}
+                onActionPressed={async (id: string) => await deleteStudentFromClass(classId!!, id)}
+              />
+
+              {/* Teachers */}
+              <UserDataTableComponent
+                data={classData!!.teachers.map((teacher) => {
+                  return {
+                    id: teacher.id,
+                    name: teacher.user.name,
+                    surname: teacher.user.surname,
+                  };
+                })}
+                title={t('teachers')}
+                onActionPressed={async (id: string) => {
+                  if (
+                    user!!.teacher &&
+                    classData!!.teachers.find((teacher) => teacher.id === user!!.teacher!!.id)
+                  ) {
+                    error.setError("You can't remove yourself");
+                    return;
+                  }
+                  await deleteTeacherFromClass(classId!!, id);
path: client/src/views/ClassGroupEditPage.tsx
user: login: martijnheeren
body: same here
created_at: 2025-05-10T17:17:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { AppBar, Box, Button, Paper, TextField, Typography } from '@mui/material';
+import { useClassById } from '../hooks/useClass.ts';
+import theme from '../util/theme.ts';
+import { MarginSize } from '../util/size.ts';
+import { AppRoutes } from '../util/app.routes.ts';
+import { UserDataTableComponent } from '../components/UserDataTableComponent.tsx';
+import { deleteStudentFromClass, deleteTeacherFromClass, updateClass } from '../api/class.ts';
+import { useAuth } from '../hooks/useAuth.ts';
+import { useError } from '../hooks/useError.ts';
+import { LoadingButton } from '@mui/lab';
+
+export function ClassGroupEditPage() {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const { user } = useAuth();
+
+  const { classId } = useParams<{ classId: string }>();
+  const {
+    data: classData,
+    isLoading: isClassDataLoading,
+    refetch: refetchClassData,
+  } = useClassById(classId!);
+  const error = useError();
+  const [className, setClassName] = useState<string>('');
+  const [classDescription, setClassDescription] = useState<string>('');
+
+  useEffect(() => {
+    if (classData) {
+      setClassName(classData.name);
+      setClassDescription(classData.description);
+    }
+  }, [classData]);
+
+  return (
+    <Box sx={{ width: '100vw' }}>
+      <Paper
+        elevation={3}
+        sx={{
+          m: '2% auto',
+          borderRadius: 2,
+          overflow: 'hidden',
+          width: '90%',
+        }}
+      >
+        <AppBar
+          position="static"
+          sx={{
+            p: MarginSize.small,
+            backgroundColor: theme.palette.primary.main,
+            display: 'flex',
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Typography variant="h5" sx={{ fontWeight: 'bold', color: 'white' }}>
+            {t('editClass')}
+          </Typography>
+
+          <Button
+            onClick={() => {
+              navigate(AppRoutes.class(classId!));
+            }}
+            sx={{ color: 'white' }}
+          >
+            {t('cancel')}
+          </Button>
+        </AppBar>
+      </Paper>
+
+      <Box
+        sx={{
+          width: '100%',
+          display: 'flex',
+          flexDirection: { xs: 'column', md: 'row' },
+          justifyContent: { xs: 'center', md: 'space-evenly' },
+          alignItems: 'center',
+        }}
+      >
+        {!isClassDataLoading ? (
+          <>
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 3,
+                m: MarginSize.small,
+                width: { xs: '80%', md: 'calc(100% / 3)' },
+                height: '100%',
+              }}
+            >
+              <TextField
+                label={t('className')}
+                sx={{ width: '100%' }}
+                value={className}
+                onChange={(e: any) => setClassName(e.target.value)}
+              />
+              <TextField
+                multiline={true}
+                rows={6}
+                sx={{ width: '100%' }}
+                label={t('classDescription')}
+                value={classDescription}
+                onChange={(e: any) => setClassDescription(e.target.value)}
+              />
+              <Box sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}>
+                <Button
+                  onClick={async () => {
+                    await updateClass(classId!, {
+                      name: className,
+                      description: classDescription,
+                    });
+                    navigate(AppRoutes.class(classId!));
+                  }}
+                  sx={{
+                    color: theme.palette.primary.main,
+                    border: `1px solid ${theme.palette.primary.main}`,
+                  }}
+                >
+                  {t('save')}
+                </Button>
+              </Box>
+            </Box>
+
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                gap: 5,
+                width: '100%',
+                p: 3,
+              }}
+            >
+              <UserDataTableComponent
+                data={classData!!.students.map((student) => {
+                  return {
+                    id: student.id,
+                    name: student.user.name,
+                    surname: student.user.surname,
+                  };
+                })}
+                title={t('students')}
+                onActionPressed={async (id: string) => await deleteStudentFromClass(classId!!, id)}
+              />
+
+              {/* Teachers */}
+              <UserDataTableComponent
+                data={classData!!.teachers.map((teacher) => {
+                  return {
+                    id: teacher.id,
+                    name: teacher.user.name,
+                    surname: teacher.user.surname,
+                  };
+                })}
+                title={t('teachers')}
+                onActionPressed={async (id: string) => {
+                  if (
+                    user!!.teacher &&
+                    classData!!.teachers.find((teacher) => teacher.id === user!!.teacher!!.id)
+                  ) {
+                    error.setError("You can't remove yourself");
path: client/src/views/ClassGroupEditPage.tsx
user: login: martijnheeren
body: Moet deze error niet van de backend komen?
created_at: 2025-05-10T17:19:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { AppBar, Box, Button, Paper, TextField, Typography } from '@mui/material';
+import { useClassById } from '../hooks/useClass.ts';
+import theme from '../util/theme.ts';
+import { MarginSize } from '../util/size.ts';
+import { AppRoutes } from '../util/app.routes.ts';
+import { UserDataTableComponent } from '../components/UserDataTableComponent.tsx';
+import { deleteStudentFromClass, deleteTeacherFromClass, updateClass } from '../api/class.ts';
+import { useAuth } from '../hooks/useAuth.ts';
+import { useError } from '../hooks/useError.ts';
+import { LoadingButton } from '@mui/lab';
+
+export function ClassGroupEditPage() {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const { user } = useAuth();
+
+  const { classId } = useParams<{ classId: string }>();
+  const {
+    data: classData,
+    isLoading: isClassDataLoading,
+    refetch: refetchClassData,
+  } = useClassById(classId!);
+  const error = useError();
+  const [className, setClassName] = useState<string>('');
+  const [classDescription, setClassDescription] = useState<string>('');
+
+  useEffect(() => {
+    if (classData) {
+      setClassName(classData.name);
+      setClassDescription(classData.description);
+    }
+  }, [classData]);
+
+  return (
+    <Box sx={{ width: '100vw' }}>
+      <Paper
+        elevation={3}
+        sx={{
+          m: '2% auto',
+          borderRadius: 2,
+          overflow: 'hidden',
+          width: '90%',
+        }}
+      >
+        <AppBar
+          position="static"
+          sx={{
+            p: MarginSize.small,
+            backgroundColor: theme.palette.primary.main,
+            display: 'flex',
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Typography variant="h5" sx={{ fontWeight: 'bold', color: 'white' }}>
+            {t('editClass')}
+          </Typography>
+
+          <Button
+            onClick={() => {
+              navigate(AppRoutes.class(classId!));
+            }}
+            sx={{ color: 'white' }}
+          >
+            {t('cancel')}
+          </Button>
+        </AppBar>
+      </Paper>
+
+      <Box
+        sx={{
+          width: '100%',
+          display: 'flex',
+          flexDirection: { xs: 'column', md: 'row' },
+          justifyContent: { xs: 'center', md: 'space-evenly' },
+          alignItems: 'center',
+        }}
+      >
+        {!isClassDataLoading ? (
+          <>
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 3,
+                m: MarginSize.small,
+                width: { xs: '80%', md: 'calc(100% / 3)' },
+                height: '100%',
+              }}
+            >
+              <TextField
+                label={t('className')}
+                sx={{ width: '100%' }}
+                value={className}
+                onChange={(e: any) => setClassName(e.target.value)}
+              />
+              <TextField
+                multiline={true}
+                rows={6}
+                sx={{ width: '100%' }}
+                label={t('classDescription')}
+                value={classDescription}
+                onChange={(e: any) => setClassDescription(e.target.value)}
+              />
+              <Box sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}>
+                <Button
+                  onClick={async () => {
+                    await updateClass(classId!, {
+                      name: className,
+                      description: classDescription,
+                    });
+                    navigate(AppRoutes.class(classId!));
+                  }}
+                  sx={{
+                    color: theme.palette.primary.main,
+                    border: `1px solid ${theme.palette.primary.main}`,
+                  }}
+                >
+                  {t('save')}
+                </Button>
+              </Box>
+            </Box>
+
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                gap: 5,
+                width: '100%',
+                p: 3,
+              }}
+            >
+              <UserDataTableComponent
+                data={classData!!.students.map((student) => {
+                  return {
+                    id: student.id,
+                    name: student.user.name,
+                    surname: student.user.surname,
+                  };
+                })}
+                title={t('students')}
+                onActionPressed={async (id: string) => await deleteStudentFromClass(classId!!, id)}
+              />
+
+              {/* Teachers */}
+              <UserDataTableComponent
+                data={classData!!.teachers.map((teacher) => {
+                  return {
+                    id: teacher.id,
+                    name: teacher.user.name,
+                    surname: teacher.user.surname,
+                  };
+                })}
+                title={t('teachers')}
+                onActionPressed={async (id: string) => {
+                  if (
+                    user!!.teacher &&
+                    classData!!.teachers.find((teacher) => teacher.id === user!!.teacher!!.id)
+                  ) {
+                    error.setError("You can't remove yourself");
+                    return;
+                  }
+                  await deleteTeacherFromClass(classId!!, id);
path: client/src/views/ClassGroupEditPage.tsx
user: login: RobinMeersman
body: op zich hoeft da ni, ik heb geen idee waarom het anders wel zo is, maar zal het aanpassen zodat het uniform is
created_at: 2025-05-15T16:36:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -162,20 +162,18 @@ function ClassDashboardPage() {
             </List>
 
             <Typography variant="h6" sx={{ mt: 3, fontWeight: 'bold' }}>
-              {t('notes')}
+              {t('classDescription')}
             </Typography>
             <Typography variant="body2" sx={{ color: 'gray' }}>
-              {classData!.notes || // TODO add notes to class
-                'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'}
+              {classData!.description}
path: client/src/views/ClassDashboardPage.tsx
user: login: Copilot
body: [nitpick] Consider providing a fallback message in case the class description is empty to improve user experience.
```suggestion
              {classData!.description || t('noDescriptionAvailable')}
```
created_at: 2025-05-16T13:24:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -88,6 +89,8 @@ export const en = {
     submission: 'Submission',
     back: 'Back',
     score: 'Score',
+    editClass: 'Editing class',
path: client/src/util/locale/en.ts
user: login: Copilot
body: [nitpick] Consider aligning the 'editClass' translation with other locales (e.g., 'Edit Class') for consistency.
```suggestion
    editClass: 'Edit Class',
```
created_at: 2025-05-16T13:24:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,199 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { AppBar, Box, Button, Paper, TextField, Typography } from '@mui/material';
+import {
+  useClassById,
+  useDeleteStudentFromClass,
+  useDeleteTeacherFromClass,
+} from '../hooks/useClass.ts';
+import theme from '../util/theme.ts';
+import { MarginSize } from '../util/size.ts';
+import { AppRoutes } from '../util/app.routes.ts';
+import { UserDataTableComponent } from '../components/UserDataTableComponent.tsx';
+import { updateClass } from '../api/class.ts';
+import { useAuth } from '../hooks/useAuth.ts';
+import { useError } from '../hooks/useError.ts';
+
+export function ClassGroupEditPage() {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const { user } = useAuth();
+
+  const { classId } = useParams<{ classId: string }>();
+  const {
+    data: classData,
+    isLoading: isClassDataLoading,
+    refetch: refetchClassData,
+  } = useClassById(classId!);
+  const error = useError();
+  const [className, setClassName] = useState<string>('');
+  const [classDescription, setClassDescription] = useState<string>('');
+  const deleteStudent = useDeleteStudentFromClass();
+  const deleteTeacher = useDeleteTeacherFromClass();
+
+  useEffect(() => {
+    if (classData) {
+      setClassName(classData.name);
+      setClassDescription(classData.description);
+    }
+  }, [classData]);
+
+  return (
+    <Box sx={{ width: '100vw' }}>
+      <Paper
+        elevation={3}
+        sx={{
+          m: '2% auto',
+          borderRadius: 2,
+          overflow: 'hidden',
+          width: '90%',
+        }}
+      >
+        <AppBar
+          position="static"
+          sx={{
+            p: MarginSize.small,
+            backgroundColor: theme.palette.primary.main,
+            display: 'flex',
+            flexDirection: 'row',
+            justifyContent: 'space-between',
+          }}
+        >
+          <Typography variant="h5" sx={{ fontWeight: 'bold', color: 'white' }}>
+            {t('editClass')}
+          </Typography>
+
+          <Button
+            onClick={() => {
+              navigate(AppRoutes.class(classId!));
+            }}
+            sx={{ color: 'white' }}
+          >
+            {t('cancel')}
+          </Button>
+        </AppBar>
+      </Paper>
+
+      <Box
+        sx={{
+          width: '100%',
+          display: 'flex',
+          flexDirection: { xs: 'column', md: 'row' },
+          justifyContent: { xs: 'center', md: 'space-evenly' },
+          alignItems: 'center',
+        }}
+      >
+        {!isClassDataLoading ? (
+          <>
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                alignItems: 'center',
+                justifyContent: 'center',
+                gap: 3,
+                m: MarginSize.small,
+                width: { xs: '80%', md: 'calc(100% / 3)' },
+                height: '100%',
+              }}
+            >
+              <TextField
+                label={t('className')}
+                sx={{ width: '100%' }}
+                value={className}
+                onChange={(e: any) => setClassName(e.target.value)}
+              />
+              <TextField
+                multiline={true}
+                rows={6}
+                sx={{ width: '100%' }}
+                label={t('classDescription')}
+                value={classDescription}
+                onChange={(e: any) => setClassDescription(e.target.value)}
+              />
+              <Box sx={{ width: '100%', display: 'flex', justifyContent: 'end' }}>
+                <Button
+                  onClick={async () => {
+                    await updateClass(classId!, {
+                      name: className,
+                      description: classDescription,
+                    });
+                    navigate(AppRoutes.class(classId!));
+                  }}
+                  sx={{
+                    color: theme.palette.primary.main,
+                    border: `1px solid ${theme.palette.primary.main}`,
+                  }}
+                >
+                  {t('save')}
+                </Button>
+              </Box>
+            </Box>
+
+            <Box
+              sx={{
+                display: 'flex',
+                flexDirection: 'column',
+                gap: 5,
+                width: '100%',
+                p: 3,
+              }}
+            >
+              <UserDataTableComponent
+                data={classData!!.students.map((student) => {
path: client/src/views/ClassGroupEditPage.tsx
user: login: Copilot
body: [nitpick] Consider using optional chaining (classData?.students) instead of non-null assertions for improved safety, even though the branch is already guarded.
```suggestion
                data={classData?.students?.map((student) => {
```
created_at: 2025-05-16T13:24:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: martijnheeren
created_at: 2025-05-09T09:54:52Z
body: Kan je ook enkele screenshots van je componenten zetten in je PR? Vb van UserDataTableComponent, dat maakt het reviewen iets eenvoudiger
author_association: CONTRIBUTOR
user: login: RobinMeersman
created_at: 2025-05-10T10:08:48Z
body: > Kan je ook enkele screenshots van je componenten zetten in je PR? Vb van UserDataTableComponent, dat maakt het reviewen iets eenvoudiger

Yes, komt in orde. Was ik vergeten toevoegen
author_association: CONTRIBUTOR
user: login: martijnheeren
created_at: 2025-05-16T09:11:39Z
body: Kan je oplossen dat de integration tests niet slagen?
author_association: CONTRIBUTOR
user: login: RobinMeersman
created_at: 2025-05-16T21:54:32Z
body: > Kan je oplossen dat de integration tests niet slagen?

Dat is de databank. Ik weet niet hoe ik dit moet oplossen 🙈
author_association: CONTRIBUTOR
user: login: martijnheeren
created_at: 2025-05-17T06:40:35Z
body: Misschien kan @Geras1mleo of @Jasmaert  eens kijken hoe dat moet worden opgelost
author_association: CONTRIBUTOR
user: login: RobinMeersman
created_at: 2025-05-17T09:08:44Z
body: > De student moet de button om de klas te bewerken niet kunnen zien. For some reason krijg ik als student geen assignments te zien. Kan jij eens testen wat er gebeurt als je een student verwijdert terwijl die een assignment heeft? Als je jezelf probeert te verwijderen krijg je ook gewoon een errorcode en geen message die zegt "je kan jezelf niet verwijderen" ofso

Vreemd, ik geef normaal error.message aan de setError hook.
author_association: CONTRIBUTOR
user: login: martijnheeren
created_at: 2025-05-18T07:20:42Z
body: > Vreemd, ik geef normaal error.message aan de setError hook.

Ja ik denk dat het een flaw is van onze backend of snackbar ofso.
author_association: CONTRIBUTOR