Merge branch 'dev' into github-actions/coverage
This commit is contained in:
commit
bd266d3594
111 changed files with 3698 additions and 2908 deletions
|
@ -21,6 +21,7 @@
|
|||
"@tanstack/vue-query": "^5.69.0",
|
||||
"axios": "^1.8.2",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.2",
|
||||
"vue-router": "^4.5.0",
|
||||
|
|
39
frontend/src/controllers/answers.ts
Normal file
39
frontend/src/controllers/answers.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import type { AnswerData, AnswerDTO, AnswerId } from "@dwengo-1/common/interfaces/answer";
|
||||
import { BaseController } from "@/controllers/base-controller.ts";
|
||||
import type { QuestionId } from "@dwengo-1/common/interfaces/question";
|
||||
|
||||
export interface AnswersResponse {
|
||||
answers: AnswerDTO[] | AnswerId[];
|
||||
}
|
||||
|
||||
export interface AnswerResponse {
|
||||
answer: AnswerDTO;
|
||||
}
|
||||
|
||||
export class AnswerController extends BaseController {
|
||||
constructor(questionId: QuestionId) {
|
||||
this.loId = questionId.learningObjectIdentifier;
|
||||
this.sequenceNumber = questionId.sequenceNumber;
|
||||
super(`learningObject/${loId.hruid}/:${loId.version}/questions/${this.sequenceNumber}/answers`);
|
||||
}
|
||||
|
||||
async getAll(full = true): Promise<AnswersResponse> {
|
||||
return this.get<AnswersResponse>("/", { lang: this.loId.lang, full });
|
||||
}
|
||||
|
||||
async getBy(seq: number): Promise<AnswerResponse> {
|
||||
return this.get<AnswerResponse>(`/${seq}`, { lang: this.loId.lang });
|
||||
}
|
||||
|
||||
async create(answerData: AnswerData): Promise<AnswerResponse> {
|
||||
return this.post<AnswerResponse>("/", answerData, { lang: this.loId.lang });
|
||||
}
|
||||
|
||||
async remove(seq: number): Promise<AnswerResponse> {
|
||||
return this.delete<AnswerResponse>(`/${seq}`, { lang: this.loId.lang });
|
||||
}
|
||||
|
||||
async update(seq: number, answerData: AnswerData): Promise<AnswerResponse> {
|
||||
return this.put<AnswerResponse>(`/${seq}`, answerData, { lang: this.loId.lang });
|
||||
}
|
||||
}
|
|
@ -1,5 +1,51 @@
|
|||
import type { AssignmentDTO } from "@dwengo-1/interfaces/assignment";
|
||||
import { BaseController } from "./base-controller";
|
||||
import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment";
|
||||
import type { SubmissionsResponse } from "./submissions";
|
||||
import type { QuestionsResponse } from "./questions";
|
||||
import type { GroupsResponse } from "./groups";
|
||||
|
||||
export interface AssignmentsResponse {
|
||||
assignments: AssignmentDTO[];
|
||||
} // TODO ID
|
||||
assignments: AssignmentDTO[] | string[];
|
||||
}
|
||||
|
||||
export interface AssignmentResponse {
|
||||
assignment: AssignmentDTO;
|
||||
}
|
||||
|
||||
export class AssignmentController extends BaseController {
|
||||
constructor(classid: string) {
|
||||
super(`class/${classid}/assignments`);
|
||||
}
|
||||
|
||||
async getAll(full = true): Promise<AssignmentsResponse> {
|
||||
return this.get<AssignmentsResponse>(`/`, { full });
|
||||
}
|
||||
|
||||
async getByNumber(num: number): Promise<AssignmentResponse> {
|
||||
return this.get<AssignmentResponse>(`/${num}`);
|
||||
}
|
||||
|
||||
async createAssignment(data: AssignmentDTO): Promise<AssignmentResponse> {
|
||||
return this.post<AssignmentResponse>(`/`, data);
|
||||
}
|
||||
|
||||
async deleteAssignment(num: number): Promise<AssignmentResponse> {
|
||||
return this.delete<AssignmentResponse>(`/${num}`);
|
||||
}
|
||||
|
||||
async updateAssignment(num: number, data: Partial<AssignmentDTO>): Promise<AssignmentResponse> {
|
||||
return this.put<AssignmentResponse>(`/${num}`, data);
|
||||
}
|
||||
|
||||
async getSubmissions(assignmentNumber: number, full = true): Promise<SubmissionsResponse> {
|
||||
return this.get<SubmissionsResponse>(`/${assignmentNumber}/submissions`, { full });
|
||||
}
|
||||
|
||||
async getQuestions(assignmentNumber: number, full = true): Promise<QuestionsResponse> {
|
||||
return this.get<QuestionsResponse>(`/${assignmentNumber}/questions`, { full });
|
||||
}
|
||||
|
||||
async getGroups(assignmentNumber: number, full = true): Promise<GroupsResponse> {
|
||||
return this.get<GroupsResponse>(`/${assignmentNumber}/groups`, { full });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export abstract class BaseController {
|
|||
}
|
||||
|
||||
private static assertSuccessResponse(response: AxiosResponse<unknown, unknown>): void {
|
||||
if (response.status / 100 !== 2) {
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new HttpErrorResponseException(response);
|
||||
}
|
||||
}
|
||||
|
@ -21,20 +21,20 @@ export abstract class BaseController {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
protected async post<T>(path: string, body: unknown): Promise<T> {
|
||||
const response = await apiClient.post<T>(this.absolutePathFor(path), body);
|
||||
protected async post<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.post<T>(this.absolutePathFor(path), body, { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async delete<T>(path: string): Promise<T> {
|
||||
const response = await apiClient.delete<T>(this.absolutePathFor(path));
|
||||
protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async put<T>(path: string, body: unknown): Promise<T> {
|
||||
const response = await apiClient.put<T>(this.absolutePathFor(path), body);
|
||||
protected async put<T>(path: string, body: unknown, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.put<T>(this.absolutePathFor(path), body, { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,80 @@
|
|||
import type { ClassDTO } from "@dwengo-1/interfaces/class";
|
||||
import { BaseController } from "./base-controller";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import type { StudentsResponse } from "./students";
|
||||
import type { AssignmentsResponse } from "./assignments";
|
||||
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||
import type { TeachersResponse } from "@/controllers/teachers.ts";
|
||||
|
||||
export interface ClassesResponse {
|
||||
classes: ClassDTO[] | string[];
|
||||
}
|
||||
|
||||
export interface ClassResponse {
|
||||
class: ClassDTO;
|
||||
}
|
||||
|
||||
export interface TeacherInvitationsResponse {
|
||||
invites: TeacherInvitationDTO[];
|
||||
}
|
||||
|
||||
export interface TeacherInvitationResponse {
|
||||
invite: TeacherInvitationDTO;
|
||||
}
|
||||
|
||||
export class ClassController extends BaseController {
|
||||
constructor() {
|
||||
super("class");
|
||||
}
|
||||
|
||||
async getAll(full = true): Promise<ClassesResponse> {
|
||||
return this.get<ClassesResponse>(`/`, { full });
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<ClassResponse> {
|
||||
return this.get<ClassResponse>(`/${id}`);
|
||||
}
|
||||
|
||||
async createClass(data: ClassDTO): Promise<ClassResponse> {
|
||||
return this.post<ClassResponse>(`/`, data);
|
||||
}
|
||||
|
||||
async deleteClass(id: string): Promise<ClassResponse> {
|
||||
return this.delete<ClassResponse>(`/${id}`);
|
||||
}
|
||||
|
||||
async updateClass(id: string, data: Partial<ClassDTO>): Promise<ClassResponse> {
|
||||
return this.put<ClassResponse>(`/${id}`, data);
|
||||
}
|
||||
|
||||
async getStudents(id: string, full = true): Promise<StudentsResponse> {
|
||||
return this.get<StudentsResponse>(`/${id}/students`, { full });
|
||||
}
|
||||
|
||||
async addStudent(id: string, username: string): Promise<ClassResponse> {
|
||||
return this.post<ClassResponse>(`/${id}/students`, { username });
|
||||
}
|
||||
|
||||
async deleteStudent(id: string, username: string): Promise<ClassResponse> {
|
||||
return this.delete<ClassResponse>(`/${id}/students/${username}`);
|
||||
}
|
||||
|
||||
async getTeachers(id: string, full = true): Promise<TeachersResponse> {
|
||||
return this.get<TeachersResponse>(`/${id}/teachers`, { full });
|
||||
}
|
||||
|
||||
async addTeacher(id: string, username: string): Promise<ClassResponse> {
|
||||
return this.post<ClassResponse>(`/${id}/teachers`, { username });
|
||||
}
|
||||
|
||||
async deleteTeacher(id: string, username: string): Promise<ClassResponse> {
|
||||
return this.delete<ClassResponse>(`/${id}/teachers/${username}`);
|
||||
}
|
||||
|
||||
async getTeacherInvitations(id: string, full = true): Promise<TeacherInvitationsResponse> {
|
||||
return this.get<TeacherInvitationsResponse>(`/${id}/teacher-invitations`, { full });
|
||||
}
|
||||
|
||||
async getAssignments(id: string, full = true): Promise<AssignmentsResponse> {
|
||||
return this.get<AssignmentsResponse>(`/${id}/assignments`, { full });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ThemeController } from "@/controllers/themes.ts";
|
||||
import { LearningObjectController } from "@/controllers/learning-objects.ts";
|
||||
import { LearningPathController } from "@/controllers/learning-paths.ts";
|
||||
import { ClassController } from "@/controllers/classes.ts";
|
||||
|
||||
export function controllerGetter<T>(factory: new () => T): () => T {
|
||||
let instance: T | undefined;
|
||||
|
@ -16,3 +17,4 @@ export function controllerGetter<T>(factory: new () => T): () => T {
|
|||
export const getThemeController = controllerGetter(ThemeController);
|
||||
export const getLearningObjectController = controllerGetter(LearningObjectController);
|
||||
export const getLearningPathController = controllerGetter(LearningPathController);
|
||||
export const getClassController = controllerGetter(ClassController);
|
||||
|
|
|
@ -1,5 +1,46 @@
|
|||
import type { GroupDTO } from "@dwengo-1/interfaces/group";
|
||||
import { BaseController } from "./base-controller";
|
||||
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
|
||||
import type { SubmissionsResponse } from "./submissions";
|
||||
import type { QuestionsResponse } from "./questions";
|
||||
|
||||
export interface GroupsResponse {
|
||||
groups: GroupDTO[];
|
||||
} // | TODO id
|
||||
}
|
||||
|
||||
export interface GroupResponse {
|
||||
group: GroupDTO;
|
||||
}
|
||||
|
||||
export class GroupController extends BaseController {
|
||||
constructor(classid: string, assignmentNumber: number) {
|
||||
super(`class/${classid}/assignments/${assignmentNumber}/groups`);
|
||||
}
|
||||
|
||||
async getAll(full = true): Promise<GroupsResponse> {
|
||||
return this.get<GroupsResponse>(`/`, { full });
|
||||
}
|
||||
|
||||
async getByNumber(num: number): Promise<GroupResponse> {
|
||||
return this.get<GroupResponse>(`/${num}`);
|
||||
}
|
||||
|
||||
async createGroup(data: GroupDTO): Promise<GroupResponse> {
|
||||
return this.post<GroupResponse>(`/`, data);
|
||||
}
|
||||
|
||||
async deleteGroup(num: number): Promise<GroupResponse> {
|
||||
return this.delete<GroupResponse>(`/${num}`);
|
||||
}
|
||||
|
||||
async updateGroup(num: number, data: Partial<GroupDTO>): Promise<GroupResponse> {
|
||||
return this.put<GroupResponse>(`/${num}`, data);
|
||||
}
|
||||
|
||||
async getSubmissions(groupNumber: number, full = true): Promise<SubmissionsResponse> {
|
||||
return this.get<SubmissionsResponse>(`/${groupNumber}/submissions`, { full });
|
||||
}
|
||||
|
||||
async getQuestions(groupNumber: number, full = true): Promise<QuestionsResponse> {
|
||||
return this.get<QuestionsResponse>(`/${groupNumber}/questions`, { full });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,38 @@
|
|||
import type { QuestionDTO, QuestionId } from "@dwengo-1/interfaces/question";
|
||||
import type { QuestionData, QuestionDTO, QuestionId } from "@dwengo-1/common/interfaces/question";
|
||||
import { BaseController } from "@/controllers/base-controller.ts";
|
||||
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
export interface QuestionsResponse {
|
||||
questions: QuestionDTO[] | QuestionId[];
|
||||
}
|
||||
|
||||
export interface QuestionResponse {
|
||||
question: QuestionDTO;
|
||||
}
|
||||
|
||||
export class QuestionController extends BaseController {
|
||||
constructor(loId: LearningObjectIdentifierDTO) {
|
||||
this.loId = loId;
|
||||
super(`learningObject/${loId.hruid}/:${loId.version}/questions`);
|
||||
}
|
||||
|
||||
async getAll(full = true): Promise<QuestionsResponse> {
|
||||
return this.get<QuestionsResponse>("/", { lang: this.loId.lang, full });
|
||||
}
|
||||
|
||||
async getBy(sequenceNumber: number): Promise<QuestionResponse> {
|
||||
return this.get<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang });
|
||||
}
|
||||
|
||||
async create(questionData: QuestionData): Promise<QuestionResponse> {
|
||||
return this.post<QuestionResponse>("/", questionData, { lang: this.loId.lang });
|
||||
}
|
||||
|
||||
async remove(sequenceNumber: number): Promise<QuestionResponse> {
|
||||
return this.delete<QuestionResponse>(`/${sequenceNumber}`, { lang: this.loId.lang });
|
||||
}
|
||||
|
||||
async update(sequenceNumber: number, questionData: QuestionData): Promise<QuestionResponse> {
|
||||
return this.put<QuestionResponse>(`/${sequenceNumber}`, questionData, { lang: this.loId.lang });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import type { AssignmentsResponse } from "@/controllers/assignments.ts";
|
|||
import type { GroupsResponse } from "@/controllers/groups.ts";
|
||||
import type { SubmissionsResponse } from "@/controllers/submissions.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { StudentDTO } from "@dwengo-1/interfaces/student";
|
||||
import type { ClassJoinRequestDTO } from "@dwengo-1/interfaces/class-join-request";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
import type { ClassJoinRequestDTO } from "@dwengo-1/common/interfaces/class-join-request";
|
||||
|
||||
export interface StudentsResponse {
|
||||
students: StudentDTO[] | string[];
|
||||
|
@ -70,7 +70,7 @@ export class StudentController extends BaseController {
|
|||
}
|
||||
|
||||
async createJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {
|
||||
return this.post<JoinRequestResponse>(`/${username}/joinRequests}`, classId);
|
||||
return this.post<JoinRequestResponse>(`/${username}/joinRequests`, { classId });
|
||||
}
|
||||
|
||||
async deleteJoinRequest(username: string, classId: string): Promise<JoinRequestResponse> {
|
||||
|
|
|
@ -1,5 +1,32 @@
|
|||
import { type SubmissionDTO, SubmissionDTOId } from "@dwengo-1/interfaces/submission";
|
||||
import { BaseController } from "./base-controller";
|
||||
import type { SubmissionDTO, SubmissionDTOId } from "@dwengo-1/common/interfaces/submission";
|
||||
|
||||
export interface SubmissionsResponse {
|
||||
submissions: SubmissionDTO[] | SubmissionDTOId[];
|
||||
}
|
||||
|
||||
export interface SubmissionResponse {
|
||||
submission: SubmissionDTO;
|
||||
}
|
||||
|
||||
export class SubmissionController extends BaseController {
|
||||
constructor(classid: string, assignmentNumber: number, groupNumber: number) {
|
||||
super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}`);
|
||||
}
|
||||
|
||||
async getAll(full = true): Promise<SubmissionsResponse> {
|
||||
return this.get<SubmissionsResponse>(`/`, { full });
|
||||
}
|
||||
|
||||
async getByNumber(submissionNumber: number): Promise<SubmissionResponse> {
|
||||
return this.get<SubmissionResponse>(`/${submissionNumber}`);
|
||||
}
|
||||
|
||||
async createSubmission(data: unknown): Promise<SubmissionResponse> {
|
||||
return this.post<SubmissionResponse>(`/`, data);
|
||||
}
|
||||
|
||||
async deleteSubmission(submissionNumber: number): Promise<SubmissionResponse> {
|
||||
return this.delete<SubmissionResponse>(`/${submissionNumber}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BaseController } from "@/controllers/base-controller.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 { TeacherDTO } from "@dwengo-1/interfaces/teacher";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
|
||||
export interface TeachersResponse {
|
||||
teachers: TeacherDTO[] | string[];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BaseController } from "@/controllers/base-controller.ts";
|
||||
import type { Theme } from "@dwengo-1/interfaces/theme";
|
||||
import type { Theme } from "@dwengo-1/common/interfaces/theme";
|
||||
|
||||
export class ThemeController extends BaseController {
|
||||
constructor() {
|
||||
|
|
|
@ -17,6 +17,11 @@
|
|||
"inclusive": "Inclusiv",
|
||||
"sociallyRelevant": "Gesellschaftlich relevant",
|
||||
"translate": "übersetzen",
|
||||
"joinClass": "Klasse beitreten",
|
||||
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
||||
"invalidFormat": "Ungültiges Format",
|
||||
"submitCode": "senden",
|
||||
"members": "Mitglieder",
|
||||
"themes": "Themen",
|
||||
"choose-theme": "Wähle ein thema",
|
||||
"choose-age": "Alter auswählen",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"noLearningPathsFoundDescription": "Es gibt keine Lernpfade, die zu Ihrem Suchbegriff passen.",
|
||||
"legendNotCompletedYet": "Noch nicht fertig",
|
||||
"legendCompleted": "Fertig",
|
||||
"legendTeacherExclusive": "Information für Lehrkräfte"
|
||||
"legendTeacherExclusive": "Information für Lehrkräfte",
|
||||
"code": "code",
|
||||
"class": "Klasse",
|
||||
"invitations": "Einladungen",
|
||||
"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.",
|
||||
"classname": "Klassenname",
|
||||
"EnterNameOfClass": "einen Klassennamen eingeben.",
|
||||
"create": "erstellen",
|
||||
"sender": "Absender",
|
||||
"nameIsMandatory": "Der Klassenname ist ein Pflichtfeld",
|
||||
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
||||
"close": "schließen",
|
||||
"copied": "kopiert!",
|
||||
"accept": "akzeptieren",
|
||||
"deny": "ablehnen",
|
||||
"sent": "sent",
|
||||
"failed": "gescheitert",
|
||||
"wrong": "etwas ist schief gelaufen",
|
||||
"created": "erstellt"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
"sociallyRelevant": "Socially relevant",
|
||||
"login": "log in",
|
||||
"translate": "translate",
|
||||
"joinClass": "Join class",
|
||||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||
"invalidFormat": "Invalid format.",
|
||||
"submitCode": "submit",
|
||||
"members": "members",
|
||||
"themes": "Themes",
|
||||
"choose-theme": "Select a theme",
|
||||
"choose-age": "Select age",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"high-school": "16-18 years old",
|
||||
"older": "18 and older"
|
||||
},
|
||||
"read-more": "Read more"
|
||||
"read-more": "Read more",
|
||||
"code": "code",
|
||||
"class": "class",
|
||||
"invitations": "invitations",
|
||||
"createClass": "create class",
|
||||
"classname": "classname",
|
||||
"EnterNameOfClass": "Enter a classname.",
|
||||
"create": "create",
|
||||
"sender": "sender",
|
||||
"nameIsMandatory": "classname is mandatory",
|
||||
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
||||
"close": "close",
|
||||
"copied": "copied!",
|
||||
"accept": "accept",
|
||||
"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.",
|
||||
"sent": "sent",
|
||||
"failed": "failed",
|
||||
"wrong": "something went wrong",
|
||||
"created": "created"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
"inclusive": "Inclusif",
|
||||
"sociallyRelevant": "Socialement pertinent",
|
||||
"translate": "traduire",
|
||||
"joinClass": "Rejoindre une classe",
|
||||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||
"invalidFormat": "Format non valide.",
|
||||
"submitCode": "envoyer",
|
||||
"members": "membres",
|
||||
"themes": "Thèmes",
|
||||
"choose-theme": "Choisis un thème",
|
||||
"choose-age": "Choisis un âge",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"high-school": "16-18 ans",
|
||||
"older": "18 et plus"
|
||||
},
|
||||
"read-more": "En savoir plus"
|
||||
"read-more": "En savoir plus",
|
||||
"code": "code",
|
||||
"class": "classe",
|
||||
"invitations": "invitations",
|
||||
"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.",
|
||||
"classname": "nom de classe",
|
||||
"EnterNameOfClass": "saisir un nom de classe.",
|
||||
"create": "créer",
|
||||
"sender": "expéditeur",
|
||||
"nameIsMandatory": "le nom de classe est obligatoire",
|
||||
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
||||
"close": "fermer",
|
||||
"copied": "copié!",
|
||||
"accept": "accepter",
|
||||
"deny": "refuser",
|
||||
"sent": "envoyé",
|
||||
"failed": "échoué",
|
||||
"wrong": "quelque chose n'a pas fonctionné",
|
||||
"created": "créé"
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
"sociallyRelevant": "Maatschappelijk relevant",
|
||||
"login": "log in",
|
||||
"translate": "vertalen",
|
||||
"joinClass": "Word lid van een klas",
|
||||
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
||||
"invalidFormat": "Ongeldig formaat.",
|
||||
"submitCode": "verzenden",
|
||||
"members": "leden",
|
||||
"themes": "Lesthema's",
|
||||
"choose-theme": "Kies een thema",
|
||||
"choose-age": "Kies een leeftijd",
|
||||
|
@ -51,5 +56,24 @@
|
|||
"high-school": "3e graad secundair",
|
||||
"older": "Hoger onderwijs"
|
||||
},
|
||||
"read-more": "Lees meer"
|
||||
"read-more": "Lees meer",
|
||||
"code": "code",
|
||||
"class": "klas",
|
||||
"invitations": "uitnodigingen",
|
||||
"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.",
|
||||
"classname": "klasnaam",
|
||||
"EnterNameOfClass": "Geef een klasnaam op.",
|
||||
"create": "aanmaken",
|
||||
"sender": "afzender",
|
||||
"nameIsMandatory": "klasnaam is verplicht",
|
||||
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
||||
"close": "sluiten",
|
||||
"copied": "gekopieerd!",
|
||||
"accept": "accepteren",
|
||||
"deny": "weigeren",
|
||||
"sent": "verzonden",
|
||||
"failed": "mislukt",
|
||||
"wrong": "er ging iets verkeerd",
|
||||
"created": "gecreëerd"
|
||||
}
|
||||
|
|
51
frontend/src/queries/answers.ts
Normal file
51
frontend/src/queries/answers.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import type { QuestionId } from "@dwengo-1/common/dist/interfaces/question.ts";
|
||||
import { type MaybeRefOrGetter, toValue } from "vue";
|
||||
import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts";
|
||||
import type { AnswerData } from "@dwengo-1/common/dist/interfaces/answer.ts";
|
||||
|
||||
// TODO caching
|
||||
|
||||
export function useAnswersQuery(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
full: MaybeRefOrGetter<boolean> = true,
|
||||
): UseQueryReturnType<AnswersResponse, Error> {
|
||||
return useQuery({
|
||||
queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)),
|
||||
enabled: () => Boolean(toValue(questionId)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useAnswerQuery(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
sequenceNumber: MaybeRefOrGetter<number>,
|
||||
): UseQueryReturnType<AnswerResponse, Error> {
|
||||
return useQuery({
|
||||
queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)),
|
||||
enabled: () => Boolean(toValue(questionId)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateAnswerMutation(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> {
|
||||
return useMutation({
|
||||
mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteAnswerMutation(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
): UseMutationReturnType<AnswerResponse, Error, number, unknown> {
|
||||
return useMutation({
|
||||
mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAnswerMutation(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> {
|
||||
return useMutation({
|
||||
mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data),
|
||||
});
|
||||
}
|
93
frontend/src/queries/questions.ts
Normal file
93
frontend/src/queries/questions.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { QuestionController, type QuestionResponse, type QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { QuestionData, QuestionId } from "@dwengo-1/common/interfaces/question";
|
||||
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
import { computed, type MaybeRefOrGetter, toValue } from "vue";
|
||||
import {
|
||||
useMutation,
|
||||
type UseMutationReturnType,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
|
||||
export function questionsQueryKey(
|
||||
loId: LearningObjectIdentifierDTO,
|
||||
full: boolean,
|
||||
): [string, string, number, string, boolean] {
|
||||
return ["questions", loId.hruid, loId.version, loId.language, full];
|
||||
}
|
||||
|
||||
export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] {
|
||||
const loId = questionId.learningObjectIdentifier;
|
||||
return ["question", loId.hruid, loId.version, loId.language, questionId.sequenceNumber];
|
||||
}
|
||||
|
||||
export function useQuestionsQuery(
|
||||
loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>,
|
||||
full: MaybeRefOrGetter<boolean> = true,
|
||||
): UseQueryReturnType<QuestionsResponse, Error> {
|
||||
return useQuery({
|
||||
queryKey: computed(() => questionsQueryKey(toValue(loId), toValue(full))),
|
||||
queryFn: async () => new QuestionController(toValue(loId)).getAll(toValue(full)),
|
||||
enabled: () => Boolean(toValue(loId)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useQuestionQuery(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
): UseQueryReturnType<QuestionResponse, Error> {
|
||||
const loId = toValue(questionId).learningObjectIdentifier;
|
||||
const sequenceNumber = toValue(questionId).sequenceNumber;
|
||||
return useQuery({
|
||||
queryKey: computed(() => questionQueryKey(loId, sequenceNumber)),
|
||||
queryFn: async () => new QuestionController(loId).getBy(sequenceNumber),
|
||||
enabled: () => Boolean(toValue(questionId)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateQuestionMutation(
|
||||
loId: MaybeRefOrGetter<LearningObjectIdentifierDTO>,
|
||||
): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data) => new QuestionController(toValue(loId)).create(data),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
|
||||
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateQuestionMutation(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
): UseMutationReturnType<QuestionResponse, Error, QuestionData, unknown> {
|
||||
const queryClient = useQueryClient();
|
||||
const loId = toValue(questionId).learningObjectIdentifier;
|
||||
const sequenceNumber = toValue(questionId).sequenceNumber;
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (data) => new QuestionController(loId).update(sequenceNumber, data),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
|
||||
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
|
||||
await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteQuestionMutation(
|
||||
questionId: MaybeRefOrGetter<QuestionId>,
|
||||
): UseMutationReturnType<QuestionResponse, Error, void, unknown> {
|
||||
const queryClient = useQueryClient();
|
||||
const loId = toValue(questionId).learningObjectIdentifier;
|
||||
const sequenceNumber = toValue(questionId).sequenceNumber;
|
||||
return useMutation({
|
||||
mutationFn: async () => new QuestionController(loId).remove(sequenceNumber),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
|
||||
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
|
||||
await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) });
|
||||
},
|
||||
});
|
||||
}
|
|
@ -19,7 +19,7 @@ import type { AssignmentsResponse } from "@/controllers/assignments.ts";
|
|||
import type { GroupsResponse } from "@/controllers/groups.ts";
|
||||
import type { SubmissionsResponse } from "@/controllers/submissions.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { StudentDTO } from "@dwengo-1/interfaces/student";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
|
||||
const studentController = new StudentController();
|
||||
|
||||
|
@ -179,7 +179,7 @@ export function useCreateJoinRequestMutation(): UseMutationReturnType<
|
|||
mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId),
|
||||
onSuccess: async (newJoinRequest) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester),
|
||||
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester.username),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -196,7 +196,7 @@ export function useDeleteJoinRequestMutation(): UseMutationReturnType<
|
|||
return useMutation({
|
||||
mutationFn: async ({ username, classId }) => studentController.deleteJoinRequest(username, classId),
|
||||
onSuccess: async (deletedJoinRequest) => {
|
||||
const username = deletedJoinRequest.request.requester;
|
||||
const username = deletedJoinRequest.request.requester.username;
|
||||
const classId = deletedJoinRequest.request.class;
|
||||
await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) });
|
||||
await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) });
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { computed, toValue } from "vue";
|
||||
import type { MaybeRefOrGetter } from "vue";
|
||||
import { useMutation, useQuery, useQueryClient, UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseMutationReturnType,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { TeacherDTO } from "@dwengo-1/interfaces/teacher";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
||||
|
||||
const teacherController = new TeacherController();
|
||||
|
|
|
@ -1,7 +1,224 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { onMounted, ref } from "vue";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ClassController, type ClassResponse } from "@/controllers/classes";
|
||||
import type { StudentsResponse } from "@/controllers/students";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// Username of logged in teacher
|
||||
const username = ref<string | undefined>(undefined);
|
||||
const classController: ClassController = new ClassController();
|
||||
|
||||
// Find class id from route
|
||||
const route = useRoute();
|
||||
const classId: string = route.params.id as string;
|
||||
|
||||
const isLoading = ref(true);
|
||||
const currentClass = ref<ClassDTO | undefined>(undefined);
|
||||
const students = ref<StudentDTO[]>([]);
|
||||
|
||||
// Find the username of the logged in user so it can be used to fetch other information
|
||||
// When loading the page
|
||||
onMounted(async () => {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject?.profile?.preferred_username ?? undefined;
|
||||
|
||||
// Get class of which information should be shown
|
||||
const classResponse: ClassResponse = await classController.getById(classId);
|
||||
if (classResponse && classResponse.class) {
|
||||
currentClass.value = classResponse.class;
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
// Fetch all students of the class
|
||||
const studentsResponse: StudentsResponse = await classController.getStudents(classId);
|
||||
if (studentsResponse && studentsResponse.students) students.value = studentsResponse.students as StudentDTO[];
|
||||
});
|
||||
|
||||
// TODO: Boolean that handles visibility for dialogs
|
||||
// Popup to verify removing student
|
||||
const dialog = ref(false);
|
||||
const selectedStudent = ref<StudentDTO | null>(null);
|
||||
|
||||
function showPopup(s: StudentDTO): void {
|
||||
selectedStudent.value = s;
|
||||
dialog.value = true;
|
||||
}
|
||||
|
||||
// Remove student from class
|
||||
function removeStudentFromclass(): void {
|
||||
dialog.value = false;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
<main>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="text-center py-10"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ currentClass!.displayName }}</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="ma-4"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
fluid
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("students") }}</th>
|
||||
<th class="header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="s in students"
|
||||
:key="s.id"
|
||||
>
|
||||
<td>
|
||||
{{ s.firstName + " " + s.lastName }}
|
||||
</td>
|
||||
<td>
|
||||
<v-btn @click="showPopup"> {{ t("remove") }} </v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">{{ t("areusure") }}</v-card-title>
|
||||
|
||||
<style scoped></style>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
text
|
||||
@click="dialog = false"
|
||||
>
|
||||
{{ t("cancel") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
@click="removeStudentFromclass"
|
||||
>{{ t("yes") }}</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</main>
|
||||
</template>
|
||||
<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 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,380 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { computed, onMounted, ref, type ComputedRef } from "vue";
|
||||
import { validate, version } from "uuid";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students";
|
||||
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
|
||||
import { StudentController } from "@/controllers/students";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import { TeacherController } from "@/controllers/teachers";
|
||||
|
||||
const { t } = useI18n();
|
||||
const studentController: StudentController = new StudentController();
|
||||
const teacherController: TeacherController = new TeacherController();
|
||||
|
||||
// Username of logged in student
|
||||
const username = ref<string | undefined>(undefined);
|
||||
|
||||
// Find the username of the logged in user so it can be used to fetch other information
|
||||
// When loading the page
|
||||
onMounted(async () => {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject?.profile?.preferred_username ?? undefined;
|
||||
});
|
||||
|
||||
// Fetch all classes of the logged in student
|
||||
const { data: classesResponse, isLoading, error } = useStudentClassesQuery(username);
|
||||
|
||||
// Empty list when classes are not yet loaded, else the list of classes of the user
|
||||
const classes: ComputedRef<ClassDTO[]> = computed(() => {
|
||||
// The classes are not yet fetched
|
||||
if (!classesResponse.value) {
|
||||
return [];
|
||||
}
|
||||
// The user has no classes
|
||||
if (classesResponse.value.classes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return classesResponse.value.classes as ClassDTO[];
|
||||
});
|
||||
|
||||
// Students of selected class are shown when logged in student presses on the member count
|
||||
const selectedClass = ref<ClassDTO | null>(null);
|
||||
const students = ref<StudentDTO[]>([]);
|
||||
const teachers = ref<TeacherDTO[]>([]);
|
||||
const getStudents = ref(false);
|
||||
|
||||
// Boolean that handles visibility for dialogs
|
||||
// Clicking on membercount will show a dialog with all members
|
||||
const dialog = ref(false);
|
||||
|
||||
// Function to display all members of a class in a dialog
|
||||
async function openStudentDialog(c: ClassDTO): Promise<void> {
|
||||
selectedClass.value = c;
|
||||
|
||||
// Clear previous value
|
||||
getStudents.value = true;
|
||||
students.value = [];
|
||||
dialog.value = true;
|
||||
|
||||
// Fetch students from their usernames to display their full names
|
||||
const studentDTOs: (StudentDTO | null)[] = await Promise.all(
|
||||
c.students.map(async (uid) => {
|
||||
try {
|
||||
const res = await studentController.getByUsername(uid);
|
||||
return res.student;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Only show students that are not fetched ass *null*
|
||||
students.value = studentDTOs.filter(Boolean) as StudentDTO[];
|
||||
}
|
||||
|
||||
async function openTeacherDialog(c: ClassDTO): Promise<void> {
|
||||
selectedClass.value = c;
|
||||
|
||||
// Clear previous value
|
||||
getStudents.value = false;
|
||||
teachers.value = [];
|
||||
dialog.value = true;
|
||||
|
||||
// Fetch names of teachers
|
||||
const teacherDTOs: (TeacherDTO | null)[] = await Promise.all(
|
||||
c.teachers.map(async (uid) => {
|
||||
try {
|
||||
const res = await teacherController.getByUsername(uid);
|
||||
return res.teacher;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
teachers.value = teacherDTOs.filter(Boolean) as TeacherDTO[];
|
||||
}
|
||||
|
||||
// Hold the code a student gives in to join a class
|
||||
const code = ref<string>("");
|
||||
|
||||
// The code a student sends in to join a class needs to be formatted as v4 to be valid
|
||||
// These rules are used to display a message to the user if they use a code that has an invalid format
|
||||
const codeRules = [
|
||||
(value: string | undefined): string | boolean => {
|
||||
if (value === undefined || value === "") {
|
||||
return true;
|
||||
} else if (value !== undefined && validate(value) && version(value) === 4) {
|
||||
return true;
|
||||
}
|
||||
return t("invalidFormat");
|
||||
},
|
||||
];
|
||||
|
||||
// Used to send the actual class join request
|
||||
const { mutate } = useCreateJoinRequestMutation();
|
||||
|
||||
// Function called when a student submits a code to join a class
|
||||
function submitCode(): void {
|
||||
// Check if the code is valid
|
||||
if (code.value !== undefined && validate(code.value) && version(code.value) === 4) {
|
||||
mutate(
|
||||
{ username: username.value!, classId: code.value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showSnackbar(t("sent"), "success");
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
},
|
||||
},
|
||||
);
|
||||
code.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
const snackbar = ref({
|
||||
visible: false,
|
||||
message: "",
|
||||
color: "success",
|
||||
});
|
||||
|
||||
function showSnackbar(message: string, color: string): void {
|
||||
snackbar.value.message = message;
|
||||
snackbar.value.color = color;
|
||||
snackbar.value.visible = true;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
<main>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="text-center py-10"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<style scoped></style>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="text-center py-10 text-error"
|
||||
>
|
||||
<v-icon large>mdi-alert-circle</v-icon>
|
||||
<p>Error loading: {{ error.message }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="ma-4"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
fluid
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("classes") }}</th>
|
||||
<th class="header">{{ t("teachers") }}</th>
|
||||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="c in classes"
|
||||
:key="c.id"
|
||||
>
|
||||
<td>{{ c.displayName }}</td>
|
||||
<td
|
||||
class="link"
|
||||
@click="openTeacherDialog(c)"
|
||||
>
|
||||
{{ c.teachers.length }}
|
||||
</td>
|
||||
<td
|
||||
class="link"
|
||||
@click="openStudentDialog(c)"
|
||||
>
|
||||
{{ c.students.length }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
width="400"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title> {{ selectedClass?.displayName }} </v-card-title>
|
||||
<v-card-text>
|
||||
<ul v-if="getStudents">
|
||||
<li
|
||||
v-for="student in students"
|
||||
:key="student.username"
|
||||
>
|
||||
{{ student.firstName + " " + student.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="teacher in teachers"
|
||||
:key="teacher.username"
|
||||
>
|
||||
{{ teacher.firstName + " " + teacher.lastName }}
|
||||
</li>
|
||||
</ul>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialog = false"
|
||||
>Close</v-btn
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<div>
|
||||
<div class="join">
|
||||
<h2>{{ t("joinClass") }}</h2>
|
||||
<p>{{ t("JoinClassExplanation") }}</p>
|
||||
|
||||
<v-sheet
|
||||
class="pa-4 sheet"
|
||||
max-width="400"
|
||||
>
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
label="CODE"
|
||||
v-model="code"
|
||||
placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX"
|
||||
:rules="codeRules"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mt-4"
|
||||
color="#f6faf2"
|
||||
type="submit"
|
||||
@click="submitCode"
|
||||
block
|
||||
>{{ t("submitCode") }}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-sheet>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-snackbar
|
||||
v-model="snackbar.visible"
|
||||
:color="snackbar.color"
|
||||
timeout="3000"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</main>
|
||||
</template>
|
||||
<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 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,404 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import { computed, onMounted, ref, type ComputedRef } from "vue";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||
import type { TeacherInvitationDTO } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||
import { useTeacherClassesQuery } from "@/queries/teachers";
|
||||
import { ClassController, type ClassResponse } from "@/controllers/classes";
|
||||
|
||||
const { t } = useI18n();
|
||||
const classController = new ClassController();
|
||||
|
||||
// Username of logged in teacher
|
||||
const username = ref<string | undefined>(undefined);
|
||||
|
||||
// Find the username of the logged in user so it can be used to fetch other information
|
||||
// When loading the page
|
||||
onMounted(async () => {
|
||||
const userObject = await authState.loadUser();
|
||||
username.value = userObject?.profile?.preferred_username ?? undefined;
|
||||
});
|
||||
|
||||
// Fetch all classes of the logged in teacher
|
||||
const { data: classesResponse, isLoading, error, refetch } = useTeacherClassesQuery(username, true);
|
||||
|
||||
// Empty list when classes are not yet loaded, else the list of classes of the user
|
||||
const classes: ComputedRef<ClassDTO[]> = computed(() => {
|
||||
// The classes are not yet fetched
|
||||
if (!classesResponse.value) {
|
||||
return [];
|
||||
}
|
||||
// The user has no classes
|
||||
if (classesResponse.value.classes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return classesResponse.value.classes as ClassDTO[];
|
||||
});
|
||||
|
||||
// Boolean that handles visibility for dialogs
|
||||
// Creating a class will generate a popup with the generated code
|
||||
const dialog = ref(false);
|
||||
|
||||
// Code generated when new class was created
|
||||
const code = ref<string>("");
|
||||
|
||||
// TODO: waiting on frontend controllers
|
||||
const invitations = ref<TeacherInvitationDTO[]>([]);
|
||||
|
||||
// Function to handle a accepted invitation request
|
||||
function acceptRequest(): void {
|
||||
//TODO: avoid linting issues when merging by filling the function
|
||||
invitations.value = [];
|
||||
}
|
||||
|
||||
// Function to handle a denied invitation request
|
||||
function denyRequest(): void {
|
||||
//TODO: avoid linting issues when merging by filling the function
|
||||
invitations.value = [];
|
||||
}
|
||||
|
||||
// Teacher should be able to set a displayname when making a class
|
||||
const className = ref<string>("");
|
||||
|
||||
// The name can only contain dash, underscore letters and numbers
|
||||
// These rules are used to display a message to the user if the name is not valid
|
||||
const nameRules = [
|
||||
(value: string | undefined): string | boolean => {
|
||||
if (!value) return true;
|
||||
if (value && /^[a-zA-Z0-9_-]+$/.test(value)) return true;
|
||||
return t("onlyUse");
|
||||
},
|
||||
];
|
||||
|
||||
// Function called when a teacher creates a class
|
||||
async function createClass(): Promise<void> {
|
||||
// Check if the class name is valid
|
||||
if (className.value && className.value.length > 0 && /^[a-zA-Z0-9_-]+$/.test(className.value)) {
|
||||
try {
|
||||
const classDto: ClassDTO = {
|
||||
id: "",
|
||||
displayName: className.value,
|
||||
teachers: [username.value!],
|
||||
students: [],
|
||||
joinRequests: [],
|
||||
};
|
||||
const classResponse: ClassResponse = await classController.createClass(classDto);
|
||||
const createdClass: ClassDTO = classResponse.class;
|
||||
code.value = createdClass.id;
|
||||
dialog.value = true;
|
||||
showSnackbar(t("created"), "success");
|
||||
|
||||
// Reload the table with classes so the new class appears
|
||||
await refetch();
|
||||
} catch (_) {
|
||||
showSnackbar(t("wrong"), "error");
|
||||
}
|
||||
}
|
||||
if (!className.value || className.value === "") {
|
||||
showSnackbar(t("name is mandatory"), "error");
|
||||
}
|
||||
}
|
||||
|
||||
const snackbar = ref({
|
||||
visible: false,
|
||||
message: "",
|
||||
color: "success",
|
||||
});
|
||||
|
||||
function showSnackbar(message: string, color: string): void {
|
||||
snackbar.value.message = message;
|
||||
snackbar.value.color = color;
|
||||
snackbar.value.visible = true;
|
||||
}
|
||||
|
||||
// Show the teacher, copying of the code was a successs
|
||||
const copied = ref(false);
|
||||
|
||||
// Copy the generated code to the clipboard
|
||||
async function copyToClipboard(): Promise<void> {
|
||||
await navigator.clipboard.writeText(code.value);
|
||||
copied.value = true;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<main></main>
|
||||
</template>
|
||||
<main>
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="text-center py-10"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
/>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<style scoped></style>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="text-center py-10 text-error"
|
||||
>
|
||||
<v-icon large>mdi-alert-circle</v-icon>
|
||||
<p>Error loading: {{ error.message }}</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="ma-4"
|
||||
>
|
||||
<v-row
|
||||
no-gutters
|
||||
fluid
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("classes") }}</th>
|
||||
<th class="header">
|
||||
{{ t("code") }}
|
||||
</th>
|
||||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="c in classes"
|
||||
:key="c.id"
|
||||
>
|
||||
<td>
|
||||
<v-btn
|
||||
:to="`/class/${c.id}`"
|
||||
variant="text"
|
||||
>
|
||||
{{ c.displayName }}
|
||||
<v-icon end> mdi-menu-right </v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>{{ c.id }}</td>
|
||||
<td>{{ c.students.length }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="6"
|
||||
>
|
||||
<div>
|
||||
<h2>{{ t("createClass") }}</h2>
|
||||
|
||||
<v-sheet
|
||||
class="pa-4 sheet"
|
||||
max-width="600px"
|
||||
>
|
||||
<p>{{ t("createClassInstructions") }}</p>
|
||||
<v-form @submit.prevent>
|
||||
<v-text-field
|
||||
class="mt-4"
|
||||
:label="`${t('classname')}`"
|
||||
v-model="className"
|
||||
:placeholder="`${t('EnterNameOfClass')}`"
|
||||
:rules="nameRules"
|
||||
variant="outlined"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
class="mt-4"
|
||||
color="#f6faf2"
|
||||
type="submit"
|
||||
@click="createClass"
|
||||
block
|
||||
>{{ t("create") }}</v-btn
|
||||
>
|
||||
</v-form>
|
||||
</v-sheet>
|
||||
<v-container>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
max-width="400px"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title class="headline">code</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="code"
|
||||
readonly
|
||||
append-inner-icon="mdi-content-copy"
|
||||
@click:append-inner="copyToClipboard"
|
||||
></v-text-field>
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-if="copied"
|
||||
class="text-center mt-2"
|
||||
>
|
||||
{{ t("copied") }}
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
text
|
||||
@click="
|
||||
dialog = false;
|
||||
copied = false;
|
||||
"
|
||||
>
|
||||
{{ t("close") }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<h1 class="title">
|
||||
{{ t("invitations") }}
|
||||
</h1>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("class") }}</th>
|
||||
<th class="header">{{ t("sender") }}</th>
|
||||
<th class="header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="i in invitations"
|
||||
:key="(i.class as ClassDTO).id"
|
||||
>
|
||||
<td>
|
||||
{{ (i.class as ClassDTO).displayName }}
|
||||
</td>
|
||||
<td>{{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }}</td>
|
||||
<td class="text-right">
|
||||
<div>
|
||||
<v-btn
|
||||
color="green"
|
||||
@click="acceptRequest"
|
||||
class="mr-2"
|
||||
>
|
||||
{{ t("accept") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="red"
|
||||
@click="denyRequest"
|
||||
>
|
||||
{{ t("deny") }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</div>
|
||||
<v-snackbar
|
||||
v-model="snackbar.visible"
|
||||
:color="snackbar.color"
|
||||
timeout="3000"
|
||||
>
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
</main>
|
||||
</template>
|
||||
<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 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #0b75bb;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import authState from "@/services/auth/auth-service.ts";
|
||||
import TeacherClasses from "./TeacherClasses.vue";
|
||||
import StudentClasses from "./StudentClasses.vue";
|
||||
|
||||
// Determine if role is student or teacher to render correct view
|
||||
const role: string = authState.authState.activeRole!;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main></main>
|
||||
<main>
|
||||
<TeacherClasses v-if="role === 'teacher'"></TeacherClasses>
|
||||
<StudentClasses v-else></StudentClasses>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue