Merge dev into fix/progress-bar

This commit is contained in:
Joyelle Ndagijimana 2025-05-13 09:21:34 +02:00
commit fe9ff5ec16
46 changed files with 4156 additions and 1232 deletions

View file

@ -37,6 +37,7 @@
"jwks-rsa": "^3.1.0", "jwks-rsa": "^3.1.0",
"loki-logger-ts": "^1.0.2", "loki-logger-ts": "^1.0.2",
"marked": "^15.0.7", "marked": "^15.0.7",
"nanoid": "^5.1.5",
"response-time": "^2.3.3", "response-time": "^2.3.3",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
"uuid": "^11.1.0", "uuid": "^11.1.0",

View file

@ -7,7 +7,6 @@ import {
getJoinRequestsByClass, getJoinRequestsByClass,
getStudentsByTeacher, getStudentsByTeacher,
getTeacher, getTeacher,
getTeacherQuestions,
updateClassJoinRequestStatus, updateClassJoinRequestStatus,
} from '../services/teachers.js'; } from '../services/teachers.js';
import { requireFields } from './error-helper.js'; import { requireFields } from './error-helper.js';
@ -70,16 +69,6 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
res.json({ students }); res.json({ students });
} }
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
const username = req.params.username;
const full = req.query.full === 'true';
requireFields({ username });
const questions = await getTeacherQuestions(username, full);
res.json({ questions });
}
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> { export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
const classId = req.params.classId; const classId = req.params.classId;
requireFields({ classId }); requireFields({ classId });

View file

@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
import { Teacher } from '../../entities/users/teacher.entity.js';
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> { export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> { public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
@ -32,11 +31,4 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
} }
); );
} }
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
return this.find(
{ admins: teacher },
{ populate: ['admins'] } // Make sure to load admin relations
);
}
} }

View file

@ -26,6 +26,9 @@ export class Assignment {
@Property({ type: 'string' }) @Property({ type: 'string' })
learningPathHruid!: string; learningPathHruid!: string;
@Property({ type: 'datetime', nullable: true })
deadline?: Date;
@Enum({ @Enum({
items: () => Language, items: () => Language,
}) })

View file

@ -1,15 +1,17 @@
import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { v4 } from 'uuid';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { Student } from '../users/student.entity.js'; import { Student } from '../users/student.entity.js';
import { ClassRepository } from '../../data/classes/class-repository.js'; import { ClassRepository } from '../../data/classes/class-repository.js';
import { customAlphabet } from 'nanoid';
const generateClassId = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 6);
@Entity({ @Entity({
repository: () => ClassRepository, repository: () => ClassRepository,
}) })
export class Class { export class Class {
@PrimaryKey() @PrimaryKey()
classId? = v4(); classId? = generateClassId();
@Property({ type: 'string' }) @Property({ type: 'string' })
displayName!: string; displayName!: string;

View file

@ -20,6 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
description: assignment.description, description: assignment.description,
learningPath: assignment.learningPathHruid, learningPath: assignment.learningPathHruid,
language: assignment.learningPathLanguage, language: assignment.learningPathLanguage,
deadline: assignment.deadline ?? new Date(),
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)), groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
}; };
} }
@ -31,6 +32,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
description: assignmentData.description, description: assignmentData.description,
learningPathHruid: assignmentData.learningPath, learningPathHruid: assignmentData.learningPath,
learningPathLanguage: languageMap[assignmentData.language], learningPathLanguage: languageMap[assignmentData.language],
deadline: assignmentData.deadline,
groups: [], groups: [],
}); });
} }

View file

@ -6,7 +6,6 @@ import {
getStudentJoinRequestHandler, getStudentJoinRequestHandler,
getTeacherClassHandler, getTeacherClassHandler,
getTeacherHandler, getTeacherHandler,
getTeacherQuestionHandler,
getTeacherStudentHandler, getTeacherStudentHandler,
updateStudentJoinRequestHandler, updateStudentJoinRequestHandler,
} from '../controllers/teachers.js'; } from '../controllers/teachers.js';
@ -27,8 +26,6 @@ router.get('/:username/classes', getTeacherClassHandler);
router.get('/:username/students', getTeacherStudentHandler); router.get('/:username/students', getTeacherStudentHandler);
router.get('/:username/questions', getTeacherQuestionHandler);
router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler);
router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler);

View file

@ -1,12 +1,5 @@
import { import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
getClassJoinRequestRepository,
getClassRepository,
getLearningObjectRepository,
getQuestionRepository,
getTeacherRepository,
} from '../data/repositories.js';
import { mapToClassDTO } from '../interfaces/class.js'; import { mapToClassDTO } from '../interfaces/class.js';
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js'; import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
import { Teacher } from '../entities/users/teacher.entity.js'; import { Teacher } from '../entities/users/teacher.entity.js';
import { fetchStudent } from './students.js'; import { fetchStudent } from './students.js';
@ -15,10 +8,6 @@ import { mapToStudentRequestDTO } from '../interfaces/student-request.js';
import { TeacherRepository } from '../data/users/teacher-repository.js'; import { TeacherRepository } from '../data/users/teacher-repository.js';
import { ClassRepository } from '../data/classes/class-repository.js'; import { ClassRepository } from '../data/classes/class-repository.js';
import { Class } from '../entities/classes/class.entity.js'; import { Class } from '../entities/classes/class.entity.js';
import { LearningObjectRepository } from '../data/content/learning-object-repository.js';
import { LearningObject } from '../entities/content/learning-object.entity.js';
import { QuestionRepository } from '../data/questions/question-repository.js';
import { Question } from '../entities/questions/question.entity.js';
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js'; import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
import { Student } from '../entities/users/student.entity.js'; import { Student } from '../entities/users/student.entity.js';
import { NotFoundException } from '../exceptions/not-found-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js';
@ -26,7 +15,6 @@ import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { ClassDTO } from '@dwengo-1/common/interfaces/class'; import { ClassDTO } from '@dwengo-1/common/interfaces/class';
import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
import { ConflictException } from '../exceptions/conflict-exception.js'; import { ConflictException } from '../exceptions/conflict-exception.js';
@ -119,28 +107,6 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
return students.map((student) => student.username); return students.map((student) => student.username);
} }
export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
const teacher: Teacher = await fetchTeacher(username);
// Find all learning objects that this teacher manages
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
if (!learningObjects || learningObjects.length === 0) {
return [];
}
// Fetch all questions related to these learning objects
const questionRepository: QuestionRepository = getQuestionRepository();
const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects);
if (full) {
return questions.map(mapToQuestionDTO);
}
return questions.map(mapToQuestionDTOId);
}
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> { export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
const classRepository: ClassRepository = getClassRepository(); const classRepository: ClassRepository = getClassRepository();
const cls: Class | null = await classRepository.findById(classId); const cls: Class | null = await classRepository.findById(classId);

View file

@ -0,0 +1,47 @@
import { setupTestApp } from '../setup-tests.js';
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
import { Request, Response } from 'express';
import { createClassHandler, deleteClassHandler } from '../../src/controllers/classes';
describe('Class controllers', () => {
let req: Partial<Request>;
let res: Partial<Response>;
let jsonMock: Mock;
let statusMock: Mock;
beforeAll(async () => {
await setupTestApp();
});
beforeEach(async () => {
jsonMock = vi.fn();
statusMock = vi.fn().mockReturnThis();
res = {
json: jsonMock,
status: statusMock,
};
});
it('create and delete class', async () => {
req = {
body: { displayName: 'coole_nieuwe_klas' },
};
await createClassHandler(req as Request, res as Response);
const result = jsonMock.mock.lastCall?.[0];
// Console.log('class', result.class);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
req = {
params: { id: result.class.id },
};
await deleteClassHandler(req as Request, res as Response);
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
});
});

View file

@ -21,6 +21,7 @@ import { BadRequestException } from '../../src/exceptions/bad-request-exception.
import { ConflictException } from '../../src/exceptions/conflict-exception.js'; import { ConflictException } from '../../src/exceptions/conflict-exception.js';
import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js';
import { StudentDTO } from '@dwengo-1/common/interfaces/student'; import { StudentDTO } from '@dwengo-1/common/interfaces/student';
import { getClass02 } from '../test_assets/classes/classes.testdata';
describe('Student controllers', () => { describe('Student controllers', () => {
let req: Partial<Request>; let req: Partial<Request>;
@ -186,7 +187,7 @@ describe('Student controllers', () => {
it('Get join request by student and class', async () => { it('Get join request by student and class', async () => {
req = { req = {
params: { username: 'PinkFloyd', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, params: { username: 'PinkFloyd', classId: getClass02().classId },
}; };
await getStudentRequestHandler(req as Request, res as Response); await getStudentRequestHandler(req as Request, res as Response);
@ -201,7 +202,7 @@ describe('Student controllers', () => {
it('Create and delete join request', async () => { it('Create and delete join request', async () => {
req = { req = {
params: { username: 'TheDoors' }, params: { username: 'TheDoors' },
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, body: { classId: getClass02().classId },
}; };
await createStudentRequestHandler(req as Request, res as Response); await createStudentRequestHandler(req as Request, res as Response);
@ -209,7 +210,7 @@ describe('Student controllers', () => {
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() })); expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ request: expect.anything() }));
req = { req = {
params: { username: 'TheDoors', classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, params: { username: 'TheDoors', classId: getClass02().classId },
}; };
await deleteClassJoinRequestHandler(req as Request, res as Response); await deleteClassJoinRequestHandler(req as Request, res as Response);
@ -222,7 +223,7 @@ describe('Student controllers', () => {
it('Create join request student already in class error', async () => { it('Create join request student already in class error', async () => {
req = { req = {
params: { username: 'Noordkaap' }, params: { username: 'Noordkaap' },
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, body: { classId: getClass02().classId },
}; };
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException); await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);
@ -231,7 +232,7 @@ describe('Student controllers', () => {
it('Create join request duplicate', async () => { it('Create join request duplicate', async () => {
req = { req = {
params: { username: 'Tool' }, params: { username: 'Tool' },
body: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, body: { classId: getClass02().classId },
}; };
await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException); await expect(async () => createStudentRequestHandler(req as Request, res as Response)).rejects.toThrow(ConflictException);

View file

@ -12,6 +12,7 @@ import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invit
import { getClassHandler } from '../../src/controllers/classes'; import { getClassHandler } from '../../src/controllers/classes';
import { BadRequestException } from '../../src/exceptions/bad-request-exception'; import { BadRequestException } from '../../src/exceptions/bad-request-exception';
import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
import { getClass02 } from '../test_assets/classes/classes.testdata';
describe('Teacher controllers', () => { describe('Teacher controllers', () => {
let req: Partial<Request>; let req: Partial<Request>;
@ -57,7 +58,7 @@ describe('Teacher controllers', () => {
const body = { const body = {
sender: 'LimpBizkit', sender: 'LimpBizkit',
receiver: 'testleerkracht1', receiver: 'testleerkracht1',
class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', class: getClass02().classId,
} as TeacherInvitationData; } as TeacherInvitationData;
req = { body }; req = { body };
@ -67,7 +68,7 @@ describe('Teacher controllers', () => {
params: { params: {
sender: 'LimpBizkit', sender: 'LimpBizkit',
receiver: 'testleerkracht1', receiver: 'testleerkracht1',
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', classId: getClass02().classId,
}, },
body: { accepted: 'false' }, body: { accepted: 'false' },
}; };
@ -80,7 +81,7 @@ describe('Teacher controllers', () => {
params: { params: {
sender: 'LimpBizkit', sender: 'LimpBizkit',
receiver: 'FooFighters', receiver: 'FooFighters',
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', classId: getClass02().classId,
}, },
}; };
await getInvitationHandler(req as Request, res as Response); await getInvitationHandler(req as Request, res as Response);
@ -100,7 +101,7 @@ describe('Teacher controllers', () => {
const body = { const body = {
sender: 'LimpBizkit', sender: 'LimpBizkit',
receiver: 'FooFighters', receiver: 'FooFighters',
class: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', class: getClass02().classId,
} as TeacherInvitationData; } as TeacherInvitationData;
req = { body }; req = { body };
@ -111,7 +112,7 @@ describe('Teacher controllers', () => {
req = { req = {
params: { params: {
id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', id: getClass02().classId,
}, },
}; };

View file

@ -17,6 +17,7 @@ import { EntityAlreadyExistsException } from '../../src/exceptions/entity-alread
import { getStudentRequestsHandler } from '../../src/controllers/students.js'; import { getStudentRequestsHandler } from '../../src/controllers/students.js';
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
import { getClassHandler } from '../../src/controllers/classes'; import { getClassHandler } from '../../src/controllers/classes';
import { getClass02 } from '../test_assets/classes/classes.testdata';
describe('Teacher controllers', () => { describe('Teacher controllers', () => {
let req: Partial<Request>; let req: Partial<Request>;
@ -169,7 +170,7 @@ describe('Teacher controllers', () => {
it('Get join requests by class', async () => { it('Get join requests by class', async () => {
req = { req = {
params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, params: { classId: getClass02().classId },
}; };
await getStudentJoinRequestHandler(req as Request, res as Response); await getStudentJoinRequestHandler(req as Request, res as Response);
@ -183,7 +184,7 @@ describe('Teacher controllers', () => {
it('Update join request status', async () => { it('Update join request status', async () => {
req = { req = {
params: { classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', studentUsername: 'PinkFloyd' }, params: { classId: getClass02().classId, studentUsername: 'PinkFloyd' },
body: { accepted: 'true' }, body: { accepted: 'true' },
}; };
@ -201,7 +202,7 @@ describe('Teacher controllers', () => {
expect(status).toBeTruthy(); expect(status).toBeTruthy();
req = { req = {
params: { id: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89' }, params: { id: getClass02().classId },
}; };
await getClassHandler(req as Request, res as Response); await getClassHandler(req as Request, res as Response);

View file

@ -3,6 +3,7 @@ import { setupTestApp } from '../../setup-tests';
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
import { getAssignmentRepository, getClassRepository } from '../../../src/data/repositories'; import { getAssignmentRepository, getClassRepository } from '../../../src/data/repositories';
import { ClassRepository } from '../../../src/data/classes/class-repository'; import { ClassRepository } from '../../../src/data/classes/class-repository';
import { getClass02 } from '../../test_assets/classes/classes.testdata';
describe('AssignmentRepository', () => { describe('AssignmentRepository', () => {
let assignmentRepository: AssignmentRepository; let assignmentRepository: AssignmentRepository;
@ -15,7 +16,7 @@ describe('AssignmentRepository', () => {
}); });
it('should return the requested assignment', async () => { it('should return the requested assignment', async () => {
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89'); const class_ = await classRepository.findById(getClass02().classId);
const assignment = await assignmentRepository.findByClassAndId(class_!, 21001); const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
expect(assignment).toBeTruthy(); expect(assignment).toBeTruthy();
@ -23,7 +24,7 @@ describe('AssignmentRepository', () => {
}); });
it('should return all assignments for a class', async () => { it('should return all assignments for a class', async () => {
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89'); const class_ = await classRepository.findById(getClass02().classId);
const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!); const assignments = await assignmentRepository.findAllAssignmentsInClass(class_!);
expect(assignments).toBeTruthy(); expect(assignments).toBeTruthy();

View file

@ -4,6 +4,7 @@ import { GroupRepository } from '../../../src/data/assignments/group-repository'
import { getAssignmentRepository, getClassRepository, getGroupRepository } from '../../../src/data/repositories'; import { getAssignmentRepository, getClassRepository, getGroupRepository } from '../../../src/data/repositories';
import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository'; import { AssignmentRepository } from '../../../src/data/assignments/assignment-repository';
import { ClassRepository } from '../../../src/data/classes/class-repository'; import { ClassRepository } from '../../../src/data/classes/class-repository';
import { getClass01, getClass02 } from '../../test_assets/classes/classes.testdata';
describe('GroupRepository', () => { describe('GroupRepository', () => {
let groupRepository: GroupRepository; let groupRepository: GroupRepository;
@ -18,7 +19,8 @@ describe('GroupRepository', () => {
}); });
it('should return the requested group', async () => { it('should return the requested group', async () => {
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const id = getClass01().classId;
const class_ = await classRepository.findById(id);
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000); const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
@ -27,7 +29,7 @@ describe('GroupRepository', () => {
}); });
it('should return all groups for assignment', async () => { it('should return all groups for assignment', async () => {
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const class_ = await classRepository.findById(getClass01().classId);
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000); const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
const groups = await groupRepository.findAllGroupsForAssignment(assignment!); const groups = await groupRepository.findAllGroupsForAssignment(assignment!);
@ -37,7 +39,7 @@ describe('GroupRepository', () => {
}); });
it('should not find removed group', async () => { it('should not find removed group', async () => {
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89'); const class_ = await classRepository.findById(getClass02().classId);
const assignment = await assignmentRepository.findByClassAndId(class_!, 21001); const assignment = await assignmentRepository.findByClassAndId(class_!, 21001);
await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 21001); await groupRepository.deleteByAssignmentAndGroupNumber(assignment!, 21001);

View file

@ -18,6 +18,7 @@ import { Submission } from '../../../src/entities/assignments/submission.entity'
import { Class } from '../../../src/entities/classes/class.entity'; import { Class } from '../../../src/entities/classes/class.entity';
import { Assignment } from '../../../src/entities/assignments/assignment.entity'; import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata'; import { testLearningObject01 } from '../../test_assets/content/learning-objects.testdata';
import { getClass01 } from '../../test_assets/classes/classes.testdata';
describe('SubmissionRepository', () => { describe('SubmissionRepository', () => {
let submissionRepository: SubmissionRepository; let submissionRepository: SubmissionRepository;
@ -54,7 +55,7 @@ describe('SubmissionRepository', () => {
it('should find the most recent submission for a group', async () => { it('should find the most recent submission for a group', async () => {
const id = new LearningObjectIdentifier('id03', Language.English, 1); const id = new LearningObjectIdentifier('id03', Language.English, 1);
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const class_ = await classRepository.findById(getClass01().classId);
const assignment = await assignmentRepository.findByClassAndId(class_!, 21000); const assignment = await assignmentRepository.findByClassAndId(class_!, 21000);
const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001); const group = await groupRepository.findByAssignmentAndGroupNumber(assignment!, 21001);
const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!); const submission = await submissionRepository.findMostRecentSubmissionForGroup(id, group!);
@ -67,7 +68,7 @@ describe('SubmissionRepository', () => {
let assignment: Assignment | null; let assignment: Assignment | null;
let loId: LearningObjectIdentifier; let loId: LearningObjectIdentifier;
it('should find all submissions for a certain learning object and assignment', async () => { it('should find all submissions for a certain learning object and assignment', async () => {
clazz = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); clazz = await classRepository.findById(getClass01().classId);
assignment = await assignmentRepository.findByClassAndId(clazz!, 21000); assignment = await assignmentRepository.findByClassAndId(clazz!, 21000);
loId = { loId = {
hruid: 'id02', hruid: 'id02',

View file

@ -4,6 +4,7 @@ import { ClassJoinRequestRepository } from '../../../src/data/classes/class-join
import { getClassJoinRequestRepository, getClassRepository, getStudentRepository } from '../../../src/data/repositories'; import { getClassJoinRequestRepository, getClassRepository, getStudentRepository } from '../../../src/data/repositories';
import { StudentRepository } from '../../../src/data/users/student-repository'; import { StudentRepository } from '../../../src/data/users/student-repository';
import { ClassRepository } from '../../../src/data/classes/class-repository'; import { ClassRepository } from '../../../src/data/classes/class-repository';
import { getClass02, getClass03 } from '../../test_assets/classes/classes.testdata';
describe('ClassJoinRequestRepository', () => { describe('ClassJoinRequestRepository', () => {
let classJoinRequestRepository: ClassJoinRequestRepository; let classJoinRequestRepository: ClassJoinRequestRepository;
@ -26,7 +27,7 @@ describe('ClassJoinRequestRepository', () => {
}); });
it('should list all requests to a single class', async () => { it('should list all requests to a single class', async () => {
const class_ = await cassRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89'); const class_ = await cassRepository.findById(getClass02().classId);
const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!); const requests = await classJoinRequestRepository.findAllOpenRequestsTo(class_!);
expect(requests).toBeTruthy(); expect(requests).toBeTruthy();
@ -35,7 +36,7 @@ describe('ClassJoinRequestRepository', () => {
it('should not find a removed request', async () => { it('should not find a removed request', async () => {
const student = await studentRepository.findByUsername('SmashingPumpkins'); const student = await studentRepository.findByUsername('SmashingPumpkins');
const class_ = await cassRepository.findById('80dcc3e0-1811-4091-9361-42c0eee91cfa'); const class_ = await cassRepository.findById(getClass03().classId);
await classJoinRequestRepository.deleteBy(student!, class_!); await classJoinRequestRepository.deleteBy(student!, class_!);
const request = await classJoinRequestRepository.findAllRequestsBy(student!); const request = await classJoinRequestRepository.findAllRequestsBy(student!);

View file

@ -2,6 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
import { ClassRepository } from '../../../src/data/classes/class-repository'; import { ClassRepository } from '../../../src/data/classes/class-repository';
import { setupTestApp } from '../../setup-tests'; import { setupTestApp } from '../../setup-tests';
import { getClassRepository } from '../../../src/data/repositories'; import { getClassRepository } from '../../../src/data/repositories';
import { getClass01, getClass04 } from '../../test_assets/classes/classes.testdata';
describe('ClassRepository', () => { describe('ClassRepository', () => {
let classRepository: ClassRepository; let classRepository: ClassRepository;
@ -18,16 +19,16 @@ describe('ClassRepository', () => {
}); });
it('should return requested class', async () => { it('should return requested class', async () => {
const classVar = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const classVar = await classRepository.findById(getClass01().classId);
expect(classVar).toBeTruthy(); expect(classVar).toBeTruthy();
expect(classVar?.displayName).toBe('class01'); expect(classVar?.displayName).toBe('class01');
}); });
it('class should be gone after deletion', async () => { it('class should be gone after deletion', async () => {
await classRepository.deleteById('33d03536-83b8-4880-9982-9bbf2f908ddf'); await classRepository.deleteById(getClass04().classId);
const classVar = await classRepository.findById('33d03536-83b8-4880-9982-9bbf2f908ddf'); const classVar = await classRepository.findById(getClass04().classId);
expect(classVar).toBeNull(); expect(classVar).toBeNull();
}); });

View file

@ -4,6 +4,7 @@ import { getClassRepository, getTeacherInvitationRepository, getTeacherRepositor
import { TeacherInvitationRepository } from '../../../src/data/classes/teacher-invitation-repository'; import { TeacherInvitationRepository } from '../../../src/data/classes/teacher-invitation-repository';
import { TeacherRepository } from '../../../src/data/users/teacher-repository'; import { TeacherRepository } from '../../../src/data/users/teacher-repository';
import { ClassRepository } from '../../../src/data/classes/class-repository'; import { ClassRepository } from '../../../src/data/classes/class-repository';
import { getClass01, getClass02 } from '../../test_assets/classes/classes.testdata';
describe('ClassRepository', () => { describe('ClassRepository', () => {
let teacherInvitationRepository: TeacherInvitationRepository; let teacherInvitationRepository: TeacherInvitationRepository;
@ -34,7 +35,7 @@ describe('ClassRepository', () => {
}); });
it('should return all invitations for a class', async () => { it('should return all invitations for a class', async () => {
const class_ = await classRepository.findById('34d484a1-295f-4e9f-bfdc-3e7a23d86a89'); const class_ = await classRepository.findById(getClass02().classId);
const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!); const invitations = await teacherInvitationRepository.findAllInvitationsForClass(class_!);
expect(invitations).toBeTruthy(); expect(invitations).toBeTruthy();
@ -42,7 +43,7 @@ describe('ClassRepository', () => {
}); });
it('should not find a removed invitation', async () => { it('should not find a removed invitation', async () => {
const class_ = await classRepository.findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const class_ = await classRepository.findById(getClass01().classId);
const sender = await teacherRepository.findByUsername('FooFighters'); const sender = await teacherRepository.findByUsername('FooFighters');
const receiver = await teacherRepository.findByUsername('LimpBizkit'); const receiver = await teacherRepository.findByUsername('LimpBizkit');
await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!); await teacherInvitationRepository.deleteBy(class_!, sender!, receiver!);

View file

@ -14,6 +14,7 @@ import { Language } from '@dwengo-1/common/util/language';
import { Question } from '../../../src/entities/questions/question.entity'; import { Question } from '../../../src/entities/questions/question.entity';
import { Class } from '../../../src/entities/classes/class.entity'; import { Class } from '../../../src/entities/classes/class.entity';
import { Assignment } from '../../../src/entities/assignments/assignment.entity'; import { Assignment } from '../../../src/entities/assignments/assignment.entity';
import { getClass01 } from '../../test_assets/classes/classes.testdata';
describe('QuestionRepository', () => { describe('QuestionRepository', () => {
let questionRepository: QuestionRepository; let questionRepository: QuestionRepository;
@ -37,7 +38,7 @@ describe('QuestionRepository', () => {
const id = new LearningObjectIdentifier('id03', Language.English, 1); const id = new LearningObjectIdentifier('id03', Language.English, 1);
const student = await studentRepository.findByUsername('Noordkaap'); const student = await studentRepository.findByUsername('Noordkaap');
const clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); const clazz = await getClassRepository().findById(getClass01().classId);
const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000); const assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 21001); const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment!, 21001);
await questionRepository.createQuestion({ await questionRepository.createQuestion({
@ -56,7 +57,7 @@ describe('QuestionRepository', () => {
let assignment: Assignment | null; let assignment: Assignment | null;
let loId: LearningObjectIdentifier; let loId: LearningObjectIdentifier;
it('should find all questions for a certain learning object and assignment', async () => { it('should find all questions for a certain learning object and assignment', async () => {
clazz = await getClassRepository().findById('8764b861-90a6-42e5-9732-c0d9eb2f55f9'); clazz = await getClassRepository().findById(getClass01().classId);
assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000); assignment = await getAssignmentRepository().findByClassAndId(clazz!, 21000);
loId = { loId = {
hruid: 'id05', hruid: 'id05',

View file

@ -6,13 +6,20 @@ import { testLearningPathWithConditions } from '../content/learning-paths.testda
import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata'; import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] { export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 7);
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 7);
const today = new Date();
today.setHours(23, 59);
assignment01 = em.create(Assignment, { assignment01 = em.create(Assignment, {
id: 21000, id: 21000,
within: classes[0], within: classes[0],
title: 'dire straits', title: 'dire straits',
description: 'reading', description: 'reading',
learningPathHruid: 'id02', learningPathHruid: 'un_ai',
learningPathLanguage: Language.English, learningPathLanguage: Language.English,
deadline: today,
groups: [], groups: [],
}); });
@ -23,6 +30,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'reading', description: 'reading',
learningPathHruid: 'id01', learningPathHruid: 'id01',
learningPathLanguage: Language.English, learningPathLanguage: Language.English,
deadline: futureDate,
groups: [], groups: [],
}); });
@ -33,6 +41,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'will be deleted', description: 'will be deleted',
learningPathHruid: 'id02', learningPathHruid: 'id02',
learningPathLanguage: Language.English, learningPathLanguage: Language.English,
deadline: pastDate,
groups: [], groups: [],
}); });
@ -43,6 +52,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'with a description', description: 'with a description',
learningPathHruid: 'id01', learningPathHruid: 'id01',
learningPathLanguage: Language.English, learningPathLanguage: Language.English,
deadline: pastDate,
groups: [], groups: [],
}); });
@ -53,6 +63,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
description: 'You have to do the testing learning path with a condition.', description: 'You have to do the testing learning path with a condition.',
learningPathHruid: testLearningPathWithConditions.hruid, learningPathHruid: testLearningPathWithConditions.hruid,
learningPathLanguage: testLearningPathWithConditions.language as Language, learningPathLanguage: testLearningPathWithConditions.language as Language,
deadline: futureDate,
groups: [], groups: [],
}); });

View file

@ -10,7 +10,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const teacherClass01: Teacher[] = teachers.slice(4, 5); const teacherClass01: Teacher[] = teachers.slice(4, 5);
class01 = em.create(Class, { class01 = em.create(Class, {
classId: '8764b861-90a6-42e5-9732-c0d9eb2f55f9', classId: 'X2J9QT', // 8764b861-90a6-42e5-9732-c0d9eb2f55f9
displayName: 'class01', displayName: 'class01',
teachers: teacherClass01, teachers: teacherClass01,
students: studentsClass01, students: studentsClass01,
@ -20,7 +20,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const teacherClass02: Teacher[] = teachers.slice(1, 2); const teacherClass02: Teacher[] = teachers.slice(1, 2);
class02 = em.create(Class, { class02 = em.create(Class, {
classId: '34d484a1-295f-4e9f-bfdc-3e7a23d86a89', classId: '7KLPMA', // 34d484a1-295f-4e9f-bfdc-3e7a23d86a89
displayName: 'class02', displayName: 'class02',
teachers: teacherClass02, teachers: teacherClass02,
students: studentsClass02, students: studentsClass02,
@ -30,7 +30,7 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const teacherClass03: Teacher[] = teachers.slice(2, 3); const teacherClass03: Teacher[] = teachers.slice(2, 3);
class03 = em.create(Class, { class03 = em.create(Class, {
classId: '80dcc3e0-1811-4091-9361-42c0eee91cfa', classId: 'R0D3UZ', // 80dcc3e0-1811-4091-9361-42c0eee91cfa
displayName: 'class03', displayName: 'class03',
teachers: teacherClass03, teachers: teacherClass03,
students: studentsClass03, students: studentsClass03,
@ -40,14 +40,14 @@ export function makeTestClasses(em: EntityManager, students: Student[], teachers
const teacherClass04: Teacher[] = teachers.slice(2, 3); const teacherClass04: Teacher[] = teachers.slice(2, 3);
class04 = em.create(Class, { class04 = em.create(Class, {
classId: '33d03536-83b8-4880-9982-9bbf2f908ddf', classId: 'Q8N5YC', // 33d03536-83b8-4880-9982-9bbf2f908ddf
displayName: 'class04', displayName: 'class04',
teachers: teacherClass04, teachers: teacherClass04,
students: studentsClass04, students: studentsClass04,
}); });
classWithTestleerlingAndTestleerkracht = em.create(Class, { classWithTestleerlingAndTestleerkracht = em.create(Class, {
classId: 'a75298b5-18aa-471d-8eeb-5d77eb989393', classId: 'ZAV71B', // Was a75298b5-18aa-471d-8eeb-5d77eb989393
displayName: 'Testklasse', displayName: 'Testklasse',
teachers: [getTestleerkracht1()], teachers: [getTestleerkracht1()],
students: [getTestleerling1()], students: [getTestleerling1()],

View file

@ -7,6 +7,7 @@ export interface AssignmentDTO {
description: string; description: string;
learningPath: string; learningPath: string;
language: string; language: string;
deadline: Date;
groups: GroupDTO[] | string[][]; groups: GroupDTO[] | string[][];
} }

View file

@ -0,0 +1,54 @@
.h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
font-size: 50px;
padding-left: 1%;
}
.empty-message {
text-align: center;
font-size: 18px;
}
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
.table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
.table td,
.table th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
@media screen and (max-width: 850px) {
.h1 {
text-align: center;
padding-left: 0;
}
}

View file

@ -57,6 +57,22 @@
</div> </div>
<v-row v-else> <v-row v-else>
<v-col
cols="12"
sm="6"
md="4"
lg="4"
class="d-flex"
>
<ThemeCard
path="/learningPath/search"
:is-absolute-path="true"
:title="t('searchAllLearningPathsTitle')"
:description="t('searchAllLearningPathsDescription')"
icon="mdi-magnify"
class="fill-height grey-bg-card"
/>
</v-col>
<v-col <v-col
v-for="card in cards" v-for="card in cards"
:key="card.key" :key="card.key"
@ -74,24 +90,13 @@
class="fill-height" class="fill-height"
/> />
</v-col> </v-col>
<v-col
cols="12"
sm="6"
md="4"
lg="4"
class="d-flex"
>
<ThemeCard
path="/learningPath/search"
:is-absolute-path="true"
:title="t('searchAllLearningPathsTitle')"
:description="t('searchAllLearningPathsDescription')"
icon="mdi-magnify"
class="fill-height"
/>
</v-col>
</v-row> </v-row>
</v-container> </v-container>
</template> </template>
<style scoped></style> <style scoped>
.grey-bg-card {
background-color: #f6faf2;
border: 2px solid #0e6942;
}
</style>

View file

@ -14,6 +14,7 @@
const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable
const name: string = auth.authState.user!.profile.name!; const name: string = auth.authState.user!.profile.name!;
const email = auth.authState.user!.profile.email;
const initials: string = name const initials: string = name
.split(" ") .split(" ")
.map((n) => n[0]) .map((n) => n[0])
@ -90,7 +91,11 @@
<!-- >--> <!-- >-->
<!-- {{ t("discussions") }}--> <!-- {{ t("discussions") }}-->
<!-- </v-btn>--> <!-- </v-btn>-->
<v-menu open-on-hover> </v-toolbar-items>
<v-menu
open-on-hover
open-on-click
>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn <v-btn
v-bind="props" v-bind="props"
@ -114,7 +119,6 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-menu> </v-menu>
</v-toolbar-items>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-dialog max-width="500"> <v-dialog max-width="500">
<template v-slot:activator="{ props: activatorProps }"> <template v-slot:activator="{ props: activatorProps }">
@ -158,12 +162,43 @@
</v-card> </v-card>
</template> </template>
</v-dialog> </v-dialog>
<v-avatar <v-menu min-width="200px">
size="large" <template v-slot:activator="{ props }">
color="#0e6942" <v-btn
class="user-button" icon
>{{ initials }}</v-avatar v-bind="props"
> >
<v-avatar
color="#0e6942"
size="large"
class="user-button"
>
<span>{{ initials }}</span>
</v-avatar>
</v-btn>
</template>
<v-card>
<v-card-text>
<div class="mx-auto text-center">
<v-avatar color="#0e6942">
<span class="text-h5">{{ initials }}</span>
</v-avatar>
<h3>{{ name }}</h3>
<p class="text-caption mt-1">{{ email }}</p>
<v-divider class="my-3"></v-divider>
<v-btn
variant="text"
rounded
append-icon="mdi-logout"
@click="performLogout"
to="/login"
>{{ t("logout") }}</v-btn
>
<v-divider class="my-3"></v-divider>
</div>
</v-card-text>
</v-card>
</v-menu>
</v-app-bar> </v-app-bar>
<v-navigation-drawer <v-navigation-drawer
v-model="drawer" v-model="drawer"
@ -248,6 +283,12 @@
text-transform: none; text-transform: none;
} }
.translate-button {
z-index: 1;
position: relative;
margin-left: 10px;
}
@media (max-width: 700px) { @media (max-width: 700px) {
.menu { .menu {
display: none; display: none;

View file

@ -1,49 +1,30 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, watch } from "vue";
import { deadlineRules } from "@/utils/assignment-rules.ts"; import { deadlineRules } from "@/utils/assignment-rules.ts";
const date = ref(""); const emit = defineEmits<(e: "update:deadline", value: Date) => void>();
const time = ref("23:59");
const emit = defineEmits(["update:deadline"]);
const formattedDeadline = computed(() => { const datetime = ref("");
if (!date.value || !time.value) return "";
return `${date.value} ${time.value}`; // Watch the datetime value and emit the update
watch(datetime, (val) => {
const newDate = new Date(val);
if (!isNaN(newDate.getTime())) {
emit("update:deadline", newDate);
}
}); });
function updateDeadline(): void {
if (date.value && time.value) {
emit("update:deadline", formattedDeadline.value);
}
}
</script> </script>
<template> <template>
<div>
<v-card-text> <v-card-text>
<v-text-field <v-text-field
v-model="date" v-model="datetime"
label="Select Deadline Date" type="datetime-local"
type="date" label="Select Deadline"
variant="outlined" variant="outlined"
density="compact" density="compact"
:rules="deadlineRules" :rules="deadlineRules"
required required
@update:modelValue="updateDeadline" />
></v-text-field>
</v-card-text> </v-card-text>
<v-card-text>
<v-text-field
v-model="time"
label="Select Deadline Time"
type="time"
variant="outlined"
density="compact"
@update:modelValue="updateDeadline"
></v-text-field>
</v-card-text>
</div>
</template> </template>
<style scoped></style>

View file

@ -1,6 +1,5 @@
import { BaseController } from "@/controllers/base-controller.ts"; import { BaseController } from "@/controllers/base-controller.ts";
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts"; import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { ClassesResponse } from "@/controllers/classes.ts"; import type { ClassesResponse } from "@/controllers/classes.ts";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
@ -40,10 +39,6 @@ export class TeacherController extends BaseController {
return this.get<StudentsResponse>(`/${username}/students`, { full }); return this.get<StudentsResponse>(`/${username}/students`, { full });
} }
async getQuestions(username: string, full = false): Promise<QuestionsResponse> {
return this.get<QuestionsResponse>(`/${username}/questions`, { full });
}
async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> { async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> {
return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`); return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`);
} }

View file

@ -21,6 +21,7 @@
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.", "JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
"invalidFormat": "Ungültiges Format", "invalidFormat": "Ungültiges Format",
"submitCode": "senden", "submitCode": "senden",
"submit": "senden",
"members": "Mitglieder", "members": "Mitglieder",
"themes": "Themen", "themes": "Themen",
"choose-theme": "Wählen Sie ein Thema", "choose-theme": "Wählen Sie ein Thema",
@ -68,10 +69,10 @@
"pick-class": "Wählen Sie eine klasse", "pick-class": "Wählen Sie eine klasse",
"choose-students": "Studenten auswählen", "choose-students": "Studenten auswählen",
"create-group": "Gruppe erstellen", "create-group": "Gruppe erstellen",
"class": "klasse", "class": "Klasse",
"delete": "löschen", "delete": "löschen",
"view-assignment": "Auftrag anzeigen", "view-assignment": "Auftrag anzeigen",
"code": "code", "code": "Code",
"invitations": "Einladungen", "invitations": "Einladungen",
"createClass": "Klasse erstellen", "createClass": "Klasse erstellen",
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.", "createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
@ -83,7 +84,7 @@
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden", "onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
"close": "schließen", "close": "schließen",
"copied": "kopiert!", "copied": "kopiert!",
"accept": "akzeptieren", "accept": "Akzeptieren",
"deny": "ablehnen", "deny": "ablehnen",
"sent": "sent", "sent": "sent",
"failed": "fehlgeschlagen", "failed": "fehlgeschlagen",
@ -109,7 +110,7 @@
"remove": "entfernen", "remove": "entfernen",
"students": "Studenten", "students": "Studenten",
"classJoinRequests": "Beitrittsanfragen", "classJoinRequests": "Beitrittsanfragen",
"reject": "ablehnen", "reject": "Ablehnen",
"areusure": "Sind Sie sicher?", "areusure": "Sind Sie sicher?",
"yes": "ja", "yes": "ja",
"teachers": "Lehrer", "teachers": "Lehrer",
@ -120,5 +121,18 @@
"invite": "einladen", "invite": "einladen",
"assignmentIndicator": "AUFGABE", "assignmentIndicator": "AUFGABE",
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen", "searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen." "searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen.",
"no-students-found": "Diese Klasse hat keine Schüler.",
"no-invitations-found": "Sie haben keine ausstehenden Einladungen.",
"no-join-requests-found": "Es gibt keine ausstehenden Beitrittsanfragen für diese Klasse.",
"no-classes-found": "Sie sind noch keinem Kurs beigetreten.",
"classCreated": "Klasse erstellt!",
"success": "Erfolg",
"submitted": "eingereicht",
"see-submission": "Einsendung anzeigen",
"view-submissions": "Einsendungen anzeigen",
"valid-username": "Bitte geben Sie einen gültigen Benutzernamen ein",
"creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es erneut",
"no-assignments": "Derzeit gibt es keine Zuweisungen.",
"deadline": "deadline"
} }

View file

@ -33,6 +33,7 @@
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.", "JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
"invalidFormat": "Invalid format.", "invalidFormat": "Invalid format.",
"submitCode": "submit", "submitCode": "submit",
"submit": "submit",
"members": "Members", "members": "Members",
"themes": "Themes", "themes": "Themes",
"choose-theme": "Select a theme", "choose-theme": "Select a theme",
@ -68,21 +69,21 @@
"pick-class": "Pick a class", "pick-class": "Pick a class",
"choose-students": "Select students", "choose-students": "Select students",
"create-group": "Create group", "create-group": "Create group",
"class": "class", "class": "Class",
"delete": "delete", "delete": "delete",
"view-assignment": "View assignment", "view-assignment": "View assignment",
"code": "code", "code": "Code",
"invitations": "invitations", "invitations": "Invitations",
"createClass": "create class", "createClass": "Create class",
"classname": "classname", "classname": "classname",
"EnterNameOfClass": "Enter a classname.", "EnterNameOfClass": "Enter a classname.",
"create": "create", "create": "create",
"sender": "sender", "sender": "Sender",
"nameIsMandatory": "classname is mandatory", "nameIsMandatory": "classname is mandatory",
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)", "onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
"close": "close", "close": "close",
"copied": "copied!", "copied": "copied!",
"accept": "accept", "accept": "Accept",
"deny": "deny", "deny": "deny",
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.", "createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
"sent": "sent", "sent": "sent",
@ -107,12 +108,12 @@
"progress": "Progress", "progress": "Progress",
"created": "created", "created": "created",
"remove": "remove", "remove": "remove",
"students": "students", "students": "Students",
"classJoinRequests": "join requests", "classJoinRequests": "Join requests",
"reject": "reject", "reject": "Reject",
"areusure": "Are you sure?", "areusure": "Are you sure?",
"yes": "yes", "yes": "yes",
"teachers": "teachers", "teachers": "Teachers",
"accepted": "accepted", "accepted": "accepted",
"rejected": "rejected", "rejected": "rejected",
"enterUsername": "enter the username of the teacher you would like to invite", "enterUsername": "enter the username of the teacher you would like to invite",
@ -120,5 +121,18 @@
"invite": "invite", "invite": "invite",
"assignmentIndicator": "ASSIGNMENT", "assignmentIndicator": "ASSIGNMENT",
"searchAllLearningPathsTitle": "Search all learning paths", "searchAllLearningPathsTitle": "Search all learning paths",
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths." "searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths.",
"no-students-found": "This class has no students.",
"no-invitations-found": "You have no pending invitations.",
"no-join-requests-found": "There are no pending join requests for this class.",
"no-classes-found": "You are not yet part of a class.",
"classCreated": "class created!",
"success": "success",
"submitted": "submitted",
"see-submission": "view submission",
"view-submissions": "view submissions",
"valid-username": "please enter a valid username",
"creationFailed": "creation failed, please try again",
"no-assignments": "There are currently no assignments.",
"deadline": "deadline"
} }

View file

@ -33,6 +33,7 @@
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.", "JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
"invalidFormat": "Format non valide.", "invalidFormat": "Format non valide.",
"submitCode": "envoyer", "submitCode": "envoyer",
"submit": "envoyer",
"members": "Membres", "members": "Membres",
"themes": "Thèmes", "themes": "Thèmes",
"choose-theme": "Choisis un thème", "choose-theme": "Choisis un thème",
@ -68,22 +69,22 @@
"pick-class": "Choisissez une classe", "pick-class": "Choisissez une classe",
"choose-students": "Sélectionnez des élèves", "choose-students": "Sélectionnez des élèves",
"create-group": "Créer un groupe", "create-group": "Créer un groupe",
"class": "classe", "class": "Classe",
"delete": "supprimer", "delete": "supprimer",
"view-assignment": "Voir le travail", "view-assignment": "Voir le travail",
"code": "code", "code": "Code",
"invitations": "invitations", "invitations": "Invitations",
"createClass": "créer une classe", "createClass": "Créer une classe",
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.", "createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
"classname": "nom de classe", "classname": "nom de classe",
"EnterNameOfClass": "saisir un nom de classe.", "EnterNameOfClass": "saisir un nom de classe.",
"create": "créer", "create": "créer",
"sender": "expéditeur", "sender": "Expéditeur",
"nameIsMandatory": "le nom de classe est obligatoire", "nameIsMandatory": "le nom de classe est obligatoire",
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)", "onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
"close": "fermer", "close": "fermer",
"copied": "copié!", "copied": "copié!",
"accept": "accepter", "accept": "Accepter",
"deny": "refuser", "deny": "refuser",
"sent": "envoyé", "sent": "envoyé",
"failed": "échoué", "failed": "échoué",
@ -107,12 +108,13 @@
"submission": "Soumission", "submission": "Soumission",
"progress": "Progrès", "progress": "Progrès",
"remove": "supprimer", "remove": "supprimer",
"students": "étudiants", "students": "Étudiants",
"classJoinRequests": "demandes d'adhésion",
"reject": "rejeter", "classJoinRequests": "Demandes d'adhésion",
"reject": "Rejeter",
"areusure": "Êtes-vous sûr?", "areusure": "Êtes-vous sûr?",
"yes": "oui", "yes": "oui",
"teachers": "enseignants", "teachers": "Enseignants",
"accepted": "acceptée", "accepted": "acceptée",
"rejected": "rejetée", "rejected": "rejetée",
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter", "enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
@ -120,5 +122,18 @@
"invite": "inviter", "invite": "inviter",
"assignmentIndicator": "DEVOIR", "assignmentIndicator": "DEVOIR",
"searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage", "searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage",
"searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles." "searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles.",
"no-students-found": "Cette classe n'a pas d'élèves.",
"no-invitations-found": "Vous n'avez aucune invitation en attente.",
"no-join-requests-found": "Il n'y a aucune demande d'adhésion en attente pour cette classe.",
"no-classes-found": "Vous ne faites pas encore partie d'une classe.",
"classCreated": "Classe créée !",
"success": "succès",
"submitted": "soumis",
"see-submission": "voir la soumission",
"view-submissions": "voir les soumissions",
"valid-username": "veuillez entrer un nom d'utilisateur valide",
"creationFailed": "échec de la création, veuillez réessayer",
"no-assignments": "Il n'y a actuellement aucun travail.",
"deadline": "délai"
} }

View file

@ -33,6 +33,7 @@
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.", "JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
"invalidFormat": "Ongeldig formaat.", "invalidFormat": "Ongeldig formaat.",
"submitCode": "verzenden", "submitCode": "verzenden",
"submit": "verzenden",
"members": "Leden", "members": "Leden",
"themes": "Lesthema's", "themes": "Lesthema's",
"choose-theme": "Kies een thema", "choose-theme": "Kies een thema",
@ -68,22 +69,22 @@
"pick-class": "Kies een klas", "pick-class": "Kies een klas",
"choose-students": "Studenten selecteren", "choose-students": "Studenten selecteren",
"create-group": "Groep aanmaken", "create-group": "Groep aanmaken",
"class": "klas", "class": "Klas",
"delete": "verwijderen", "delete": "verwijderen",
"view-assignment": "Opdracht bekijken", "view-assignment": "Opdracht bekijken",
"code": "code", "code": "Code",
"invitations": "uitnodigingen", "invitations": "Uitnodigingen",
"createClass": "klas aanmaken", "createClass": "Klas aanmaken",
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.", "createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
"classname": "klasnaam", "classname": "klasnaam",
"EnterNameOfClass": "Geef een klasnaam op.", "EnterNameOfClass": "Geef een klasnaam op.",
"create": "aanmaken", "create": "aanmaken",
"sender": "afzender", "sender": "Afzender",
"nameIsMandatory": "klasnaam is verplicht", "nameIsMandatory": "klasnaam is verplicht",
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)", "onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
"close": "sluiten", "close": "sluiten",
"copied": "gekopieerd!", "copied": "gekopieerd!",
"accept": "accepteren", "accept": "Accepteren",
"deny": "weigeren", "deny": "weigeren",
"sent": "verzonden", "sent": "verzonden",
"failed": "mislukt", "failed": "mislukt",
@ -107,12 +108,12 @@
"submission": "Indiening", "submission": "Indiening",
"progress": "Vooruitgang", "progress": "Vooruitgang",
"remove": "verwijder", "remove": "verwijder",
"students": "studenten", "students": "Studenten",
"classJoinRequests": "deelname verzoeken", "classJoinRequests": "Deelname verzoeken",
"reject": "weiger", "reject": "Weiger",
"areusure": "Bent u zeker?", "areusure": "Bent u zeker?",
"yes": "ja", "yes": "ja",
"teachers": "leerkrachten", "teachers": "Leerkrachten",
"accepted": "geaccepteerd", "accepted": "geaccepteerd",
"rejected": "geweigerd", "rejected": "geweigerd",
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in", "enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
@ -120,5 +121,18 @@
"invite": "uitnodigen", "invite": "uitnodigen",
"assignmentIndicator": "OPDRACHT", "assignmentIndicator": "OPDRACHT",
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken", "searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken." "searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken.",
"no-students-found": "Deze klas heeft geen leerlingen.",
"no-invitations-found": "U heeft geen openstaande uitnodigingen.",
"no-join-requests-found": "Er zijn geen openstaande verzoeken om lid te worden van deze klas.",
"no-classes-found": "U maakt nog geen deel uit van een klas.",
"classCreated": "Klas aangemaakt!",
"success": "succes",
"submitted": "ingediend",
"see-submission": "inzending bekijken",
"view-submissions": "inzendingen bekijken",
"valid-username": "voer een geldige gebruikersnaam in",
"creationFailed": "aanmaak mislukt, probeer het opnieuw",
"no-assignments": "Er zijn momenteel geen opdrachten.",
"deadline": "deadline"
} }

View file

@ -10,7 +10,6 @@ import {
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts"; import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
import type { ClassesResponse } from "@/controllers/classes.ts"; import type { ClassesResponse } from "@/controllers/classes.ts";
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts"; import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher"; import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts"; import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
@ -33,10 +32,6 @@ function teacherStudentsQueryKey(username: string, full: boolean): [string, stri
return ["teacher-students", username, full]; return ["teacher-students", username, full];
} }
function teacherQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] {
return ["teacher-questions", username, full];
}
export function teacherClassJoinRequests(classId: string): [string, string] { export function teacherClassJoinRequests(classId: string): [string, string] {
return ["teacher-class-join-requests", classId]; return ["teacher-class-join-requests", classId];
} }
@ -80,17 +75,6 @@ export function useTeacherStudentsQuery(
}); });
} }
export function useTeacherQuestionsQuery(
username: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = false,
): UseQueryReturnType<QuestionsResponse, Error> {
return useQuery({
queryKey: computed(() => teacherQuestionsQueryKey(toValue(username)!, toValue(full))),
queryFn: async () => teacherController.getQuestions(toValue(username)!, toValue(full)),
enabled: () => Boolean(toValue(username)),
});
}
export function useTeacherJoinRequestsQuery( export function useTeacherJoinRequestsQuery(
username: MaybeRefOrGetter<string | undefined>, username: MaybeRefOrGetter<string | undefined>,
classId: MaybeRefOrGetter<string | undefined>, classId: MaybeRefOrGetter<string | undefined>,

View file

@ -28,7 +28,7 @@
alt="Dwengo logo" alt="Dwengo logo"
style="align-self: center" style="align-self: center"
/> />
<h1>{{ t("homeTitle") }}</h1> <h1 class="h1">{{ t("homeTitle") }}</h1>
<p class="info"> <p class="info">
{{ t("homeIntroduction1") }} {{ t("homeIntroduction1") }}
</p> </p>
@ -84,7 +84,10 @@
</div> </div>
</div> </div>
<div class="container_right"> <div class="container_right">
<v-menu open-on-hover> <v-menu
open-on-hover
open-on-click
>
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn <v-btn
v-bind="props" v-bind="props"

View file

@ -1,6 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from "vue-router";
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
import auth from "@/services/auth/auth-service.ts"; import auth from "@/services/auth/auth-service.ts";
import { watch } from "vue";
const router = useRouter();
watch(
() => auth.isLoggedIn.value,
async (newVal) => {
if (newVal) {
await router.push("/user");
}
},
{ immediate: true },
);
async function loginAsStudent(): Promise<void> { async function loginAsStudent(): Promise<void> {
await auth.loginAs("student"); await auth.loginAs("student");
@ -9,10 +23,6 @@
async function loginAsTeacher(): Promise<void> { async function loginAsTeacher(): Promise<void> {
await auth.loginAs("teacher"); await auth.loginAs("teacher");
} }
async function performLogout(): Promise<void> {
await auth.logout();
}
</script> </script>
<template> <template>
@ -65,13 +75,6 @@
</div> </div>
</ul> </ul>
</div> </div>
<div v-if="auth.isLoggedIn.value">
<p>
You are currently logged in as {{ auth.authState.user!.profile.name }} ({{ auth.authState.activeRole }})
</p>
<v-btn @click="performLogout">Logout</v-btn>
<v-btn to="/user">home</v-btn>
</div>
</main> </main>
</template> </template>

View file

@ -48,7 +48,7 @@
// Disable combobox when learningPath prop is passed // Disable combobox when learningPath prop is passed
const lpIsSelected = route.query.hruid !== undefined; const lpIsSelected = route.query.hruid !== undefined;
const deadline = ref(null); const deadline = ref(new Date());
const description = ref(""); const description = ref("");
const groups = ref<string[][]>([]); const groups = ref<string[][]>([]);
@ -86,6 +86,7 @@
title: assignmentTitle.value, title: assignmentTitle.value,
description: description.value, description: description.value,
learningPath: lp || "", learningPath: lp || "",
deadline: deadline.value,
language: language.value, language: language.value,
groups: groups.value, groups: groups.value,
}; };
@ -96,7 +97,7 @@
<template> <template>
<div class="main-container"> <div class="main-container">
<h1 class="title">{{ t("new-assignment") }}</h1> <h1 class="h1">{{ t("new-assignment") }}</h1>
<v-card class="form-card"> <v-card class="form-card">
<v-form <v-form
ref="form" ref="form"

View file

@ -9,8 +9,9 @@
import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
import { asyncComputed } from "@vueuse/core"; import { asyncComputed } from "@vueuse/core";
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
import "../../assets/common.css";
const { t } = useI18n(); const { t, locale } = useI18n();
const router = useRouter(); const router = useRouter();
const role = ref(auth.authState.activeRole); const role = ref(auth.authState.activeRole);
@ -27,13 +28,13 @@
classesQueryResults = useStudentClassesQuery(username, true); classesQueryResults = useStudentClassesQuery(username, true);
} }
//TODO: remove later
const classController = new ClassController(); const classController = new ClassController();
//TODO: replace by query that fetches all user's assignment const assignments = asyncComputed(
const assignments = asyncComputed(async () => { async () => {
const classes = classesQueryResults?.data?.value?.classes; const classes = classesQueryResults?.data?.value?.classes;
if (!classes) return []; if (!classes) return [];
const result = await Promise.all( const result = await Promise.all(
(classes as ClassDTO[]).map(async (cls) => { (classes as ClassDTO[]).map(async (cls) => {
const { assignments } = await classController.getAssignments(cls.id); const { assignments } = await classController.getAssignments(cls.id);
@ -44,13 +45,30 @@
description: a.description, description: a.description,
learningPath: a.learningPath, learningPath: a.learningPath,
language: a.language, language: a.language,
deadline: a.deadline,
groups: a.groups, groups: a.groups,
})); }));
}), }),
); );
return result.flat(); // Order the assignments by deadline
}, []); return result.flat().sort((a, b) => {
const now = Date.now();
const aTime = new Date(a.deadline).getTime();
const bTime = new Date(b.deadline).getTime();
const aIsPast = aTime < now;
const bIsPast = bTime < now;
if (aIsPast && !bIsPast) return 1;
if (!aIsPast && bIsPast) return -1;
return aTime - bTime;
});
},
[],
{ evaluating: true },
);
async function goToCreateAssignment(): Promise<void> { async function goToCreateAssignment(): Promise<void> {
await router.push("/assignment/create"); await router.push("/assignment/create");
@ -72,6 +90,35 @@
mutate({ cid: clsId, an: num }); mutate({ cid: clsId, an: num });
} }
function formatDate(date?: string | Date): string {
if (!date) return "";
const d = new Date(date);
// Choose locale based on selected language
const currentLocale = locale.value;
return d.toLocaleDateString(currentLocale, {
weekday: "short",
day: "2-digit",
month: "long",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
function getDeadlineClass(deadline?: string | Date): string {
if (!deadline) return "";
const date = new Date(deadline);
const now = new Date();
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);
if (date.getTime() < now.getTime()) return "deadline-passed";
if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours";
return "deadline-upcoming";
}
onMounted(async () => { onMounted(async () => {
const user = await auth.loadUser(); const user = await auth.loadUser();
username.value = user?.profile?.preferred_username ?? ""; username.value = user?.profile?.preferred_username ?? "";
@ -80,7 +127,7 @@
<template> <template>
<div class="assignments-container"> <div class="assignments-container">
<h1>{{ t("assignments") }}</h1> <h1 class="h1">{{ t("assignments") }}</h1>
<v-btn <v-btn
v-if="isTeacher" v-if="isTeacher"
@ -107,6 +154,13 @@
{{ assignment.class.displayName }} {{ assignment.class.displayName }}
</span> </span>
</div> </div>
<div
class="assignment-deadline"
:class="getDeadlineClass(assignment.deadline)"
>
{{ t("deadline") }}:
<span>{{ formatDate(assignment.deadline) }}</span>
</div>
</div> </div>
<div class="spacer"></div> <div class="spacer"></div>
@ -131,6 +185,13 @@
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="assignments.length === 0">
<v-col cols="12">
<div class="no-assignments">
{{ t("no-assignments") }}
</div>
</v-col>
</v-row>
</v-container> </v-container>
</div> </div>
</template> </template>
@ -139,18 +200,32 @@
.assignments-container { .assignments-container {
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 2% 4%;
box-sizing: border-box; box-sizing: border-box;
} }
.center-btn { .center-btn {
display: block; display: block;
margin-left: auto; margin: 0 auto 2rem auto;
margin-right: auto; font-weight: 600;
background-color: #10ad61;
color: white;
transition: background-color 0.2s;
}
.center-btn:hover {
background-color: #0e6942;
} }
.assignment-card { .assignment-card {
padding: 1rem; padding: 1.25rem;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
background-color: white;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.assignment-card:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
} }
.top-content { .top-content {
@ -158,6 +233,35 @@
word-break: break-word; word-break: break-word;
} }
.assignment-title {
font-weight: 700;
font-size: 1.4rem;
color: #0e6942;
margin-bottom: 0.3rem;
}
.assignment-class,
.assignment-deadline {
font-size: 0.95rem;
color: #444;
margin-bottom: 0.2rem;
}
.class-name {
font-weight: 600;
color: #097180;
}
.assignment-deadline.deadline-passed {
color: #d32f2f;
font-weight: bold;
}
.assignment-deadline.deadline-in24hours {
color: #f57c00;
font-weight: bold;
}
.spacer { .spacer {
flex: 1; flex: 1;
} }
@ -165,24 +269,14 @@
.button-row { .button-row {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 0.5rem; gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.assignment-title { .no-assignments {
font-weight: bold; text-align: center;
font-size: 1.5rem; font-size: 1.2rem;
margin-bottom: 0.1rem; color: #777;
word-break: break-word; padding: 3rem 0;
}
.assignment-class {
color: #666;
font-size: 0.95rem;
}
.class-name {
font-weight: 500;
color: #333;
} }
</style> </style>

View file

@ -13,6 +13,7 @@
import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations"; import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation"; import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
import { useDisplay } from "vuetify"; import { useDisplay } from "vuetify";
import "../../assets/common.css";
const { t } = useI18n(); const { t } = useI18n();
@ -112,7 +113,7 @@
function sentInvite(): void { function sentInvite(): void {
if (!usernameTeacher.value) { if (!usernameTeacher.value) {
showSnackbar(t("please enter a valid username"), "error"); showSnackbar(t("valid-username"), "error");
return; return;
} }
const data: TeacherInvitationData = { const data: TeacherInvitationData = {
@ -186,7 +187,7 @@
v-slot="classResponse: { data: ClassResponse }" v-slot="classResponse: { data: ClassResponse }"
> >
<div> <div>
<h1 class="title">{{ classResponse.data.class.displayName }}</h1> <h1 class="h1">{{ classResponse.data.class.displayName }}</h1>
<using-query-result <using-query-result
:query-result="getStudents" :query-result="getStudents"
v-slot="studentsResponse: { data: StudentsResponse }" v-slot="studentsResponse: { data: StudentsResponse }"
@ -211,19 +212,34 @@
<th class="header"></th> <th class="header"></th>
</tr> </tr>
</thead> </thead>
<tbody>
<tbody v-if="studentsResponse.data.students.length">
<tr <tr
v-for="s in studentsResponse.data.students as StudentDTO[]" v-for="s in studentsResponse.data.students as StudentDTO[]"
:key="s.id" :key="s.id"
> >
<td> <td>{{ s.firstName + " " + s.lastName }}</td>
{{ s.firstName + " " + s.lastName }}
</td>
<td> <td>
<v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn> <v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn>
</td> </td>
</tr> </tr>
</tbody> </tbody>
<tbody v-else>
<tr>
<td
colspan="2"
class="empty-message"
>
<v-icon
icon="mdi-information-outline"
size="small"
>
</v-icon>
{{ t("no-students-found") }}
</td>
</tr>
</tbody>
</v-table> </v-table>
</v-col> </v-col>
<using-query-result <using-query-result
@ -242,7 +258,7 @@
<th class="header">{{ t("accept") + "/" + t("reject") }}</th> <th class="header">{{ t("accept") + "/" + t("reject") }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody v-if="joinRequests.data.joinRequests.length">
<tr <tr
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]" v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
:key="(jr.class, jr.requester, jr.status)" :key="(jr.class, jr.requester, jr.status)"
@ -287,6 +303,21 @@
</td> </td>
</tr> </tr>
</tbody> </tbody>
<tbody v-else>
<tr>
<td
colspan="2"
class="empty-message"
>
<v-icon
icon="mdi-information-outline"
size="small"
>
</v-icon>
{{ t("no-join-requests-found") }}
</td>
</tr>
</tbody>
</v-table> </v-table>
</v-col> </v-col>
</using-query-result> </using-query-result>
@ -356,49 +387,6 @@
</main> </main>
</template> </template>
<style scoped> <style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 { h2 {
color: #0e6942; color: #0e6942;
font-size: 30px; font-size: 30px;
@ -407,6 +395,7 @@
.join { .join {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: 1%;
gap: 20px; gap: 20px;
margin-top: 50px; margin-top: 50px;
} }
@ -416,16 +405,7 @@
text-decoration: underline; text-decoration: underline;
} }
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join { .join {
text-align: center; text-align: center;
align-items: center; align-items: center;

View file

@ -12,6 +12,7 @@
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes"; import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
import type { StudentsResponse } from "@/controllers/students"; import type { StudentsResponse } from "@/controllers/students";
import type { TeachersResponse } from "@/controllers/teachers"; import type { TeachersResponse } from "@/controllers/teachers";
import "../../assets/common.css";
const { t } = useI18n(); const { t } = useI18n();
@ -135,7 +136,7 @@
></v-empty-state> ></v-empty-state>
</div> </div>
<div v-else> <div v-else>
<h1 class="title">{{ t("classes") }}</h1> <h1 class="h1">{{ t("classes") }}</h1>
<using-query-result <using-query-result
:query-result="classesQuery" :query-result="classesQuery"
v-slot="classResponse: { data: ClassesResponse }" v-slot="classResponse: { data: ClassesResponse }"
@ -161,7 +162,7 @@
<th class="header">{{ t("members") }}</th> <th class="header">{{ t("members") }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody v-if="classResponse.data.classes.length">
<tr <tr
v-for="c in classResponse.data.classes as ClassDTO[]" v-for="c in classResponse.data.classes as ClassDTO[]"
:key="c.id" :key="c.id"
@ -181,6 +182,21 @@
</td> </td>
</tr> </tr>
</tbody> </tbody>
<tbody v-else>
<tr>
<td
colspan="3"
class="empty-message"
>
<v-icon
icon="mdi-information-outline"
size="small"
>
</v-icon>
{{ t("no-classes-found") }}
</td>
</tr>
</tbody>
</v-table> </v-table>
</v-col> </v-col>
</v-row> </v-row>
@ -271,49 +287,6 @@
</main> </main>
</template> </template>
<style scoped> <style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 { h2 {
color: #0e6942; color: #0e6942;
font-size: 30px; font-size: 30px;
@ -321,6 +294,7 @@
.join { .join {
display: flex; display: flex;
margin-left: 1%;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
margin-top: 50px; margin-top: 50px;
@ -331,16 +305,7 @@
text-decoration: underline; text-decoration: underline;
} }
main {
margin-left: 30px;
}
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {
h1 {
text-align: center;
padding-left: 0;
}
.join { .join {
text-align: center; text-align: center;
align-items: center; align-items: center;

View file

@ -15,6 +15,7 @@
useTeacherInvitationsReceivedQuery, useTeacherInvitationsReceivedQuery,
} from "@/queries/teacher-invitations"; } from "@/queries/teacher-invitations";
import { useDisplay } from "vuetify"; import { useDisplay } from "vuetify";
import "../../assets/common.css";
const { t } = useI18n(); const { t } = useI18n();
@ -112,7 +113,7 @@
dialog.value = true; dialog.value = true;
} }
if (!className.value || className.value === "") { if (!className.value || className.value === "") {
showSnackbar(t("name is mandatory"), "error"); showSnackbar(t("nameIsMandatory"), "error");
} }
} }
@ -137,6 +138,13 @@
copied.value = true; copied.value = true;
} }
async function copyCode(selectedCode: string): Promise<void> {
code.value = selectedCode;
await copyToClipboard();
showSnackbar(t("copied"), "white");
copied.value = false;
}
// Custom breakpoints // Custom breakpoints
const customBreakpoints = { const customBreakpoints = {
xs: 0, xs: 0,
@ -183,7 +191,7 @@
></v-empty-state> ></v-empty-state>
</div> </div>
<div v-else> <div v-else>
<h1 class="title">{{ t("classes") }}</h1> <h1 class="h1">{{ t("classes") }}</h1>
<using-query-result <using-query-result
:query-result="classesQuery" :query-result="classesQuery"
v-slot="classesResponse: { data: ClassesResponse }" v-slot="classesResponse: { data: ClassesResponse }"
@ -212,7 +220,7 @@
<th class="header">{{ t("members") }}</th> <th class="header">{{ t("members") }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody v-if="classesResponse.data.classes.length">
<tr <tr
v-for="c in classesResponse.data.classes as ClassDTO[]" v-for="c in classesResponse.data.classes as ClassDTO[]"
:key="c.id" :key="c.id"
@ -227,7 +235,14 @@
</v-btn> </v-btn>
</td> </td>
<td> <td>
<span v-if="!isMdAndDown">{{ c.id }}</span> <v-btn
v-if="!isMdAndDown"
variant="text"
append-icon="mdi-content-copy"
@click="copyCode(c.id)"
>
{{ c.id }}
</v-btn>
<span <span
v-else v-else
style="cursor: pointer" style="cursor: pointer"
@ -239,6 +254,21 @@
<td>{{ c.students.length }}</td> <td>{{ c.students.length }}</td>
</tr> </tr>
</tbody> </tbody>
<tbody v-else>
<tr>
<td
colspan="3"
class="empty-message"
>
<v-icon
icon="mdi-information-outline"
size="small"
>
</v-icon>
{{ t("no-classes-found") }}
</td>
</tr>
</tbody>
</v-table> </v-table>
</v-col> </v-col>
<v-col <v-col
@ -318,7 +348,7 @@
</v-container> </v-container>
</using-query-result> </using-query-result>
<h1 class="title"> <h1 class="h1">
{{ t("invitations") }} {{ t("invitations") }}
</h1> </h1>
<v-container <v-container
@ -342,6 +372,7 @@
:query-result="allClassesQuery" :query-result="allClassesQuery"
v-slot="classesResponse: { data: ClassesResponse }" v-slot="classesResponse: { data: ClassesResponse }"
> >
<template v-if="invitationsResponse.data.invitations.length">
<tr <tr
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]" v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
:key="i.classId" :key="i.classId"
@ -355,7 +386,9 @@
</td> </td>
<td> <td>
{{ {{
(i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName (i.sender as TeacherDTO).firstName +
" " +
(i.sender as TeacherDTO).lastName
}} }}
</td> </td>
<td class="text-right"> <td class="text-right">
@ -397,6 +430,22 @@
></span> ></span>
</td> </td>
</tr> </tr>
</template>
<template v-else>
<tr>
<td
colspan="3"
class="empty-message"
>
<v-icon
icon="mdi-information-outline"
size="small"
>
</v-icon>
{{ t("no-invitations-found") }}
</td>
</tr>
</template>
</using-query-result> </using-query-result>
</using-query-result> </using-query-result>
</tbody> </tbody>
@ -449,49 +498,6 @@
</main> </main>
</template> </template>
<style scoped> <style scoped>
.header {
font-weight: bold !important;
background-color: #0e6942;
color: white;
padding: 10px;
}
table thead th:first-child {
border-top-left-radius: 10px;
}
.table thead th:last-child {
border-top-right-radius: 10px;
}
.table tbody tr:nth-child(odd) {
background-color: white;
}
.table tbody tr:nth-child(even) {
background-color: #f6faf2;
}
td,
th {
border-bottom: 1px solid #0e6942;
border-top: 1px solid #0e6942;
}
.table {
width: 90%;
padding-top: 10px;
border-collapse: collapse;
}
h1 {
color: #0e6942;
text-transform: uppercase;
font-weight: bolder;
padding-top: 2%;
font-size: 50px;
}
h2 { h2 {
color: #0e6942; color: #0e6942;
font-size: 30px; font-size: 30px;
@ -509,16 +515,7 @@
text-decoration: underline; text-decoration: underline;
} }
main {
margin-left: 30px;
}
@media screen and (max-width: 850px) { @media screen and (max-width: 850px) {
h1 {
text-align: center;
padding-left: 0;
}
.join { .join {
text-align: center; text-align: center;
align-items: center; align-items: center;
@ -541,10 +538,6 @@
flex-direction: column !important; flex-direction: column !important;
} }
.table {
width: 100%;
}
.responsive-col { .responsive-col {
max-width: 100% !important; max-width: 100% !important;
flex-basis: 100% !important; flex-basis: 100% !important;

View file

@ -3,6 +3,7 @@
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts"; import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts";
import BrowseThemes from "@/components/BrowseThemes.vue"; import BrowseThemes from "@/components/BrowseThemes.vue";
import "../../assets/common.css";
const { t, locale } = useI18n(); const { t, locale } = useI18n();
@ -46,7 +47,7 @@
<template> <template>
<div class="main-container"> <div class="main-container">
<h1 class="title">{{ t("themes") }}</h1> <h1 class="h1">{{ t("themes") }}</h1>
<v-container class="dropdowns"> <v-container class="dropdowns">
<v-select <v-select
class="v-select" class="v-select"
@ -77,24 +78,6 @@
</template> </template>
<style scoped> <style scoped>
.main-container {
min-height: 100vh;
min-width: 100vw;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.title {
max-width: 50rem;
margin-left: 1rem;
margin-top: 1rem;
text-align: center;
display: flex;
justify-content: center;
}
.dropdowns { .dropdowns {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -107,12 +90,6 @@
min-width: 100px; min-width: 100px;
} }
@media (max-width: 768px) {
.main-container {
padding: 1rem;
}
}
@media (max-width: 700px) { @media (max-width: 700px) {
.dropdowns { .dropdowns {
flex-direction: column; flex-direction: column;

View file

@ -287,6 +287,8 @@
<template v-slot:default> <template v-slot:default>
<v-btn <v-btn
class="button-in-nav" class="button-in-nav"
width="100%"
:color="COLORS.teacherExclusive"
@click="assign()" @click="assign()"
>{{ t("assignLearningPath") }}</v-btn >{{ t("assignLearningPath") }}</v-btn
> >

View file

@ -17,32 +17,52 @@
</script> </script>
<template> <template>
<div class="search-field-container"> <v-container class="search-page-container">
<learning-path-search-field class="search-field"></learning-path-search-field> <v-row
</div> justify="center"
class="mb-6"
>
<v-col
cols="12"
sm="8"
md="6"
lg="4"
>
<learning-path-search-field class="search-field" />
</v-col>
</v-row>
<v-row justify="center">
<v-col cols="12">
<using-query-result <using-query-result
:query-result="searchQueryResults" :query-result="searchQueryResults"
v-slot="{ data }: { data: LearningPath[] }" v-slot="{ data }: { data: LearningPath[] }"
> >
<learning-paths-grid :learning-paths="data"></learning-paths-grid> <learning-paths-grid :learning-paths="data" />
</using-query-result> </using-query-result>
<div content="empty-state-container">
<v-empty-state <div
v-if="!query" v-if="!query"
class="empty-state-container"
>
<v-empty-state
icon="mdi-magnify" icon="mdi-magnify"
:title="t('enterSearchTerm')" :title="t('enterSearchTerm')"
:text="t('enterSearchTermDescription')" :text="t('enterSearchTermDescription')"
></v-empty-state> />
</div> </div>
</v-col>
</v-row>
</v-container>
</template> </template>
<style scoped> <style scoped>
.search-field-container { .search-page-container {
display: block; padding-top: 40px;
margin: 20px; padding-bottom: 40px;
} }
.search-field { .search-field {
max-width: 300px; max-width: 100%;
} }
</style> </style>

View file

@ -5,7 +5,7 @@ describe("AssignmentController Tests", () => {
let controller: AssignmentController; let controller: AssignmentController;
beforeEach(() => { beforeEach(() => {
controller = new AssignmentController("8764b861-90a6-42e5-9732-c0d9eb2f55f9"); // Example class ID controller = new AssignmentController("X2J9QT"); // Example class ID (class01)
}); });
it("should fetch all assignments", async () => { it("should fetch all assignments", async () => {

View file

@ -3,7 +3,7 @@ import { GroupController } from "../../src/controllers/groups";
describe("Test controller groups", () => { describe("Test controller groups", () => {
it("Get groups", async () => { it("Get groups", async () => {
const classId = "8764b861-90a6-42e5-9732-c0d9eb2f55f9"; const classId = "X2J9QT"; // Class01
const assignmentNumber = 21000; const assignmentNumber = 21000;
const controller = new GroupController(classId, assignmentNumber); const controller = new GroupController(classId, assignmentNumber);

View file

@ -5,7 +5,7 @@ import { Language } from "../../src/data-objects/language";
describe("Test controller submissions", () => { describe("Test controller submissions", () => {
it("Get submission by number", async () => { it("Get submission by number", async () => {
const hruid = "id03"; const hruid = "id03";
const classId = "8764b861-90a6-42e5-9732-c0d9eb2f55f9"; const classId = "X2J9QT"; // Class01
const controller = new SubmissionController(hruid); const controller = new SubmissionController(hruid);
const data = await controller.getByNumber(Language.English, 1, classId, 1, 1, 1); const data = await controller.getByNumber(Language.English, 1, classId, 1, 1, 1);

4061
package-lock.json generated

File diff suppressed because it is too large Load diff